Skip to content

feat(anthropic): use native mid-conversation system messages on Fable 5/Mythos 5/Opus 4.8 - #68275

Closed
trevornk wants to merge 2 commits into
NousResearch:mainfrom
trevornk:feat/anthropic-mid-conversation-system-messages
Closed

trevornk wants to merge 2 commits into
NousResearch:mainfrom
trevornk:feat/anthropic-mid-conversation-system-messages

Conversation

@trevornk

@trevornk trevornk commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Anthropic's native Messages API supports mid-conversation {"role": "system"} entries on Claude Fable 5, Claude Mythos 5, and Claude Opus 4.8. Hermes can use that shape for a real runtime /steer without rewriting the top-level system prompt or invalidating its cached prefix.

Hermes still appends the visible [OUT-OF-BAND USER MESSAGE] marker to the active tool result for role-alternation-safe fallback behavior. Marker text is presentation only and is never trusted as provenance. The runtime also records the exact steer text in private, top-level internal metadata on the canonical tool message. The Anthropic adapter promotes a steer only when that runtime-owned metadata exists and the exact corresponding marker can be removed from the exact converted tool_result block.

Raw tool output, web content, files, or MCP responses that imitate the public marker remain ordinary tool output.

Implementation

  • Adds private _hermes_trusted_steer metadata only at Hermes's two runtime steer-injection paths.
  • Consumes that metadata during Anthropic conversion; it is not serialized to Anthropic, OpenAI Chat Completions, Responses API, or Bedrock payloads.
  • Binds provenance to the exact converted tool_result object rather than a sanitized tool-call ID, preventing collisions such as call.a / call_a from cross-binding instructions.
  • Promotes every structurally trusted historical steer at the same position on every replay, immediately after its tool-result user turn. This keeps the outgoing message prefix stable across requests.
  • Leaves canonical Hermes history unchanged.
  • Gates native promotion to Fable 5 / Mythos 5 / Opus 4.8 on native Anthropic endpoints. Sonnet 5, GPT, and third-party Anthropic-compatible endpoints retain the inline fallback.

Security and cache invariants

  • Public delimiter text cannot authorize a system message.
  • Internal metadata never reaches provider payloads.
  • Tool-result/system/assistant ordering remains Anthropic-placement compliant.
  • Historical replays are deterministic and cache-prefix stable.
  • Colliding or duplicate tool IDs cannot associate a steer with the wrong result.

Testing

Focused coverage includes model/endpoint gating, string and block tool-result shapes, genuine runtime metadata injection, forged-marker rejection, exact-text matching, historical replay stability, duplicate/sanitized-ID collisions, provider payload metadata exclusion, and role ordering.

pytest -q tests/agent/test_anthropic_adapter.py -k 'MidConversation or TrustedSteer' tests/run_agent/test_steer.py
21 passed

An independent post-fix review found no remaining security or logic blockers.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/anthropic Anthropic native Messages API labels Jul 21, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the narrowly scoped provider-side implementation. One blocking issue needs redesign before it is safe to salvage.

Problems

  • agent/anthropic_adapter.py:2548-2561 parses static delimiters from a tool_result and promotes the captured text as role: system. The delimiters are public static strings in agent/prompt_builder.py:637-643, and current Hermes appends them to ordinary tool-result content at agent/agent_runtime_helpers.py:3739-3752. A tool can therefore return identical delimiter-shaped content and have it re-labeled as user input with system authority. This conflicts with the current trust contract in agent/prompt_builder.py:651-656; Anthropic's mid-conversation-system-message documentation also explicitly warns not to elevate raw tool output.
  • The new tests construct accepted markers with format_steer_marker (tests/agent/test_anthropic_adapter.py:1408-1438, PR head) but do not test forged marker text returned by a tool.

Suggested changes

  • Preserve trusted /steer provenance separately through the adapter; do not infer it by parsing tool-output text.
  • Add adversarial tests proving delimiter-shaped raw tool output stays in the tool_result and does not produce a system message.

Automated hermes-sweeper review.

