How to Create Custom Tools for PrimeAgent: A Step-by-Step Guide
You create custom tools for PrimeAgent by writing a TypeScript extension that registers a ToolDefinition via pi.registerTool(), then placing the file in ~/.prime/agent/extensions/ or your project's .prime/agent/extensions/ folder.
PrimeAgent's extensibility model lets you equip the LLM with new capabilities beyond built-in tools like bash and edit. Custom tools plug into the same runtime as native features—complete with schema validation, streaming UI updates, and security controls. This guide covers the architecture, API, and implementation details drawn directly from the PrimeIntellect-ai/prime-agent source code.
Extension Architecture and Discovery
PrimeAgent discovers extensions automatically from two locations:
~/.prime/agent/extensions/(user-global).prime/agent/extensions/(project-local)
Extensions load via jiti, so you write TypeScript without a separate compilation step. The loader handles parsing and registration at runtime.
According to the documentation in packages/coding-agent/docs/extensions.md, the discovery system prioritizes project-local extensions, enabling per-project tool customization.
The Extension API: pi.registerTool()
Each extension receives an ExtensionAPI object (conventionally named pi). This object exposes the full extension surface, including the critical registerTool method for custom tool registration.
The registerTool signature appears in packages/coding-agent/src/core/extensions/types.ts at line 1081:
registerTool<TParams extends TObject, TDetails = unknown, TState = unknown>(
definition: ToolDefinition<TParams, TDetails, TState>
): void;
Through this API, extensions can also subscribe to events (pi.on), register commands (pi.registerCommand), and access session context.
Tool Definition Structure
A ToolDefinition requires five key properties:
| Property | Type | Purpose |
|---|---|---|
name |
string |
Identifier the LLM uses to invoke the tool |
label |
string |
Display name in the TUI |
description |
string |
Help text shown to the LLM and user |
parameters |
TObject (typebox schema) |
JSON Schema for validation |
execute |
async function |
Runtime implementation |
The execute callback receives five arguments defined in packages/coding-agent/src/core/extensions/types.ts:
async execute(
toolCallId: string, // Unique call identifier
params: Static<TParams>, // Validated parameters
signal: AbortSignal, // Cancellation support
onUpdate: (update: unknown) => void, // Streaming updates
ctx: unknown // Extension context
): Promise<ToolResult>
Registration and Execution Flow
The lifecycle spans three core files:
-
packages/coding-agent/src/core/extensions/loader.ts— Loads extensions, calls their default export withpi, and populates the session tool registry with registered definitions. -
packages/coding-agent/src/modes/interactive/components/tool-execution.ts— When the LLM emits atool_call, looks up the tool by name, validates parameters against the typebox schema, and invokesexecute. -
packages/coding-agent/src/core/tools/tool-definition-wrapper.ts— Wraps the raw result into the standardToolResultformat for streaming to the LLM and rendering in the interface.
Complete Custom Tool Example
Below is a minimal working extension based on the reference implementation in packages/coding-agent/docs/extensions.md (lines 76-90) and packages/coding-agent/examples/extensions/hello.ts:
// file: ~/.prime/agent/extensions/greet.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "@sinclair/typebox";
export default function (pi: ExtensionAPI) {
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, _signal, _onUpdate, _ctx) {
return {
content: [{ type: "text", text: `Hello, ${params.name}!` }],
details: {},
};
},
});
}
Key implementation notes:
- Schema validation: The
parametersfield uses@sinclair/typeboxfor JSON Schema generation—the same library PrimeAgent uses internally. - Result format:
contentmust be an array of content blocks;type: "text"is the simplest option. - Streaming: Call
onUpdate()during long-running operations to send progress to the TUI.
Testing Your Custom Tool
Launch PrimeAgent with the -e flag to load your extension:
prime-agent -e ~/.prime/agent/extensions/greet.ts
The LLM can now invoke your tool with a JSON tool call:
{
"toolCallId": "tc-1",
"toolName": "greet",
"arguments": { "name": "Alice" }
}
The agent executes execute(), streams the result back, and displays "Hello, Alice!" in the interface.
Advanced Patterns
Long-Running Operations with Streaming
For tools that take time, use the onUpdate callback to send intermediate state:
async execute(toolCallId, params, signal, onUpdate, ctx) {
onUpdate({ status: "processing", step: 1 });
await fetchExternalData(params.query);
onUpdate({ status: "complete", step: 2 });
return {
content: [{ type: "text", text: "Data retrieved" }],
details: { query: params.query },
};
}
Accessing Context and Cancellation
The ctx parameter provides extension-scoped state and utilities. The signal parameter integrates with PrimeAgent's abort mechanics—check signal.aborted or pass it to fetch() for cooperative cancellation.
Key Source Files Reference
| File | Role |
|---|---|
packages/coding-agent/docs/extensions.md |
Official guide with examples |
packages/coding-agent/src/core/extensions/types.ts |
ExtensionAPI and ToolDefinition types |
packages/coding-agent/src/core/extensions/loader.ts |
Extension loading and tool registry |
packages/coding-agent/src/core/tools/tool-definition-wrapper.ts |
Result normalization |
packages/coding-agent/examples/extensions/hello.ts |
Working sample extension |
packages/coding-agent/src/modes/interactive/components/tool-execution.ts |
Runtime tool invocation |
Summary
- Create an extension file in
~/.prime/agent/extensions/or.prime/agent/extensions/ - Import
ExtensionAPIand@sinclair/typeboxfor type-safe tool definitions - Call
pi.registerTool()withname,label,description,parameters(typebox schema), andexecutefunction - Return a
ToolResultwithcontentarray and optionaldetails - Launch with
-eto test, then deploy to project folders for team sharing
Frequently Asked Questions
What TypeScript features can I use in extensions?
PrimeAgent loads extensions with jiti, which supports modern TypeScript (ES modules, decorators, JSX) without pre-compilation. You can import npm packages installed in your project or use relative imports to shared code.
Can I register multiple tools in one extension?
Yes. A single extension file can call pi.registerTool() multiple times—once per tool. The loader accumulates all registrations into the session's tool registry.
How does parameter validation work?
PrimeAgent validates incoming tool call arguments against your typebox schema before execute() runs. Invalid parameters trigger an automatic error response to the LLM without invoking your code.
Can custom tools call other tools?
The ctx parameter exposes extension-scoped utilities. For cross-tool coordination, share state through the extension closure or use PrimeAgent's event system (pi.on) to react to tool execution events.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →