Branch Conversations with /fork
Fork an agent into a new thread that keeps the full history up to the fork, keep working in it, and let the original thread continue separately.
A /fork command branches a conversation. The new thread starts with everything said so far and the user keeps working in it, while the original thread goes on separately. Use it to try another direction without losing the current one. For a quick question that leaves no thread behind, see Side Questions with /btw.
Each thread is one Pi agent, keyed by the thread. Branching a thread is a fork of that agent.
What we’ll build:
- A Pi agent per thread, with a
parentaction that names the thread it branched from. - A
/forkthat forks the agent into a new thread with its own key. - Two threads that share the history up to the fork and then go their own ways.
Install
npm add @rivet-dev/pi @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord rivetkit
Create the agent
import { createRegistry } from "@earendil-works/pi-durable";
import { pi } from "@rivet-dev/pi";
import { setup } from "rivetkit";
export const agent = pi({
model: "anthropic/claude-opus-5-5",
registry: createRegistry(),
actions: {
// The thread this one branched from, or null for an original thread.
parent: (c) => c.forkChain.at(-1)?.actorId ?? null,
},
});
export const registry = setup({ use: { agent } });
registry.start();
c.forkChain lists the forks behind an agent, oldest first, so its last entry is the thread this one branched from.
Branch a conversation
import { ROOT_CONVERSATION_ID } from "@earendil-works/pi-durable";
import { createClient } from "rivetkit/client";
import type { registry } from "./server";
const client = createClient<typeof registry>();
async function say(thread: ReturnType<typeof client.agent.get>, text: string) {
const result = await thread.prompt(text, {
requestId: crypto.randomUUID(),
});
console.log(
result.status === "done" ? result.text : `No answer: ${result.reason}`,
);
}
const main = client.agent.getOrCreate(["thread", "main"]);
await say(main, "Plan a three-day trip to Lisbon.");
// /fork: branch the conversation into a new thread with its own key.
const branchKey = ["thread", crypto.randomUUID()];
const branch = await client.agent.fork(await main.resolve(), {
key: branchKey,
});
// A turn the original was in the middle of is copied too. Stop it in the
// branch, so only the original thread finishes it.
await branch.conversation.abort(ROOT_CONVERSATION_ID);
await say(branch, "Redo the plan on a tight budget.");
// The original thread goes on separately.
await say(main, "Add a day trip to Sintra.");
// Later, reopen the branch by its key.
const reopened = client.agent.get(branchKey);
console.log(await reopened.parent()); // the main thread's actor ID
client.agent.forkgives the branch the main thread’s whole history, without copying it, and a key of its own.- If the main thread was in the middle of a turn, the branch copied that turn too.
conversation.abortstops it in the branch, so only the original thread finishes it. - After the fork, each thread keeps its own messages. The branch never sees “Add a day trip to Sintra.”
Try it
export ANTHROPIC_API_KEY=sk-ant-...
npx tsx server.ts
In another terminal, run npx tsx client.ts. It prints the plan, the budget plan from the branch, the main thread’s Sintra plan, and the main thread’s ID as the branch’s parent.
Gotchas
- A key is taken once. A second fork with the same key fails with
actor.duplicate_key. Use a new ID for every branch. When you retry a fork whose reply was lost,actor.duplicate_keymeans the branch exists. - Keep your own list of threads. A fork is not listed under its source. After
forkreturns, record the new thread’s key wherever your app lists threads. - The branch starts from saved steps. Pi saves each step as it goes, so text the agent is writing at the moment of the fork is not in the branch.
- Fork only agents without a sandbox. A fork of an agent with a sandbox copies the record of that sandbox, so both threads would run their tools in the same sandbox.
- Branches cost only what they add. Each thread stores the messages it writes after the fork. The shared history is stored once.