Skip to main content
Core

Session Lifecycle

What a conversation is, the events one prompt produces, and how to steer or cancel a run.

A conversation is one thread of messages inside an agent. Each agent has a root conversation, which prompt uses by default, and can hold more conversations. A conversation:

  • Keeps the full history of messages and tool calls.
  • Handles one run at a time, calling the model and tools until the model is done. Prompts that arrive during a run wait in a queue.
  • Streams events to every client that watches it.
  • Summarizes older messages when the context fills up.
  • Survives sleep, crashes, and upgrades. See Architecture.
ClientAgentcalls the modelSandboxruns toolsprompteventstool callresultrepeats until the model stops calling tools

This client connects to the Quickstart agent, watches the root conversation, sends one prompt, and prints the events it receives:

import { createClient } from "rivetkit/client";
import type { registry } from "../quickstart/server";

const client = createClient<typeof registry>();
const conn = client.agent.getOrCreate(["user-123"]).connect();

// The text of the current answer printed so far.
let printed = "";

conn.on("pi.events", ({ events }) => {
	for (const event of events) {
		if (event.type === "run_start") console.log("[run]");
		if (event.type === "message_update") {
			for (const change of event.changes) {
				if (change.type === "text_delta") {
					process.stdout.write(change.delta);
					printed += change.delta;
				}
			}
		}
		if (event.type === "message_end") {
			// A short answer can arrive whole here, with no text_delta before it.
			const message = event.entry.model?.[0];
			if (message?.role === "assistant") {
				const text = message.content
					.flatMap((block) => (block.type === "text" ? [block.text] : []))
					.join("");
				process.stdout.write(text.slice(printed.length));
			}
			printed = "";
		}
		if (event.type === "tool_execution_start") console.log(`\n> ${event.toolName}`);
		if (event.type === "compaction_start") console.log("\n[compacting]");
		if (event.type === "run_end") console.log("\n[done]");
	}
});

const root = await conn.harness.root();
await conn.conversation.watchEvents(root.id);

await conn.prompt("Write an isPalindrome function in TypeScript to palindrome.ts, and add tests for it in palindrome.test.ts.");
await conn.dispose();

Events

The conversation.watchEvents(id) action returns a snapshot of the conversation, including a partial answer. Then the agent sends pi.events to that connection only. Each frame has conversationId, seq, and an events array. A frame with seq 0 replaces everything the client had, and a gap in seq means call watchEvents again.

Event typeWhen it fires
run_start, run_endA run starts or ends.
turn_start, turn_endA model response starts, or it and its tool calls are done.
message_start, message_endA message starts or is complete: your prompt, a model reply, or a tool result.
message_updateThe model streams output. Each item in changes has a type, such as text_delta, thinking_delta, or toolcall_delta.
tool_execution_start, tool_execution_endA tool starts or finishes.
auto_retry_start, auto_retry_endA failed model call is retried.
compaction_start, compaction_endOlder messages are summarized to free up context.
usage_changedThe conversation’s token and cost totals change.

A short answer can arrive whole in message_end, with no text_delta before it, so print the rest of the text there. Events are not replayed. To read earlier messages, call conversation.context(id), which returns { messages }. To stop listening, call conversation.unwatchEvents(id).

Steer, follow up, or cancel

While a run is in progress, prompt(text, { whenBusy }) decides what happens to the new prompt:

whenBusyWhat it does
"followUp"The default. The agent reads the message once it has no tool calls left, and the run continues.
"steer"The agent reads the message after its current tool calls finish, before its next model call.
"reject"The prompt fails with an error, and nothing is queued.

On an idle conversation, the prompt runs right away. To cancel a run, call conversation.abort(id). It stops any command still running in the sandbox. Daytona can’t cancel a command, so it runs until it exits or times out.

Only abort stops a run. If prompt times out or its client disconnects, prompt rejects, and the run keeps going until it finishes. Send the prompt again with the same requestId to get its result.

Compaction and errors

  • When the context fills up, the agent summarizes older messages and keeps going. Call conversation.compact(id) to do it yourself. The conversation keeps working while the summary is written.
  • A failed model call is retried automatically. If it keeps failing, prompt returns { status: "unanswered", reason }, and the conversation is kept.

Next: Client SDK, how to call an agent from your backend or a browser.