Extensions

Tools you write in TypeScript, owned by the package.

When a job needs a tool Pi does not have, write it. An extension is a TypeScript file the package declares and every session in it loads.

package.json
{
  "pi": {
    "extensions": ["extensions/*.ts"]
  }
}

Pi loads the TypeScript directly, so there is nothing to compile and no bundler to configure. It also resolves @earendil-works/pi-coding-agent, typebox, @earendil-works/pi-ai and @earendil-works/pi-tui for you, so those are not dependencies you declare. Anything else your extension imports goes in dependencies, with an npm install in the package.

extensions/citations.ts
import { Type } from "typebox";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  pi.registerTool({
    name: "check_citation",
    label: "Check citation",
    description: "Confirm a quote still appears at its source URL.",
    parameters: Type.Object({
      quote: Type.String(),
      url: Type.String(),
    }),
    // Full signature: (toolCallId, params, signal, onUpdate, ctx)
    async execute(_toolCallId, { quote, url }) {
      const res = await fetch(url);
      const found = (await res.text()).includes(quote);
      // This text is what the model reads, so say what to do next.
      return {
        content: [
          {
            type: "text",
            text: found
              ? `Verified at ${url}`
              : `Not found at ${url}. Do not cite it.`,
          },
        ],
      };
    },
  });
}

Declared once, gated per agent

Extensions load for every agent in the package, including delegated children. Registering a tool does not expose it: an agent still has to list check_citation in its tools. Two roles can therefore share one extension and see different halves of it.

# agents/agent.yaml
tools: [read, write, check_citation]

# agents/reviewer.yaml
tools: [read, check_citation]         # same extension, narrower role

Knowing which recipe you are in

recipe-owned tools receive the selected path and agent as PI_RECIPE_DIR and PI_AGENT_NAME, so an extension can find files in its own package without a hardcoded path. For the resolved agent itself, @introspection-ai/recipes/extensions exposes the session context.

import { getRecipeSessionContext } from "@introspection-ai/recipes/extensions";

const ctx = getRecipeSessionContext();
// ctx.session.role distinguishes a root session from a delegated child.

A host can add its own

A host passes extensionFactories and customTools to add platform tools alongside the package's own, for things a recipe should not carry: file uploads, workspace persistence, its own telemetry. Those are host resources, not portable ones, so they stay out of the package. See the SDK reference.