Extensions โ Rewire the Harness in TypeScript
Lesson 4: Extensions โ Rewire the Harness in TypeScript
This is pi's primary extension surface. An extension is a TypeScript module that exports a default factory function receiving the ExtensionAPI. It can subscribe to lifecycle events, register tools the LLM can call, add slash commands, shortcuts, and CLI flags, render custom TUI components, and persist state across restarts. Extensions are loaded with jiti, so TypeScript runs without a build step, and they hot-reload with /reload.
Locations
~/.pi/agent/extensions/*.ts # global (all projects)
~/.pi/agent/extensions/*/index.ts # global, multi-file
.pi/extensions/*.ts # project-local (after project trust)
.pi/extensions/*/index.ts # project-local, multi-file
# quick tests:
pi -e ./my-extension.ts
A minimal extension
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
// Block dangerous tool calls before they run
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
if (!ok) return { block: true, reason: "Blocked by user" };
}
});
// A custom tool the LLM can call
pi.registerTool({
name: "greet",
label: "Greet",
description: "Greet someone by name",
parameters: Type.Object({ name: Type.String({ description: "Name to greet" }) }),
async execute(toolCallId, params) {
return { content: [{ type: "text", text: `Hello, ${params.name}!` }], details: {} };
},
});
// A slash command
pi.registerCommand("hello", {
description: "Say hello",
handler: async (args, ctx) => { ctx.ui.notify(`Hello ${args || "world"}!`, "info"); },
});
}
What you can hook
| Surface | API | What it's for |
|---|---|---|
| Lifecycle events | pi.on("session_start" | "project_trust" | "resources_discover" ...) | Init work, trust decisions, resource loading |
| Agent events | before_agent_request and friends | Inspect/modify what gets sent to the model |
| Tool events | tool_call, tool result events | Block, allow, or rewrite tool calls (permission gates) |
| Input events | input | Intercept, transform, or handle user prompts |
| Model events | model switch / thinking events | React to model and reasoning changes |
| Commands | pi.registerCommand() | Custom slash commands (/mycommand) |
| Shortcuts / flags | pi.registerShortcut(), pi.registerFlag() | Keybindings and CLI flags |
| Custom tools | pi.registerTool() | New tools the LLM can call (typebox schemas) |
| UI | ctx.ui โ notify, confirm, select, input, setStatus, setWidget, custom() | Dialogs, wizards, footer status, widgets, full TUI components |
| State | pi.appendEntry(), setLabel(), sendMessage() | Persist state that survives restarts; inject messages |
| Rendering | registerMessageRenderer(), registerEntryRenderer(), registerMarkdownTransformer() | Custom display for tool calls, entries, markdown |
| Runtime | pi.exec(), pi.setModel(), pi.setActiveTools(), pi.registerProvider() | Run processes, swap models, toggle tool surface, add providers |
The extension lifecycle
Because the factory can be async, extensions can fetch remote configuration (e.g. dynamically discover models from a local server and register them via pi.registerProvider()) before the session starts. Doc guidance: don't start long-lived background resources from the factory itself โ defer them to session_start and clean up in session_shutdown.
Real-world extension patterns
- Permission gates โ confirm before
rm -rf,sudo, etc. (the example above) - Git checkpointing โ stash at each turn, restore on branch
- Path protection โ block writes to
.env,node_modules/ - Custom compaction โ summarize the conversation your way (Lesson 7)
- Interactive tools โ questions, wizards, custom dialogs
- Stateful tools โ todo lists, connection pools
- External integrations โ file watchers, webhooks, CI triggers
ctx.compact()โ trigger compaction programmatically when you know a milestone has passed.ctx.getContextUsage()โ read the current budget and warn/inject reminders near the limit.inputevent โ rewrite verbose user prompts into tighter ones before they reach the model.tool_callblocker โ deny expensive tools or cap their output (e.g. refusebashcalls that dump huge files into context).- Custom summarizers (via
session_before_compact) produce better, shorter summaries โ quality in compaction directly reduces what re-enters context later. pi.setThinkingLevel()andpi.setActiveTools()โ shrink the tool surface and reasoning budget per task (fewer declared tools = smaller system prompt).
- You Can Learn Pi Minimal Coding Agent Harness In 22 Min | Skills, Extensions, Packages โ Sean's AI Stories โ the exact extension/skills/packages workflow from this and the next two lessons.
Further Reading
- Extensions โ full reference (events, context, API)
- pi-mono on GitHub โ see examples/extensions/ for working implementations
๐ง Knowledge Check
1. How does pi load TypeScript extensions without a build step?
2. Which event lets an extension block a dangerous tool call like rm -rf before it runs?
3. Which ExtensionContext method lets an extension monitor how much of the context window is in use?