can1357/oh-my-pi · error

Cannot continue from message role: assistant

Error message

Cannot continue from message role: assistant

What it means

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.

Source

Thrown at packages/agent/src/agent-loop.ts:599

 *
 * **Important:** The last message in context must convert to a `user` or `toolResult` message
 * via `convertToLlm` — except for an assistant tail with unpaired runnable
 * tool calls (see {@link unpairedToolCallTail}), which resumes by executing
 * those calls first. Any other assistant tail is rejected here; other invalid
 * tails cannot be validated since `convertToLlm` is only called once per turn.
 */
export function agentLoopContinue(
	context: AgentContext,
	config: AgentLoopConfig,
	signal?: AbortSignal,
	streamFn?: StreamFn,
): EventStream<AgentEvent, AgentMessage[]> {
	if (context.messages.length === 0) {
		throw new Error("Cannot continue: no messages in context");
	}

	if (context.messages[context.messages.length - 1].role === "assistant" && !unpairedToolCallTail(context.messages)) {
		throw new Error("Cannot continue from message role: assistant");
	}

	const stream = createAgentStream();

	(async () => {
		const newMessages: AgentMessage[] = [];
		const currentContext: AgentContext = { ...context, messages: [...context.messages] };

		stream.push({ type: "agent_start" });

		try {
			await runLoop(currentContext, newMessages, config, signal, stream, streamFn);
		} catch (err) {
			stream.fail(err);
		}
	})();

	return stream;

View on GitHub (pinned to 9690622007)

Solutions

  1. Append a new user message (or tool results) before calling continue.
  2. If regenerating the last assistant reply, drop the trailing assistant message from context.messages first.
  3. If resuming pending tool calls, ensure the trailing assistant message keeps its unpaired tool calls so the unpairedToolCallTail check passes.

Example fix

// before
// last message is assistant; regenerate desired
await agentLoopContinue(context, config);
// after
if (context.messages.at(-1)?.role === "assistant") {
  context.messages.pop(); // drop completed assistant turn
}
await agentLoopContinue(context, config);
Defensive patterns

Strategy: validation

Validate before calling

const last = context.messages[context.messages.length - 1];
if (last?.role === "assistant" && !hasUnpairedToolCalls(last)) {
  context.messages.pop(); // or append a user/tool message
}

Type guard

function lastExpectsResponse(context: AgentContext): boolean {
  const last = context.messages[context.messages.length - 1];
  if (!last) return false;
  return last.role === "user" || last.role === "toolResult" || hasUnpairedToolCalls(last);
}

Try / catch

try {
  return agentLoopContinue(context, config);
} catch (err) {
  if (err instanceof Error && err.message.includes("Cannot continue from message role")) {
    context.messages.pop();
    return agentLoopContinue(context, config);
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/d64d93a84f2c072b. Report an issue: GitHub.