Skip to content

Step-capped agents fail with an assistant prefill 400 #56250

Open

Description

@OddballGreg

Version: @opencode/cli 2.0.24.

Steps to reproduce

  1. Configure an agent with steps: 2.
  2. Select a model behind a provider that does not accept assistant prefill. We saw it with an Anthropic model through the GitLab Duo provider (gitlab/duo-chat-opus-5-5).
  3. Send a prompt that needs more than two tool calls.

Expected: on the last step, the model gets the MAX_STEPS prompt and answers with a text summary.

Result: the last step fails with this provider error (the request_id is removed).

Anthropic API error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"This model does not support assistant message prefill. The conversation must end with a user message."}}

Cause: on the last allowed step, SessionRunner.runStep sends [...messages, Message.assistant(MAX_STEPS_PROMPT)] to request.primary. The request therefore ends on an assistant message.

Suggested fix: send the MAX_STEPS prompt as Message.user(...). Also consider a guard in request.primary that appends a short user turn when the message list ends on an assistant or system message.

Activity

  1. opencode-agent commented on Oct 16, 2026

    @opencode-agent
    Contributor

    Thanks for the clear report and for pinpointing the cause. I reproduced this on 2.0.24, and it still happens on the latest dev (d4a6223).

    I set the build agent to steps: 2 and used a simulated model that calls read on step 1. On step 2, the last allowed step, the request OpenCode sends ends with an assistant message containing the "CRITICAL - MAXIMUM STEPS REACHED" prompt. Anthropic models that don't support assistant prefill reject that kind of request with the 400 you saw. The simulated model can't return Anthropic's real error, so it shows a marker in the TUI when a request ends on an assistant message.

    The line responsible is still in packages/core/src/session/runner/llm.ts: when the step limit is reached, it appends Message.assistant(MAX_STEPS_PROMPT) to the messages.

    The script below reproduces it, and the recording and screenshot show the run.

    Related, ordered by likelihood:

    recording.mp4

    Screenshot

    Reproduction script (drive.ts)
    // Repro for #56250: on the last allowed step (agent `steps: 2`), OpenCode sends a
    // request whose final message is an assistant turn (MAX_STEPS prompt as prefill).
    // Providers/models that forbid assistant prefill (e.g. Anthropic Claude via GitLab Duo)
    // reject such a request with "The conversation must end with a user message".
    import { Effect, Stream } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    const roleOf = (m: any) => m?.role ?? m?.type
    const messagesOf = (body: any): any[] => body?.messages ?? body?.input ?? body?.contents ?? []
    const textOf = (m: any) => JSON.stringify(m?.content ?? m).slice(0, 80)
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true },
        config: { agents: { build: { steps: 2 } } } as any,
        project: { git: true, files: { "a.txt": "A\n", "b.txt": "B\n", "c.txt": "C\n" } },
      },
      ({ ui, llm }) =>
        Effect.gen(function* () {
          yield* llm.serve((request, index) => {
            const msgs = messagesOf(request.body)
            const last = msgs[msgs.length - 1]
            const role = roleOf(last)
            console.log(`REQUEST ${index + 1}: ${msgs.length} messages, last role = ${role}, last = ${textOf(last)}`)
            if (role === "assistant") {
              // Stand-in for the provider's documented 400 for prefill-forbidding models.
              return Stream.make(
                Llm.text(`PREFILL: request ${index + 1} ends with an assistant message -> Anthropic would return 400 "This model does not support assistant message prefill. The conversation must end with a user message."`),
              )
            }
            if (index < 3)
              return Stream.make(
                Llm.toolCall({ index: 0, id: `call_${index}`, name: "read", input: { path: ["a.txt", "b.txt", "c.txt"][index] } }),
                Llm.finish("tool-calls"),
              )
            return Stream.make(Llm.text("Summary: read the files."))
          })
          yield* ui.submit("Read a.txt, b.txt and c.txt one at a time, then summarize.")
          yield* ui.waitFor(
            () => Effect.map(Effect.all([ui.matches("PREFILL:"), ui.matches("Summary:")]), ([a, b]) => a || b),
            { timeout: 60000 },
          )
          yield* Effect.sleep(1500)
          yield* ui.screenshot("final")
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

  2. tlmquintino commented on Oct 18, 2026

    @tlmquintino

    Also seen on 2.0.28 with the anthropic provider (claude-sonnet-5-5), on subagents (mode: subagent, steps: 60) launched through the subagent tool. The impact for subagents is worse than a failed turn: the child's file changes up to the cap stay on disk, but the parent receives only the error, no summary, so its work is silently partial (logged as "Failed to drain Session … does not support assistant message prefill"). We hit it on three long-running subagent tasks in one day. Sending the MAX_STEPS prompt as a user message, as suggested above, would fix it for us.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions