earendil-works/pi · error · Error

Cannot continue from message role: assistant

Error message

Cannot continue from message role: assistant

What it means

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.

Source

Thrown at packages/agent/src/agent.ts:384

		const lastMessage = this._state.messages[this._state.messages.length - 1];
		if (!lastMessage) {
			throw new Error("No messages to continue from");
		}

		if (lastMessage.role === "assistant") {
			const queuedSteering = this.steeringQueue.drain();
			if (queuedSteering.length > 0) {
				await this.runPromptMessages(queuedSteering, { skipInitialSteeringPoll: true });
				return;
			}

			const queuedFollowUps = this.followUpQueue.drain();
			if (queuedFollowUps.length > 0) {
				await this.runPromptMessages(queuedFollowUps);
				return;
			}

			throw new Error("Cannot continue from message role: assistant");
		}

		await this.runContinuation();
	}

	private normalizePromptInput(
		input: string | AgentMessage | AgentMessage[],
		images?: ImageContent[],
	): AgentMessage[] {
		if (Array.isArray(input)) {
			return input;
		}

		if (typeof input !== "string") {
			return [input];
		}

		const content: Array<TextContent | ImageContent> = [{ type: "text", text: input }];

View on GitHub (pinned to 4af9d21d3b)

Solutions

  1. Send a new user message with prompt() (or queue one via followUp() and then continue()) instead of bare continue().
  2. Before continuing, check agent.state.messages.at(-1)?.role and skip or prompt when it is 'assistant'.
  3. Remember steer()/followUp() messages are drained automatically by continue() - queue before calling if you want them used.

Example fix

// before
await agent.prompt("hi");
await agent.continue(); // last message assistant, queues empty -> throws

// after
await agent.prompt("hi");
await agent.followUp({ role: "user", content: [{ type: "text", text: "keep going" }], timestamp: Date.now() });
await agent.continue(); // drains the queued follow-up
Defensive patterns

Strategy: validation

Validate before calling

const last = agent.state.messages.at(-1);
if (last && last.role === "assistant" && !agent.hasQueuedMessages()) {
  await agent.prompt("continue"); // new input required
} else {
  await agent.continue(); // queues drain automatically
}

Type guard

function canContinue(agent: Agent): boolean {
  const last = agent.state.messages.at(-1);
  return last !== undefined && (last.role !== "assistant" || agent.hasQueuedMessages());
}

Try / catch

try {
  await agent.continue();
} catch (err) {
  if (err instanceof Error && err.message.includes("role: assistant")) {
    await agent.prompt("keep going"); // fallback: send new input
  } else {
    throw err;
  }
}

Prevention

When it happens

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

Common situations: Chat UI continue buttons; orchestrators iterating continue() after each run; tests that continue a finished conversation.

Related errors


AI-assisted analysis of earendil-works/pi@4af9d21d3b (2026-08-24). Data as JSON: /api/errors/a262bd2ce29e460e. Report an issue: GitHub.