Orchestrate
Subagents
Let an agent hand a focused task to a specialist agent with its own tools.
A subagent is a specialist that a lead agent hands one focused task to. It has its own tools, starts with a fresh context, and only its answer comes back to the lead. A tool on the lead calls it by key, like any other Actor.
import { createRegistry, defineExtension } from "@earendil-works/pi-durable";
import { pi } from "@rivet-dev/pi";
import { setup } from "rivetkit";
import { askSpecialist, engineering, orders } from "./specialists";
const extensions = createRegistry();
extensions.install(defineExtension({ name: "support", tools: [askSpecialist] }));
const support = pi({ model: "anthropic/claude-opus-5-5", registry: extensions });
export const registry = setup({ use: { support, orders, engineering } });
registry.start();
import { Type } from "@earendil-works/pi-ai";
import { createRegistry, defineExtension, defineTool, ROOT_CONVERSATION_ID } from "@earendil-works/pi-durable";
import { CodingTools } from "@earendil-works/pi-durable/tools";
import { pi } from "@rivet-dev/pi";
import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b";
import type { Registry } from "rivetkit";
import { createClient } from "rivetkit/client";
import { getOrder } from "./get-order";
const orderTools = createRegistry();
orderTools.install(defineExtension({ name: "orders", tools: [getOrder] }));
export const orders = pi({ model: "openai/gpt-5.4-mini", registry: orderTools });
const codingTools = createRegistry();
codingTools.install(CodingTools);
export const engineering = pi({ model: "anthropic/claude-opus-5-5", registry: codingTools, sandbox: e2bProvider() });
export const askSpecialist = defineTool({
name: "ask_specialist",
description:
"Hand one focused question to a specialist and get its answer. Use orders for order status, refunds, and shipping. Use engineering for bugs that need someone to read or run the code.",
parameters: Type.Object({
specialist: Type.Union([Type.Literal("orders"), Type.Literal("engineering")]),
question: Type.String({ description: "Everything the specialist needs. It can't see this conversation." }),
}),
// A rerun reaches the same specialist Actor, through the memo below.
replay: "safe",
execute: async ({ specialist, question }, api, context) => {
// The memo is saved with this tool call, so a rerun gets the same id.
const id = await api.memo("specialist-id", crypto.randomUUID(), context);
const agent = client[specialist].getOrCreate([id]);
context.abortSignal?.addEventListener("abort", () => void agent.conversation.abort(ROOT_CONVERSATION_ID));
const result = await agent.prompt(question, { requestId: id });
if (result.status === "unanswered") {
return { content: [{ type: "text", text: `The specialist did not answer: ${result.reason}` }], isError: true };
}
const answer = result.text || "The specialist finished without an answer.";
return { content: [{ type: "text", text: answer }], details: { specialist } };
},
});
const client = createClient<Registry<{ orders: typeof orders; engineering: typeof engineering }>>();
import { createClient } from "rivetkit/client";
import type { registry } from "./server";
const client = createClient<typeof registry>();
const support = client.support.getOrCreate(["ticket-4821"]);
const result = await support.prompt(
"Order 1042 shows as delivered but never arrived, and the tracking page throws a 500 error. Repo: https://github.com/acme/storefront",
);
console.log(result.status === "done" ? result.text : `Unanswered: ${result.reason}`);
The get_order tool in get-order.ts is the one from Custom Tools.
- The model picks the specialist and writes the question. The question has to carry everything the specialist needs, because the specialist can’t see the lead’s conversation.
- Each specialist has only the tools for its job:
ordershasget_order, andengineeringhas a sandbox to read and run code. The support agent has neither. - Each question goes to a new specialist with an empty conversation. The tool keys it by an id that
api.memosaves with the tool call. - The tool has
replay: "safe", so Pi runs it again after a crash. The rerun gets the same id and passes it as therequestId, so it waits for the run already in progress instead of asking again. - If the lead’s run is cancelled, the tool cancels the specialist’s run too.
- The tool waits for the answer and returns it as the tool result. To hand work off without waiting, see Agent-to-Agent Messages.