fix(steer): deliver /steer as a user turn so hardened models stop rejecting it - #37618
fix(steer): deliver /steer as a user turn so hardened models stop rejecting it#37618beardthelion wants to merge 4 commits into
Conversation
|
Superseded by #40240 (merged 2026-06-06, commit The maintainer took the structural-provenance goal this PR argued for but kept the steer in the alternation-safe tool slot rather than moving it to a user turn: Verified fixed on Closing as superseded — thanks for the wire-safety analysis and the minimax-m3 corroboration, both of which fed the framing that landed. |
What does this PR do?
Alternative fix for the
/steerfalse-positive on injection-resistant models, offered alongside #36938 at the issue author's request to compare both approaches head-to-head./steeris flagged as a possible prompt-injection attempt by hardened models because the steer text is appended into the lastrole:"tool"message, immediately after the<untrusted_tool_result>fence. The model sees an imperative claiming user authority inside the channel it is trained to distrust, so its indirect-injection defense fires on legitimate operator input. Confirmed independently onminimax-m3(rejected a steer as a "user-injected instruction in the tool output ... not from you directly"), in addition to the Opus 4.8 report in #36934, so this is model-class behavior rather than one model's quirk.Where #36938 keeps the steer in the tool channel and relabels the marker (lexical provenance), this PR removes the steer from that channel entirely and delivers it as a real
role:"user"message after the tool batch (structural provenance). The two are complementary; #36938 remains the minimal-risk option.Why it is wire-safe: delivering after every
tool_call_idis answered is valid OpenAI alternation. On the Anthropic wire,_merge_consecutive_rolesfolds the trailing user message into the same user turn as thetool_resultblocks, yielding one valid turn[tool_result, ..., {type:"text", text: steer}].repair_message_sequencealready treats a user turn as closing a tool-result run. This addresses the role-alternation concern raised in #36934 / #36938 (the issue author has acknowledged it does not hold for post-batch delivery). Prompt-cache impact is symmetric with the relabel approach, since editing the last tool message also invalidates the suffix from that point.Related Issue
Refs #36934. Alternative to #36938 (same issue, different approach).
Type of Change
Changes Made
agent/agent_runtime_helpers.py: renameapply_pending_steer_to_tool_resultstodeliver_pending_steer_as_user_turn. When the message tail is a completed tool result, append{"role":"user", "content": "[The user sent this mid-task via /steer]\n<text>"}; otherwise restash the steer for the next-turn fallback.agent/tool_executor.py: route both aggregate drain sites (parallel + sequential) through the renamed method at the batch boundary; remove the two per-tool drains. Mid-batch delivery would insert a user message before latertool_call_ids are answered, whichrepair_message_sequencethen drops as orphans. The model only reads at the next API call (after the whole batch), so per-tool vs aggregate delivery is indistinguishable to it.agent/conversation_loop.py: replace the inline pre-API tool-content append with the same helper call.run_agent.py: rename theAIAgentforwarder method to match.tests/run_agent/test_steer.py: rewriteTestSteerInjectionandTestPreApiCallSteerDrainto assert user-turn delivery; addTestSteerCallSitesguarding that the old name is gone and delivery happens only at the two batch boundaries.tests/run_agent/test_concurrent_interrupt.py: rename the stubbed attribute.tests/agent/test_steer_anthropic_wire.py(new): integration test thatconvert_messages_to_anthropicmerges the steer user turn into the tool-result turn.How to Test
scripts/run_tests.sh tests/run_agent/test_steer.py tests/run_agent/test_concurrent_interrupt.py tests/agent/test_steer_anthropic_wire.py(22 tests pass).scripts/run_tests.sh tests/gateway/test_steer_command.py tests/gateway/test_busy_session_ack.py tests/cli/test_cli_steer_busy_path.py tests/acp_adapter/test_acp_commands.py(35 tests pass)./steer <instruction>mid-tool-batch; the steer is honored instead of being flagged as injection. The new Anthropic-wire test pins the serialization that makes this safe.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings): docstrings on the renamed helper explain the why; no user-facing docs affectedcli-config.yaml.exampleif I added/changed config keys: N/A (no config keys)CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows: N/A