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:
- Parameter validation against the JSON-Schema.
- Execution logic with mocked
signalandonUpdatecallbacks. - Error handling paths where
isErroris 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:
src/core/tools/bash.ts: Reference for command execution, error handling, and streaming output.src/core/tools/tool-definition-wrapper.ts: Core type definitions forToolDefinitionand the execution contract.src/core/tools/ipython.ts: Demonstrates wrapping a persistent Python kernel as a tool, includingonUpdateusage for streaming.test/tools.test.ts: Test suite covering built-in tools; serves as a template for new tool tests.docs/extensions.md: Full documentation for custom tools, UI hints, and event flow.
Summary
- Register tools via
pi.registerToolat load time or during session initialization. - Define strict schemas using Typebox to validate LLM-generated parameters before execution.
- Handle cancellation by checking
signal.abortedin long-runningexecuteimplementations. - Stream progress using
onUpdateto provide real-time feedback in the TUI. - Guide the LLM with
promptSnippetandpromptGuidelinescontaining explicit tool names and usage patterns. - Return standardized results with
content, optionaldetails, andisErrorflags for proper error signaling. - Test thoroughly under
packages/coding-agent/test/and validate withnpm 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →