pi-mono/packages/coding-agent/docs/extensions.md
2026-09-10 15:41:03 +02:00

9.4 KiB
Raw Permalink Blame History

Extensions

Extensions are TypeScript modules that add executable behavior to Pi. Use one when a workflow needs tools, commands, event handlers, model providers, session state, or terminal UI rather than instructions alone.

An extension runs inside the Pi process with the same operating-system permissions. It can inspect prompts, tool calls, files, credentials, and session history, so load extensions only from sources you trust.

Typical extensions add an agent tool, protect paths, confirm dangerous commands, react to session events, modify context, expose a command, or display persistent status.

Create and load an extension

An extension exports a default factory that receives ExtensionAPI. The factory registers capabilities for the current extension runtime.

Create ~/.pi/agent/extensions/hello.ts:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  pi.registerCommand("hello", {
    description: "Show a greeting",
    handler: async (name, ctx) => {
      ctx.ui.notify(`Hello, ${name || "world"}!`, "info");
    },
  });
}

Start Pi and run /hello. During development, load a file directly:

pi --extension ./hello.ts

Pi uses jiti, so local TypeScript extensions do not need a separate compilation step. Use Pi packages for distributed extensions and dependencies.

Add it to Pi

Place the extension in your user or project extensions directory. Pi loads direct TypeScript or JavaScript files and subdirectories containing an index.ts or index.js entry point.

Use a single file for a small extension and a directory for a multi-file implementation. Put npm dependencies in a nearby package.json. See Loading resources for all supported locations and options.

Reload replaces the extension runtime, so code after await ctx.reload() must not reuse state from the old runtime. Only personal and explicit command-line extensions can participate in the project_trust event that runs before project extensions load.

Respect the runtime lifecycle

The factory can be synchronous or asynchronous. Pi waits for an asynchronous factory before startup continues, allowing it to fetch configuration or register providers needed during startup.

Do not start processes, sockets, watchers, or timers in the factory because some invocations load extensions without starting a session. Start long-lived resources from session_start or from the command or tool that needs them. Close session-scoped resources from an idempotent session_shutdown handler.

A run proceeds from input and before_agent_start, through model, message, and tool events, to agent_end. Automatic retries, compaction, or queued work can continue afterward. Use agent_settled when an integration needs to know Pi will not continue automatically.

Choose an integration point

Capability Main API
Observe or modify lifecycle behavior pi.on()
Add a model-callable operation pi.registerTool()
Add a / command pi.registerCommand()
Add a shortcut or CLI flag pi.registerShortcut() or pi.registerFlag()
Send user or custom messages pi.sendUserMessage() or pi.sendMessage()
Persist non-context session data pi.appendEntry()
Change active tools, model, or thinking level Session control methods on pi
Add a model provider pi.registerProvider()
Add terminal rendering Renderer registration and ctx.ui
Communicate with another extension pi.events

Use the exported declarations in extensions/types.ts for exact event, context, tool, and result types.

Follow the extension contracts

Events and concurrency

Handlers run in extension load order. Some events notify; others transform data, replace results, or cancel an operation. Use each event’s declared result type rather than assuming every return value has an effect.

Events cover resource discovery, sessions, agent and message lifecycle, providers, tools, and raw input.

message_end can replace a finalized message while preserving its role. tool_call can mutate input or block execution. tool_result handlers compose, with each handler seeing prior changes.

Tool calls from one assistant message can run in parallel. Do not assume a sibling call or result exists when another tool event runs. Use ctx.signal for nested work owned by an active turn; commands and idle session events often have no operation signal.

Tools

A custom tool defines a name, model-facing description, TypeBox parameter schema, and execute() function. Its result requires model-facing content and a details field for rendering or state reconstruction. Use details: undefined when there are no structured details.

Throw from execute() to produce a failed tool result. Returning an object does not mark it as an error. Return terminate: true only when the agent should skip its automatic follow-up after every completed tool in that batch agrees to terminate.

Use sequential execution when tools share mutable in-memory state. File-mutating tools should wrap the complete read-modify-write operation with withFileMutationQueue(). Truncate large model-facing results and tell the model where to read the complete output.

See hello.ts, todo.ts, dynamic-tools.ts, and truncated-tool.ts.

Context and session changes

ExtensionContext provides the working directory, mode, UI, session manager, model runtime, abort signal, context usage, and controls for compaction and shutdown. Use ctx.modelRegistry.streamSimple() for provider-neutral nested model calls, and report nested usage so session totals remain accurate.

Command handlers receive ExtensionCommandContext, which adds operations for waiting until idle, reloading, tree navigation, and session replacement. These operations are command-only because calling them from lifecycle handlers can deadlock the runtime.

Session replacement invalidates the old context. Capture only plain data before switching, then use the fresh context supplied to withSession for session-bound work.

State

Choose storage based on how state participates in the conversation:

State Storage
Tool state that follows the active branch Tool-result details
Durable data excluded from model context pi.appendEntry()
Custom content stored and sent to the model pi.sendMessage()
Data outside one session External storage

Reconstruct branch-sensitive state from ctx.sessionManager.getBranch() during session_start. Do not rebuild it from every file entry because abandoned branches represent alternative histories. Register an entry or message renderer when custom stored content should appear in the transcript.

UI and modes

ctx.ui provides dialogs, notifications, status text, widgets, titles, editor access, and custom components. Use ctx.ui.custom() only when the interaction needs its own rendering and input. See Terminal UI for component, focus, overlay, theme, and performance guidance.

Extensions load in interactive, RPC, JSON, and print modes. Interactive mode provides the complete terminal UI. RPC can forward supported dialogs and notifications, but not custom terminal components; JSON and print modes have no UI. Guard terminal-only behavior with ctx.mode === "tui" and use ctx.hasUI for interactions supported by interactive and RPC clients.

Keep tool and event behavior independent from rendering so non-interactive modes remain functional.

Errors and cleanup

Pi reports handler errors and continues where possible. A tool_call handler failure blocks the tool as a fail-safe; a tool execution failure becomes an error result for the model.

Release resources in session_shutdown even when normal operation attempted cleanup. Keep cleanup idempotent because cancellation, reload, session replacement, and process exit can converge on the same path. Use ctx.shutdown() to request an orderly process shutdown.

Examples and reference

The checked extension examples cover tools, lifecycle events, commands, flags, shortcuts, state, rendering, providers, OAuth, remote execution, and terminal components. Start with the smallest example matching your integration point.

Use Custom Providers for model-service integrations, Terminal UI for custom components, and Pi Packages to install or distribute extensions with other resources.