Best Practices for Developing New Tools for PrimeAgent: A Complete Guide

To add a new tool to PrimeAgent, register it via pi.registerTool with a strict Typebox JSON-Schema, implement abort-aware execute logic using the signal and onUpdate callbacks, provide clear UI hints via promptSnippet and promptGuidelines, and return a standardized result object containing content, optional details, and an isError flag.

PrimeAgent’s architecture is built for extensibility through custom tools that large language models can invoke during task execution. Following the conventions established in the PrimeIntellect-ai/prime-agent repository ensures your tools integrate seamlessly with the parallel execution engine, remain type-safe, and provide a consistent user experience across TUI, RPC, and print modes.

Registering Tools Through the Extension API

All custom tools must be registered using the pi.registerTool method available through the Extension API. According to the source code in packages/coding-agent/src/core/tools/tool-definition-wrapper.ts, registration can occur at extension load time, during the session_start event, or within a command handler.

The registration object requires a unique, lowercase name, a human-readable label, and a description. These fields are indexed by the agent’s tool discovery system to populate the system prompt available to the LLM.

Defining Strict Parameter Schemas with Typebox

PrimeAgent enforces type safety at the boundary between the LLM and your code by requiring a JSON-Schema definition for all parameters. The codebase uses Typebox to build these schemas programmatically.

In packages/coding-agent/docs/extensions.md, the documentation emphasizes avoiding any types and instead using Type.Object to define required and optional fields with validation constraints. This ensures that invalid tool invocations are caught before reaching your execution logic.

For example, a tool requiring a string and an optional integer would define:

parameters: Type.Object({
  query: Type.String({ description: "The search query." }),
  limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 }))
})

Implementing Abort-Aware Execution Logic

The execute function signature defined in src/core/tools/tool-definition-wrapper.ts is:

async execute(toolCallId, params, signal, onUpdate, ctx)

Abort handling is critical for PrimeAgent’s parallel tool execution model. Your implementation must respect the signal (an AbortSignal) to cancel long-running operations when the user interrupts the session or when the agent decides to halt execution.

Streaming updates are provided via the onUpdate callback, which accepts partial MessageContent arrays. This allows the TUI to display progress for tools that perform multi-step operations, such as the Bash tool implementation found in src/core/tools/bash.ts.

Providing UI Hints for LLM Discoverability

To ensure the LLM knows when and how to invoke your tool, provide explicit UI hints:

  • promptSnippet: A short string shown in the system prompt demonstrating basic usage (e.g., "summarize <text>").
  • promptGuidelines: An array of bulleted strings providing specific usage guidance. Always include the tool name explicitly in these guidelines to improve model recall.

These hints are parsed from the tool definition and injected into the system prompt context, as documented in packages/coding-agent/docs/extensions.md.

Returning Well-Formed Tool Results

All tools must return an object conforming to the ToolResult interface:

{
  content: MessageContent[];
  details?: object;
  isError?: boolean;
}

The content array contains the primary output visible to the user and LLM. The optional details field can include structured metadata for debugging or downstream processing. Crucially, errors must be signaled with isError: true rather than thrown, allowing PrimeAgent to distinguish between execution failures and successful completions with informative output.

Adding Custom Rendering for the TUI

For advanced user experiences, optionally implement renderCall and renderResult methods. These functions control how tool invocations and their outputs appear in the Terminal User Interface (TUI).

  • renderCall(args, theme, ctx): Returns a string representing the tool call in the chat history.
  • renderResult(result, options, theme, ctx): Returns a formatted string for the result display.

The built-in IPython tool in src/core/tools/ipython.ts demonstrates how to use theme colors (via theme.fg()) to distinguish different tool states visually.

Testing Your Custom Tools

New tools should include unit tests placed under packages/coding-agent/test/, following the pattern established in tools.test.ts. Tests should exercise:

  1. Parameter validation against the JSON-Schema.
  2. Execution logic with mocked signal and onUpdate callbacks.
  3. Error handling paths where isError is returned.

Run npm run check to ensure type safety across the extension boundary before submitting changes.

Complete Implementation Example

Below is a minimal yet complete custom tool that summarizes text. It demonstrates schema definition, execution with streaming updates, error handling, and UI hints as implemented in the PrimeAgent codebase.

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

export default function (pi: ExtensionAPI) {
  // Register the tool at load time
  pi.registerTool({
    name: "summarize",
    label: "Summarize",
    description: "Generate a concise summary of the provided text.",
    // Short snippet shown in the system prompt
    promptSnippet: "summarize <text>",
    // Explicit usage guidance for the model
    promptGuidelines: [
      "Use summarize when the user asks for a brief recap of a longer passage."
    ],
    // Parameter schema – required `text` field, optional `maxLength`
    parameters: Type.Object({
      text: Type.String({ description: "The text to summarize." }),
      maxLength: Type.Optional(Type.Integer({ minimum: 10, maximum: 500 }))
    }),

    async execute(_toolCallId, params, signal, onUpdate) {
      // Simulate a streaming summarizer (replace with real logic)
      const chunks = [
        "Analyzing input…",
        "Generating summary…",
        "Done."
      ];
      for (const chunk of chunks) {
        if (signal?.aborted) break;
        // Emit partial output so the UI can show progress
        onUpdate?.({ content: [{ type: "text", text: chunk }] });
        await new Promise(r => setTimeout(r, 200));
      }

      // Simple truncation implementation
      const summary = params.maxLength
        ? params.text.slice(0, params.maxLength) + "…"
        : params.text.slice(0, 200) + "…";

      return {
        content: [{ type: "text", text: summary }],
        details: { originalLength: params.text.length }
      };
    },

    // Optional custom rendering – makes the call visually distinct
    renderCall(args, theme, ctx) {
      const title = theme.fg("accent", "Summarize");
      return `${title}: ${args.text.slice(0, 30)}…`;
    },

    renderResult(result, _options, theme, _ctx) {
      const success = theme.fg("success", "✔");
      return `${success} ${result.content[0].text}`;
    }
  });
}

Key Source Files to Reference

When developing new tools, consult these reference implementations in the PrimeIntellect-ai/prime-agent repository:

Summary

  • Register tools via pi.registerTool at load time or during session initialization.
  • Define strict schemas using Typebox to validate LLM-generated parameters before execution.
  • Handle cancellation by checking signal.aborted in long-running execute implementations.
  • Stream progress using onUpdate to provide real-time feedback in the TUI.
  • Guide the LLM with promptSnippet and promptGuidelines containing explicit tool names and usage patterns.
  • Return standardized results with content, optional details, and isError flags for proper error signaling.
  • Test thoroughly under packages/coding-agent/test/ and validate with npm run check.

Frequently Asked Questions

What is the correct way to handle errors in a PrimeAgent tool?

Return an object with isError: true and include error details in the content array. Do not throw exceptions, as uncaught errors can disrupt the agent's event pipeline and parallel execution flow.

How do I make my tool parameters optional in the schema?

Use Type.Optional() from Typebox to wrap the field definition. For example: Type.Optional(Type.Integer({ minimum: 1 })). This generates the correct JSON-Schema to indicate the field is not required while maintaining type safety.

Can I update the UI while my tool is still running?

Yes. Call the onUpdate callback within your execute function to stream partial results. This is essential for long-running operations like file searches or API calls, allowing users to see progress before the final result is returned.

Where should I place tests for my custom tool?

Add test files under packages/coding-agent/test/, following the naming convention and patterns found in tools.test.ts. Ensure your tests mock the signal and onUpdate parameters to verify both successful execution and cancellation paths.

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 →