Extensions in Prime Agent are factory functions that receive an ExtensionAPI to register tools, providers, commands, and lifecycle handlers; async factories can initialize remote data before startup, making it available to the session and CLI. Prime Agent's lifecycle runs in strict order across session setup (session_start → resources_discover) and per-prompt execution (input → before_agent_start → agent/message events → per-tool-call events → agent_end), allowing handlers to observe and intercept at each stage.
The extension loader API in packages/coding-agent/src/core/extensions/index.ts exports four functions from ./loader.js: createExtensionRuntime, discoverAndLoadExtensions, loadExtensionFromFactory, and loadExtensions.[1] From ./types.js, the same module exports the defineTool helper and the type-guard functions isBashToolResult, isEditToolResult, isIpythonToolResult, and isToolCallEventType.[1] Tool-decoration helpers wrapRegisteredTool and wrapRegisteredTools are exported from ./wrapper.js, and the built-in Herdr integration — createHerdrAgentStateExtension, hasFileBasedHerdrIntegration, and herdrAgentStateExtension — is exported from ./builtin/herdr-agent-state.js.[1]
Extensions export a default factory function that receives ExtensionAPI; the factory may be synchronous or asynchronous. When it returns a Promise, Prime Agent awaits it before continuing startup — async initialization completes before session_start, before resources_discover, and before provider registrations queued via pi.registerProvider() are flushed.[2] The full lifecycle event order is: session_start → resources_discover → (per-prompt) input → before_agent_start → agent_start → message_start/update/end → and, per tool call: turn_start, context, before_provider_request, after_provider_response, tool_execution_start, tool_call, tool_execution_update, tool_result, tool_execution_end, turn_end → agent_end.[2] An async factory can fetch remote data — for example, a local model list — before calling pi.registerProvider(), making those models available at startup and to prime-agent model list.[2] The session_before_refine lifecycle hook fires immediately before each refinement cycle; a handler receives the agent session and may return a modified session or metadata that shapes refinement behavior. session_before_refine is wired through packages/coding-agent/src/core/extensions/types.ts, extensions/runner.ts, extensions/index.ts, and core/refinement/refinement.ts.
pi.registerTool() registers a custom tool callable by the LLM. The definition requires name, label, description, parameters (a TypeBox schema), and an async execute(toolCallId, params, signal, onUpdate, ctx) function returning { content, details }.[2] The defineTool() helper from @earendil-works/pi-coding-agent is the canonical way to build a typed tool definition before passing it to pi.registerTool() — see Extensions for broader extension how-to knowledge.[3] pi.registerCommand(name, { description, handler }) registers a slash-command (e.g. /hello) accessible in the TUI; the handler receives (args, ctx).[2] pi.appendEntry<T>(type, data) stores state that survives session restarts. Entries are retrieved by iterating ctx.sessionManager.getBranch() and filtering on entry.type === "custom" && entry.customType === type.[2] A tool_call event handler can block a tool call by returning { block: true, reason: string }; returning nothing allows the call to proceed.[2] TypeBox is a JSON Schema builder for TypeScript that defines tool parameters schemas in Prime Agent extensions, enabling runtime validation of LLM-supplied arguments and automatic type inference in the execute function.
Canonical async factory pattern for dynamically discovering and registering a local OpenAI-compatible provider at startup:
export default async function (pi: ExtensionAPI) {
const response = await fetch("http://localhost:1234/v1/models");
const payload = await response.json();
pi.registerProvider("local-openai", {
baseUrl: "http://localhost:1234/v1",
apiKey: "LOCAL_OPENAI_API_KEY",
api: "openai-completions",
models: payload.data.map((model) => ({
id: model.id,
name: model.name ?? model.id,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: model.context_window ?? 128000,
maxTokens: model.max_tokens ?? 4096,
})),
});
}
Minimal custom tool extension using defineTool and pi.registerTool():
import { Type } from "@earendil-works/pi-ai";
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
const helloTool = defineTool({
name: "hello",
label: "Hello",
description: "A simple greeting tool",
parameters: Type.Object({
name: Type.String({ description: "Name to greet" }),
}),
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
return {
content: [{ type: "text", text: `Hello, ${params.name}!` }],
details: { greeted: params.name },
};
},
});
export default function (pi: ExtensionAPI) {
pi.registerTool(helloTool);
}
The tools.ts extension example demonstrates building a /tools command that uses ctx.ui.custom() with SettingsList from @earendil-works/pi-tui to render an interactive TUI dialog for toggling tools on/off, with changes applied immediately via pi.setActiveTools() and persisted via pi.appendEntry().[4] Tool selection state stored via pi.appendEntry() is restored per-branch: the tools.ts extension finds the last tools-config custom entry in ctx.sessionManager.getBranch() and filters saved tool names against currently-existing tools before restoring.[4] A reference implementation of the session_before_refine hook is provided in examples/extensions/custom-refinement.ts and documented in docs/extensions.md.
Sources