Comment thread agent/anthropic_adapter.py Outdated
for block in content:
if not (isinstance(block, dict) and block.get("type") == "tool_result"):
continue
new_inner, steer_text = _extract_trailing_steer_marker(block.get("content"))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: _extract_trailing_steer_marker() recognizes only public static text, not provenance. A tool can return the exact marker delimiters; this path then strips that output and lines 2555-2561 elevate it to a system message labeled as user input. Carry a trusted steer flag/value separately from tool-result text and add a forged-marker regression test.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 30, 2026
@trevornk
trevornk force-pushed the feat/anthropic-mid-conversation-system-messages branch from 672e3f5 to e91523e Compare August 26, 2026 22:03
Hermes Coder added 2 commits September 3, 2026 18:32
… 5/Mythos 5/Opus 4.8

Anthropic's Messages API accepts a {"role": "system"} entry inside
messages at the exact point an instruction becomes relevant, instead of
only via the top-level system field. GA (no beta header) on Claude
Fable 5, Claude Mythos 5, and Claude Opus 4.8; not available on Sonnet 5
or third-party Anthropic-compatible endpoints. It carries the same
operator-level authority as the top-level system field, but because it's
appended at the end of the message list it doesn't invalidate the cached
prefix the way editing the top-level system string would.

Hermes already relays mid-turn /steer input as a trailing
[OUT-OF-BAND USER MESSAGE] marker appended to the last tool result
(agent/prompt_builder.py:format_steer_marker,
agent/agent_runtime_helpers.py:apply_pending_steer_to_tool_results) so it
rides a role-alternation-safe slot on every model/provider. That path is
completely unchanged and remains the only option for Sonnet 5, GPT, and
third-party endpoints.

For the three gated models on a native Anthropic endpoint, promote the
trailing marker to a real mid-conversation system message instead, at
the Anthropic API boundary in convert_messages_to_anthropic — the same
place _manage_thinking_signatures already does endpoint/model-specific
rewriting. The canonical Hermes-internal message history and every other
code path (session persistence, compression, gateway steer plumbing)
are untouched; this only reshapes the outgoing Anthropic payload for the
three gated models, mirroring Anthropic's own guidance to phrase the
injected content as context ("new input arrived...") rather than a
command overriding the user.

Only the newest marker (result[-1], the one Hermes just appended for
this turn) is ever promoted; markers already embedded deeper in history
from a prior turn are left as-is since they're already part of a cached
prefix.

24 new tests covering: model gating (including OpenRouter-prefixed and
dated model names), marker extraction from both plain-string and
multimodal tool_result content, promotion behavior end-to-end through
convert_messages_to_anthropic, and no-ops for every non-qualifying case
(wrong model, third-party endpoint, no marker present, malformed
trailing message shape).
@trevornk
trevornk force-pushed the feat/anthropic-mid-conversation-system-messages branch from e91523e to b47b41f Compare September 4, 2026 00:17
@trevornk

trevornk commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (63279301bc) — was CONFLICTING/DIRTY, now MERGEABLE/CLEAN at b47b41fe10. Both commits preserved. Final shape is 7 files, +421/-8.

This was the messiest of the three: agent/anthropic_adapter.py and anthropic_message_convert.py have been reworked substantially upstream since the PR was opened, so the conflicts were resolved by adapting the feature onto main's current structure rather than reverting main's work. Checked first whether main had already landed native mid-conversation system messages — it had not, so the PR is still needed. _model_supports_mid_conversation_system is wired into the pushed adapter.

Verified locally on Python 3.11.16:

tests/agent/test_anthropic_adapter.py tests/run_agent/test_steer.py .... 158 passed in 8.07s

Worth highlighting the second commit for reviewers, since it is the security-relevant half: live steers are now identified by structural provenance (TRUSTED_STEER_KEY set on the message dict at injection time in conversation_loop.py) instead of by matching the rendered marker text. Text-matching provenance is spoofable — model- or tool-produced content can imitate the marker — whereas the structural key can only be set by the code that actually performed the injection. That is the right direction independent of the mid-conversation-system feature.

Broad-suite runs were skipped: this machine hit its memory ceiling and the OOM-killer was SIGKILLing pytest. The targeted numbers above are real; CI is the right place for the full sweep.

@trevornk

Copy link
Copy Markdown
Contributor Author

Closing this one. It has gone stale and now conflicts with main, and rather than leave a conflicted branch sitting in the review queue I would rather clear it out. If the underlying issue is still present and someone wants it, I am happy to redo it fresh against current main — the branch stays on my fork, so nothing here is lost.

@trevornk trevornk closed this Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have provider/anthropic Anthropic native Messages API sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants