Add a tool
An extension is a TypeScript file in your recipe that Pi loads into every agent session. Use one when the agent needs a stable operation (parse a file, call your API, enforce a format) rather than a procedure it reasons through.
You need: a recipe (Create a recipe).
1. Write the tool
import { type ExtensionFactory, defineTool } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
const lookupOrder = defineTool({
name: "lookup_order",
label: "Look up order",
description: "Read one order record by id.",
parameters: Type.Object({ orderId: Type.String() }),
execute: async (_id, params) => {
const res = await fetch(`https://orders.example.com/v1/orders/${params.orderId}`);
return {
content: [{ type: "text" as const, text: await res.text() }],
details: { orderId: params.orderId },
};
},
});
const extension: ExtensionFactory = (pi) => {
pi.registerTool(lookupOrder);
};
export default extension;The call needs no auth header: bind the host, and Introspection adds the stored header on the way out (Connect services and secrets).
2. Declare it in package.json
Extensions are never found by convention. Pi loads TypeScript directly; there is no build step.
{
"type": "module",
"peerDependencies": {
"@earendil-works/pi-coding-agent": "*",
"@introspection-ai/recipes": "*",
"typebox": "*"
},
"pi": { "extensions": ["extensions/*.ts"] }
}Peers are what the runtime provides; everything else your code imports goes in
dependencies (package.json).
3. Allow it in the agent
A registered tool that is not listed never reaches the model.
tools:
- read
- lookup_orderVerify
introspection check
introspection local -p "Look up order 1842."Expect a lookup_order tool call in the session output.
Hook a lifecycle event
The same factory can subscribe to Pi events. This adds today’s date to the system prompt:
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
const extension: ExtensionFactory = (pi) => {
pi.on("before_agent_start", async (event) => ({
systemPrompt: `${event.systemPrompt}\n\nToday is ${new Date().toDateString()}.`,
}));
};
export default extension;Hooks run even when none of the extension’s tools are in tools:. To refuse a
call before it runs, see Block a tool call.
Run a Python script
Declare Python in package.json, then call it with
the recipe directory (one level above extensions/) as the working directory:
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const { stdout } = await promisify(execFile)("python", ["scripts/reconcile.py"], {
cwd: new URL("..", import.meta.url),
});Record a custom event
In a managed task the sandbox sets INTROSPECTION_TOKEN and
INTROSPECTION_BASE_OTEL_URL. Add @introspection-sdk/introspection-node to
dependencies:
import { IntrospectionLogs } from "@introspection-sdk/introspection-node/otel";
const logs = new IntrospectionLogs({
token: process.env.INTROSPECTION_TOKEN,
baseOtelUrl: process.env.INTROSPECTION_BASE_OTEL_URL,
});
logs.track("refund_drafted", { orderId: "1842" });
await logs.flush();Limits
| Not supported | Do this instead |
|---|---|
| A response schema option | Define a typed tool the agent must call (Get structured output). |
Confirm dialogs (ctx.ui.confirm) | The sandbox is headless. Pause with askUserApproval (Ask the user). |
| Signing requests with a secret in code | Secrets never enter the sandbox. Put a server that handles the auth in front (The sandbox). |
Or ask your agent: “Add a lookup_order tool that calls our orders API, allow it in the main agent, and test it locally.”