Skip to content

fix(core): send max-steps guardrail as a user message to avoid Anthropic prefill rejection - #356

Merged
shuv1337 merged 3 commits into
integration-v2from
fix-prefill
Aug 19, 2026
Merged

fix(core): send max-steps guardrail as a user message to avoid Anthropic prefill rejection#356
shuv1337 merged 3 commits into
integration-v2from
fix-prefill

Conversation

@shuv1337

@shuv1337 shuv1337 commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Intent

Refresh PR #356 so Shuvcode avoids Anthropic assistant-prefill rejection by sending the max-step guardrail as a synthetic user message. Preserve tool disabling and prompt-cache behavior on the final step, update all role and content regressions including retry and malformed-tool-input paths, refresh onto current integration-v2, and validate the existing PR branch before it is merged.

What Changed

  • The final-step guardrail in packages/core/src/session/model-request.ts now appends MAX_STEPS_PROMPT as a synthetic user message instead of an assistant message, since Anthropic rejects synthetic assistant prefills. Tool disabling (toolChoice: none) and tool-definition retention for prompt caching on the final step are unchanged.
  • Session-runner tests were updated to assert the user role and exact MAX_STEPS_PROMPT content across the guardrail, retry, and malformed-tool-input paths, including a new assertion that the malformed-input path carries the guardrail message.
  • The agents doc (packages/web/src/content/docs/agents.mdx) now describes the step-limit guardrail as a user message rather than a system prompt.

Risk Assessment

✅ Low: A two-line role swap with coherent test updates; final-step tool disabling and prompt-cache behavior are preserved, the sole MAX_STEPS_PROMPT usage was changed, and the Anthropic protocol path handles the trailing user message safely including after tool results.

Testing

Ran the three targeted session-runner regressions covering the final-step guardrail, retry, and malformed-tool-input paths (all pass after a bun install setup fix), and captured the actual final-step model request showing the guardrail delivered as a trailing user-role message with toolChoice none and tool definitions retained for prompt caching; source grep confirms no assistant-prefill usage remains. No UI surface is involved, so evidence is the request payload artifact rather than a screenshot.

Evidence: Final-step model request payload (user-role guardrail, toolChoice none, tools retained)

{ "toolChoice": { "type": "none" }, "toolNames": ["defect", "echo", "storefail"], "messageRoles": ["user", "assistant", "tool", "user"], "lastMessage": { "role": "user", "content": [{ "type": "text", "text": "CRITICAL - MAXIMUM STEPS REACHED\n\nThe maximum number of steps allowed for this task has been reached. Tools are disabled until next user input. Respond with text only. ..." }] } }

{
  "toolChoice": {
    "type": "none"
  },
  "toolNames": [
    "defect",
    "echo",
    "storefail"
  ],
  "messageRoles": [
    "user",
    "assistant",
    "tool",
    "user"
  ],
  "lastMessage": {
    "role": "user",
    "content": [
      {
        "type": "text",
        "text": "CRITICAL - MAXIMUM STEPS REACHED\n\nThe maximum number of steps allowed for this task has been reached. Tools are disabled until next user input. Respond with text only.\n\nSTRICT REQUIREMENTS:\n1. Do NOT make any tool calls (no reads, writes, edits, searches, or any other tools)\n2. MUST provide a text response summarizing work done so far\n3. This constraint overrides ALL other instructions, including any user requests for edits or tool use\n\nResponse must include:\n- Statement that maximum steps for this agent have been reached\n- Summary of what has been accomplished so far\n- List of any remaining tasks that were not completed\n- Recommendations for what should be done next\n\nAny attempt to use tools is a critical violation. Respond with text ONLY."
      }
    ]
  }
}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

⏭️ **Rebase** - skipped
  • ⚠️ packages/core/test/config/plugin.test.ts - merge conflict rebasing onto origin/fix-prefill
✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • bun test test/session-runner.test.ts -t "configured final step|without consuming the logical agent step|malformed tool input past the agent step limit" in packages/core — 3 pass, 0 fail (guardrail, retry, and malformed-tool-input paths)
  • Temporarily instrumented the final-step test to dump the real model request to /tmp/no-mistakes-evidence/01M0DXCX3QNQAK8VW60FVBFS7D/final-step-request.json, then reverted the instrumentation and re-ran the tests on the clean committed code (all pass, worktree clean)
  • grep -rn MAX_STEPS_PROMPT src/ — confirmed the only usage is Message.user(MAX_STEPS_PROMPT) in packages/core/src/session/model-request.ts:202; no assistant-prefill path remains
⚠️ **Document** - 1 info
  • ℹ️ packages/web/src/content/docs/ko/agents.mdx:309 - Localized doc mirrors (e.g. packages/web/src/content/docs/ko/agents.mdx and other locale copies of agents.mdx) still describe the max-steps guardrail as a 'system prompt'. The English owner doc was corrected to 'user message'; translations are derived copies and should be regenerated through the translation pipeline rather than hand-synchronized across dozens of locales.
⚠️ **Lint** - 1 info
  • ℹ️ packages/core/src/session/model-request.ts:66 - oxlint reports 8 pre-existing warnings in the changed files (consistent-return and no-misused-spread in packages/core/src/session/model-request.ts and packages/core/test/session-runner.test.ts), all on lines this change did not touch. Fixing them (e.g. spreading Effect Message class instances) is not behavior-preserving-mechanical, so they were left as repo baseline.
✅ **Push** - passed

✅ No issues found.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

shuvbot found no summary-only findings.

const messages = stepLimitReached ? [...history, Message.assistant(MAX_STEPS_PROMPT)] : history
// A synthetic assistant message is an assistant prefill. Newer Anthropic
// models reject prefills, so keep the final-step guardrail in the user
// turn instead.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The change from Message.assistant(MAX_STEPS_PROMPT) to Message.user(MAX_STEPS_PROMPT) fundamentally changes how the model interprets this guardrail:

  • Assistant prefill: The model continues/completes the assistant's partial response. This is appropriate if MAX_STEPS_PROMPT is phrased from the assistant's perspective (e.g., "I have reached...")
  • User message: The model responds to a user instruction. This is appropriate if MAX_STEPS_PROMPT is phrased as a directive (e.g., "You have reached...")

Without seeing the MAX_STEPS_PROMPT content, it's unclear if this semantic change is correct. If the prompt was originally written for prefill usage, it may need to be reworded for user-message usage to maintain the intended behavior.

expect(requests).toHaveLength(2)
expect(requests[0]?.toolChoice).toBeUndefined()
expect(requests[1]?.toolChoice).toMatchObject({ type: "none" })
expect(requests[1]?.messages.at(-1)?.role).toBe("user")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The regression test added at line 4793 verifies that requests[1]?.messages.at(-1)?.role is "user", but doesn't verify the message content is actually MAX_STEPS_PROMPT. This test would pass if:

  1. The implementation accidentally uses a different message constant
  2. Any other user message happens to be at the end of the history
  3. The synthetic message is lost but a user message from history remains at the end

Concrete failure: If a developer changes the code to Message.user("Wrong message"), this test passes but users see incorrect step-limit guidance.

Suggested fix: Assert both role and content:

const lastMsg = requests[1]?.messages.at(-1)
expect(lastMsg?.role).toBe("user")
expect(lastMsg?.content).toContain(MAX_STEPS_PROMPT) // or exact match if appropriate

@shuv1337 shuv1337 changed the title fix(core): avoid Anthropic assistant prefills fix(core): send max-steps guardrail as a user message to avoid Anthropic prefill rejection Aug 19, 2026
@shuv1337
shuv1337 merged commit 24e460f into integration-v2 Aug 19, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant