Skip to main content
Agent

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.

YOUR WORKERRegistrycoding-toolsauditread-onlyConversation Acoding-tools, auditConversation Bcoding-tools, audit, read-onlyby name
FieldAddsSee
toolsFunctions the model can call.Custom Tools
sectionsParts of the system prompt.Instructions
hooksCode that runs around model requests, tool calls, and compaction.Hooks
wrapsDecorators for a tool or section, by name.Wrap a tool
tasksMulti-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),
		}),
	],
});
HookTaskRunsCan
beforeToolToolTaskBefore a tool call runs.Block the call or rewrite its arguments. A throw also blocks it.
afterToolToolTaskAfter a tool call returns.Replace the result.
beforeRequestGenerationTaskBefore every model request.Replace the messages of that request only.
afterResponseGenerationTaskAfter every model response.Observe it.
afterToolsGenerationTaskAfter every tool call of a round finishes.Observe the results.
onYieldGenerationTaskWhen the model gives a final answer.Continue the run with another user message.
beforeCompactCompactionTaskBefore 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

  • settings.extensions is 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. null goes 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 tasks resumes 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.