From 7b891bda66fe8d64f540b994c23af828f194116d Mon Sep 17 00:00:00 2001 From: Rauf Guliyev Date: Wed, 15 Jul 2026 00:02:09 +0000 Subject: [PATCH] fix: send max-steps instruction as user message, not assistant When an agent hits its configured steps cap mid-turn, the loop appended the "wrap up now" instruction as a role: assistant message, leaving the outgoing request ending on an assistant turn. Providers that reject assistant-message prefill bounce this immediately with "This model does not support assistant message prefill." The instruction is directed at the model, so it belongs on a user turn, not tacked onto the assistant role. Adds a regression test asserting the max-steps message is sent as role: user. --- packages/opencode/src/session/prompt.ts | 2 +- packages/opencode/test/session/prompt.test.ts | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index eb116f6b960f..88524a05ed56 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1278,7 +1278,7 @@ const layer = Layer.effect( system, messages: [ ...modelMsgs, - ...(isLastStep ? [{ role: "assistant" as const, content: MAX_STEPS_PROMPT }] : []), + ...(isLastStep ? [{ role: "user" as const, content: MAX_STEPS_PROMPT }] : []), ], tools, model, diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 491ad06aaf47..122f39085bbc 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -514,6 +514,38 @@ it.instance("loop calls LLM and returns assistant message", () => }), ) +it.instance("loop sends max steps instruction as a user message", () => + Effect.gen(function* () { + const { llm } = yield* useServerConfig((url) => ({ + ...providerCfg(url), + agent: { build: { steps: 1 } }, + })) + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const chat = yield* sessions.create({ title: "Pinned" }) + yield* prompt.prompt({ + sessionID: chat.id, + agent: "build", + noReply: true, + parts: [{ type: "text", text: "hello" }], + }) + yield* llm.text("world") + + yield* prompt.loop({ sessionID: chat.id }) + + const hits = yield* llm.hits + expect(hits).toHaveLength(1) + const messages = hits[0]?.body.messages + expect(messages).toBeArray() + if (!Array.isArray(messages)) return + const maxSteps = messages.find( + (message) => JSON.stringify(message).includes("CRITICAL - MAXIMUM STEPS REACHED"), + ) + expect(maxSteps).toMatchObject({ role: "user" }) + }), + 30_000, +) + withMcpInstructions.instance( "loop includes MCP instructions in model system context", () =>