Extensions
Bundle tools, prompt sections, hooks, and tasks into named extensions, and choose which ones each conversation uses.
An extension is a named bundle of what an agent runs with: tools, prompt sections, hooks, tool wrappers, and tasks. You install extensions in a registry and pass it to pi(). Each conversation stores the names of the extensions it uses.
| Field | Adds | See |
|---|---|---|
tools | Functions the model can call. | Custom Tools |
sections | Parts of the system prompt. | Instructions |
hooks | Code that runs around model requests, tool calls, and compaction. | Hooks |
wraps | Decorators for a tool or section, by name. | Wrap a tool |
tasks | Multi-step work that resumes after a crash. | Tools and tasks |
By default, every conversation uses every installed extension, in install order. When two selected extensions have a tool with the same name, the later one wins.
Hooks
A hook runs inside a built-in task, such as a tool call, in every conversation that selects its extension. This extension blocks the tools that change files or run commands:
import { defineExtension, hook, section, ToolTask } from "@earendil-works/pi-durable";
const changesFiles = new Set(["write", "edit", "bash"]);
export const readOnly = defineExtension({
name: "read-only",
sections: [section("mode", () => "You are in read-only mode. Read files and explain them, but do not change anything.")],
hooks: [
hook(ToolTask, {
// A blocked call never runs. The model gets the message as an error result.
beforeTool: (call) => (changesFiles.has(call.name) ? { block: `${call.name} is turned off in read-only mode.` } : undefined),
}),
],
});
| Hook | Task | Runs | Can |
|---|---|---|---|
beforeTool | ToolTask | Before a tool call runs. | Block the call or rewrite its arguments. A throw also blocks it. |
afterTool | ToolTask | After a tool call returns. | Replace the result. |
beforeRequest | GenerationTask | Before every model request. | Replace the messages of that request only. |
afterResponse | GenerationTask | After every model response. | Observe it. |
afterTools | GenerationTask | After every tool call of a round finishes. | Observe the results. |
onYield | GenerationTask | When the model gives a final answer. | Continue the run with another user message. |
beforeCompact | CompactionTask | Before a compaction summarizes the conversation. | Decline it or supply your own summary. |
Hooks run in the agent Actor on your worker, so they can call your APIs. To enforce a rule in every conversation, keep its extension in the default selection. To approve tool calls one by one, see Human in the Loop.
Wrap a tool
wrapTool decorates a tool by name, whichever extension supplied it. This one logs every bash command:
import { defineExtension, wrapTool } from "@earendil-works/pi-durable";
import { createBashTool } from "@earendil-works/pi-durable/tools";
export const audit = defineExtension({
name: "audit",
wraps: [
// Wraps whichever `bash` tool the conversation ends up with.
wrapTool(createBashTool(), (bash) => ({
...bash,
execute: (args, api, context) => {
console.log(`[audit] ${api.conversationId}: ${args.command}`);
return bash.execute(args, api, context);
},
})),
],
});
wrapSection does the same for a prompt section.
Choose extensions per conversation
import { createRegistry } 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 { setup } from "rivetkit";
import { audit } from "./audit";
import { readOnly } from "./read-only";
const extensions = createRegistry();
extensions.install(CodingTools);
extensions.install(audit);
extensions.install(readOnly);
const agent = pi({
model: "anthropic/claude-opus-5-5",
registry: extensions,
sandbox: e2bProvider(),
// New conversations start without read-only. A client turns it on per conversation.
settings: { extensions: [CodingTools, audit] },
});
export const registry = setup({ use: { agent } });
registry.start();
import { createClient } from "rivetkit/client";
import type { registry } from "./server";
const client = createClient<typeof registry>();
const agent = client.agent.getOrCreate(["acme", "checkout-review"]);
const root = await agent.harness.root();
// Add read-only to this conversation only. Extensions are passed by name.
await agent.conversation.configure(root.id, { extensions: { add: ["read-only"] } });
const answer = await agent.prompt("Where are checkout totals computed?");
console.log(answer.status === "done" ? answer.text : `Unanswered: ${answer.reason}`);
// Go back to the default selection, CodingTools and audit.
await agent.conversation.configure(root.id, { extensions: null });
const { extensions } = await agent.conversation.agent(root.id);
console.log(extensions); // ["coding-tools", "audit"]
settings.extensionsis the default selection. Without it, conversations use every installed extension.{ add, remove }changes the default for one conversation. An array selects exactly those extensions, in order.nullgoes back to the default.- Clients pass names. A name that isn’t installed rejects with a
UserError. - Changes apply to the next step. A model request that already started keeps its tools and prompt. A tool call that hasn’t started yet uses the new hooks.
The conversation.agent action returns the conversation’s extensions, tools, and prompt sections by name.
Deploys
The registry lives in your worker’s code, and every agent Actor on that worker shares it. Pi saves only the names of the extensions a conversation selects.
- Install extensions at startup. Installing one later changes only the worker that runs the code.
- Keep names stable. Renaming an extension is the same as removing it.
- A removed extension stops applying. Conversations that select it lose its tools, sections, and hooks, and get them back when a later deploy installs it again.
- Tasks wait for their extension. A task from an extension’s
tasksresumes only once that extension is installed, so keep it installed while its tasks can run. - Running tool calls finish on the old code while the Actor drains. See Upgrades and crashes.
Next: User Subscriptions, how users run agents on their own Claude or ChatGPT plan.