{"record":{"id":"a262bd2ce29e460e","repo":"earendil-works/pi","slug":"cannot-continue-from-message-role-assistant-a262bd","errorCode":null,"errorMessage":"Cannot continue from message role: assistant","messagePattern":"Cannot continue from message role: assistant","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/agent.ts","lineNumber":384,"sourceCode":"\t\tconst lastMessage = this._state.messages[this._state.messages.length - 1];\n\t\tif (!lastMessage) {\n\t\t\tthrow new Error(\"No messages to continue from\");\n\t\t}\n\n\t\tif (lastMessage.role === \"assistant\") {\n\t\t\tconst queuedSteering = this.steeringQueue.drain();\n\t\t\tif (queuedSteering.length > 0) {\n\t\t\t\tawait this.runPromptMessages(queuedSteering, { skipInitialSteeringPoll: true });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst queuedFollowUps = this.followUpQueue.drain();\n\t\t\tif (queuedFollowUps.length > 0) {\n\t\t\t\tawait this.runPromptMessages(queuedFollowUps);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthrow new Error(\"Cannot continue from message role: assistant\");\n\t\t}\n\n\t\tawait this.runContinuation();\n\t}\n\n\tprivate normalizePromptInput(\n\t\tinput: string | AgentMessage | AgentMessage[],\n\t\timages?: ImageContent[],\n\t): AgentMessage[] {\n\t\tif (Array.isArray(input)) {\n\t\t\treturn input;\n\t\t}\n\n\t\tif (typeof input !== \"string\") {\n\t\t\treturn [input];\n\t\t}\n\n\t\tconst content: Array<TextContent | ImageContent> = [{ type: \"text\", text: input }];","sourceCodeStart":366,"sourceCodeEnd":402,"githubUrl":"https://github.com/earendil-works/pi/blob/4af9d21d3b4d664e4a29fcabfec85171077248e3/packages/agent/src/agent.ts#L366-L402","documentation":"Agent.continue() requires the transcript to end with a user or toolResult message. When the last message is assistant, it first tries to rescue the call by draining queued steering messages, then queued follow-ups; only when both queues are empty does it throw. Hitting it means the agent already finished its answer and you asked it to continue with no new input - send a new prompt instead.","triggerScenarios":"Calling continue() right after a completed run whose final message is the assistant reply, with nothing queued via steer()/followUp(); resume/regenerate buttons mapped to continue() after a normal finish; continue loops that run after every turn without checking the last role.","commonSituations":"Chat UI continue buttons; orchestrators iterating continue() after each run; tests that continue a finished conversation.","solutions":["Send a new user message with prompt() (or queue one via followUp() and then continue()) instead of bare continue().","Before continuing, check agent.state.messages.at(-1)?.role and skip or prompt when it is 'assistant'.","Remember steer()/followUp() messages are drained automatically by continue() - queue before calling if you want them used."],"exampleFix":"// before\nawait agent.prompt(\"hi\");\nawait agent.continue(); // last message assistant, queues empty -> throws\n\n// after\nawait agent.prompt(\"hi\");\nawait agent.followUp({ role: \"user\", content: [{ type: \"text\", text: \"keep going\" }], timestamp: Date.now() });\nawait agent.continue(); // drains the queued follow-up","handlingStrategy":"validation","validationCode":"const last = agent.state.messages.at(-1);\nif (last && last.role === \"assistant\" && !agent.hasQueuedMessages()) {\n  await agent.prompt(\"continue\"); // new input required\n} else {\n  await agent.continue(); // queues drain automatically\n}","typeGuard":"function canContinue(agent: Agent): boolean {\n  const last = agent.state.messages.at(-1);\n  return last !== undefined && (last.role !== \"assistant\" || agent.hasQueuedMessages());\n}","tryCatchPattern":"try {\n  await agent.continue();\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"role: assistant\")) {\n    await agent.prompt(\"keep going\"); // fallback: send new input\n  } else {\n    throw err;\n  }\n}","preventionTips":["Check the transcript tail role before calling continue().","Queue follow-ups before continue() if you want them drained on this call.","Do not loop continue() after every run without new input."],"tags":["agent","continue","message-role","queueing"],"backgroundTag":"invalid-last-message-role","analyzedSha":"4af9d21d3b4d664e4a29fcabfec85171077248e3","analyzedAt":"2026-08-24T13:07:14.692Z","schemaVersion":2},"datasetVersion":"2026-08-24T17:17:21.512Z"}