{"record":{"id":"bab878ba55424937","repo":"earendil-works/pi","slug":"cannot-continue-no-messages-in-context","errorCode":null,"errorMessage":"Cannot continue: no messages in context","messagePattern":"Cannot continue: no messages in context","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/agent-loop.ts","lineNumber":71,"sourceCode":"\treturn stream;\n}\n\n/**\n * Continue an agent loop from the current context without adding a new message.\n * Used for retries - context already has user message or tool results.\n *\n * **Important:** The last message in context must convert to a `user` or `toolResult` message\n * via `convertToLlm`. If it doesn't, the LLM provider will reject the request.\n * This cannot be validated here since `convertToLlm` is only called once per turn.\n */\nexport function agentLoopContinue(\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\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\") {\n\t\tthrow new Error(\"Cannot continue from message role: assistant\");\n\t}\n\n\tconst stream = createAgentStream();\n\n\tvoid runAgentLoopContinue(\n\t\tcontext,\n\t\tconfig,\n\t\tasync (event) => {\n\t\t\tstream.push(event);\n\t\t},\n\t\tsignal,\n\t\tstreamFn,\n\t).then((messages) => {\n\t\tstream.end(messages);","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/earendil-works/pi/blob/4af9d21d3b4d664e4a29fcabfec85171077248e3/packages/agent/src/agent-loop.ts#L53-L89","documentation":"agentLoopContinue() resumes an agent loop from an existing AgentContext without adding a new message; it exists for retries where the context already ends with a user prompt or tool results. It refuses to start when context.messages is empty because there would be nothing for the LLM to respond to and the provider request would be malformed. The check runs synchronously before the EventStream is created, so the function throws instead of emitting an error event. To start a conversation, use agentLoop() with prompt messages instead.","triggerScenarios":"Calling agentLoopContinue(context, config, signal, streamFn) on a freshly built AgentContext whose messages array is empty; retry helpers that fire after an aborted turn but before any message was committed; contexts assembled by hand where the user message was never appended; calling it after clearing/resetting transcript state.","commonSituations":"Automatic retry wrappers around failed or aborted turns that resume even when nothing was sent; hand-built AgentContext objects in tests; state restoration (e.g. initialState.messages loaded from persisted JSON) that silently yields an empty array.","solutions":["Before calling agentLoopContinue, check context.messages.length > 0; when empty, start a new loop with agentLoop([userMessage], context, ...) instead.","In retry logic, only continue when the context already contains the prompt you sent (inspect the last message role/timestamp).","When restoring state from persistence, validate that the messages array is non-empty and fall back to a fresh prompt."],"exampleFix":"// before\nconst stream = agentLoopContinue(context, config, signal, streamFn); // throws: no messages in context\n\n// after\nconst userMsg: AgentMessage = { role: \"user\", content: [{ type: \"text\", text: \"hello\" }], timestamp: Date.now() };\nconst stream =\n  context.messages.length > 0\n    ? agentLoopContinue(context, config, signal, streamFn)\n    : agentLoop([userMsg], context, config, signal, streamFn);","handlingStrategy":"validation","validationCode":"if (context.messages.length === 0) {\n  // nothing to continue from - start a new loop instead\n  const stream = agentLoop([userMessage], context, config, signal, streamFn);\n} else {\n  const stream = agentLoopContinue(context, config, signal, streamFn);\n}","typeGuard":"function hasContextMessages(context: AgentContext): boolean {\n  return context.messages.length > 0;\n}","tryCatchPattern":"// agentLoopContinue throws synchronously (before the stream is created)\ntry {\n  const stream = agentLoopContinue(context, config, signal, streamFn);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"no messages in context\")) {\n    // fall back to agentLoop with a fresh prompt\n  } else {\n    throw err;\n  }\n}","preventionTips":["Treat agentLoopContinue as a retry API: only call it after a turn that already appended messages to the context.","Build AgentContext in one place and assert messages.length > 0 there during development.","Prefer the Agent class: Agent.continue() has its own empty-transcript error and drains steering/follow-up queues first."],"tags":["agent-loop","precondition","empty-context","sync-throw"],"backgroundTag":"empty-conversation-history","analyzedSha":"4af9d21d3b4d664e4a29fcabfec85171077248e3","analyzedAt":"2026-08-24T13:07:14.692Z","schemaVersion":2},"datasetVersion":"2026-08-24T17:17:21.512Z"}