Skip to Content
Platform
BuildAdd a tool

Add a tool

Give your agent a deterministic operation in a few lines of TypeScript.

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

extensions/lookup-order.ts
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.

package.json
{ "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.

agents/agent.yaml
tools: - read - lookup_order

Verify

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:

extensions/current-date.ts
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 supportedDo this instead
A response schema optionDefine 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 codeSecrets 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.”

Last updated on