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:

  1. packages/coding-agent/src/core/extensions/loader.ts — Loads extensions, calls their default export with pi, and populates the session tool registry with registered definitions.

  2. packages/coding-agent/src/modes/interactive/components/tool-execution.ts — When the LLM emits a tool_call, looks up the tool by name, validates parameters against the typebox schema, and invokes execute.

  3. packages/coding-agent/src/core/tools/tool-definition-wrapper.ts — Wraps the raw result into the standard ToolResult format 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 parameters field uses @sinclair/typebox for JSON Schema generation—the same library PrimeAgent uses internally.
  • Result format: content must 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 ExtensionAPI and @sinclair/typebox for type-safe tool definitions
  • Call pi.registerTool() with name, label, description, parameters (typebox schema), and execute function
  • Return a ToolResult with content array and optional details
  • Launch with -e to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →