{"record":{"id":"d64d93a84f2c072b","repo":"can1357/oh-my-pi","slug":"cannot-continue-from-message-role-assistant","errorCode":null,"errorMessage":"Cannot continue from message role: assistant","messagePattern":"Cannot continue from message role: assistant","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/agent-loop.ts","lineNumber":599,"sourceCode":" *\n * **Important:** The last message in context must convert to a `user` or `toolResult` message\n * via `convertToLlm` — except for an assistant tail with unpaired runnable\n * tool calls (see {@link unpairedToolCallTail}), which resumes by executing\n * those calls first. Any other assistant tail is rejected here; other invalid\n * tails cannot be validated since `convertToLlm` is only called once per turn.\n */\nexport function agentLoopContinue(\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\tsignal?: AbortSignal,\n\tstreamFn?: StreamFn,\n): EventStream<AgentEvent, AgentMessage[]> {\n\tif (context.messages.length === 0) {\n\t\tthrow new Error(\"Cannot continue: no messages in context\");\n\t}\n\n\tif (context.messages[context.messages.length - 1].role === \"assistant\" && !unpairedToolCallTail(context.messages)) {\n\t\tthrow new Error(\"Cannot continue from message role: assistant\");\n\t}\n\n\tconst stream = createAgentStream();\n\n\t(async () => {\n\t\tconst newMessages: AgentMessage[] = [];\n\t\tconst currentContext: AgentContext = { ...context, messages: [...context.messages] };\n\n\t\tstream.push({ type: \"agent_start\" });\n\n\t\ttry {\n\t\t\tawait runLoop(currentContext, newMessages, config, signal, stream, streamFn);\n\t\t} catch (err) {\n\t\t\tstream.fail(err);\n\t\t}\n\t})();\n\n\treturn stream;","sourceCodeStart":581,"sourceCodeEnd":617,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/agent/src/agent-loop.ts#L581-L617","documentation":"agentLoopContinue requires the last message to be awaiting a response. If the final message has role \"assistant\" — and it is not an unpaired tool-call tail that legitimately resumes pending tool calls — continuing would produce an assistant-to-assistant turn, which providers reject or which makes no protocol sense, so it throws. Exception: a trailing assistant message with unpaired tool calls is allowed so the loop can resume tool execution.","triggerScenarios":"Calling agentLoopContinue right after an assistant reply completed (last message role is \"assistant\", with no pending tool calls); manually appending an assistant message then calling continue.","commonSituations":"Resume/retry logic that captured the context after a finished turn instead of after the user reply; buggy session serialization that records the assistant turn last; caller intent to 'regenerate' implemented by calling continue instead of a regenerate API.","solutions":["Append a new user message (or tool results) before calling continue.","If regenerating the last assistant reply, drop the trailing assistant message from context.messages first.","If resuming pending tool calls, ensure the trailing assistant message keeps its unpaired tool calls so the unpairedToolCallTail check passes."],"exampleFix":"// before\n// last message is assistant; regenerate desired\nawait agentLoopContinue(context, config);\n// after\nif (context.messages.at(-1)?.role === \"assistant\") {\n  context.messages.pop(); // drop completed assistant turn\n}\nawait agentLoopContinue(context, config);","handlingStrategy":"validation","validationCode":"const last = context.messages[context.messages.length - 1];\nif (last?.role === \"assistant\" && !hasUnpairedToolCalls(last)) {\n  context.messages.pop(); // or append a user/tool message\n}","typeGuard":"function lastExpectsResponse(context: AgentContext): boolean {\n  const last = context.messages[context.messages.length - 1];\n  if (!last) return false;\n  return last.role === \"user\" || last.role === \"toolResult\" || hasUnpairedToolCalls(last);\n}","tryCatchPattern":"try {\n  return agentLoopContinue(context, config);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"Cannot continue from message role\")) {\n    context.messages.pop();\n    return agentLoopContinue(context, config);\n  }\n  throw err;\n}","preventionTips":["Call continue only when the last message is a user/tool message or an unpaired tool-call tail.","For regenerate semantics, drop the trailing assistant message first.","Keep tool-call pairing intact when serializing/restoring sessions."],"tags":["agent-loop","usage-error","message-role","protocol"],"backgroundTag":"invalid-message-role","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}