4.1 Tool Interface Definition
The starting point of the three-layer architecture described above is the Tool interface (src/Tool.ts)—the unified contract for all tools (built-in, MCP, REPL). This is one of the most core types in the entire system:
export type Tool<Input, Output, P extends ToolProgressData> = {
// ===== Metadata =====
name: string // Unique tool identifier
aliases?: string[] // Aliases (backward compatibility with old names)
maxResultSizeChars: number // Maximum result character count
shouldDefer?: boolean // Whether to lazy-load (ToolSearch dynamic discovery)
// ===== Core Execution =====
call(args, context, canUseTool, parentMessage, onProgress?): Promise<ToolResult<Output>>
// ===== Prompts & Descriptions =====
description(input, options): Promise<string>
prompt(options): Promise<string>
// ===== Schema Definitions =====
inputSchema: Input // Zod input Schema
inputJSONSchema?: ToolInputJSONSchema // JSON Schema (API compatible)
// ===== Security & Permissions =====
isConcurrencySafe(input): boolean // Whether it can execute concurrently
isReadOnly(input): boolean // Whether it's a read-only operation
isDestructive?(input): boolean // Whether it's a destructive operation
validateInput?(input, context): Promise<ValidationResult>
checkPermissions(input, context): Promise<PermissionResult>
// ===== UI Rendering (React Components) =====
renderToolUseMessage(input, options): React.ReactNode
renderToolResultMessage?(content, progress, options): React.ReactNode
}Each tool's returned ToolResult not only contains data, but can also inject additional messages or modify the context:
export type ToolResult<T> = {
data: T // Tool output data
newMessages?: Message[] // Additional injected messages
contextModifier?: (ctx) => ToolUseContext // Context modifier
}buildTool Factory Pattern
All tools are created through the buildTool() factory function. This function merges TOOL_DEFAULTS with the tool's custom definition, ensuring every tool has a complete set of methods:
const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: () => false, // Default assumes unsafe, preventing concurrency issues
isReadOnly: () => false, // Default assumes writes, requiring permission checks
isDestructive: () => false,
checkPermissions: () => ({ behavior: 'allow', updatedInput }), // Default allows
toAutoClassifierInput: () => '', // Default skips classifier
}
function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
return {
...TOOL_DEFAULTS,
userFacingName: () => def.name,
...def,
} as BuiltTool<D>
}This is a classic fail-closed (default-closed) security design:
isConcurrencySafe: () => false: New tools default to non-concurrent execution. Only tools that have been verified as truly safe (such as pure read operations) explicitly opt-in totrue. This avoids race conditions caused by new tools missing the concurrency safety flag.isReadOnly: () => false: Defaults to assuming the tool has write side effects, so it must go through permission checks. Read-only tools (such as GrepTool, GlobTool) explicitly declare themselves as read-only to skip the permission popup.toAutoClassifierInput: () => '': Defaults to skipping the LLM classifier's auto-approval. This means security-related tools won't be accidentally auto-approved—the tool author must explicitly provide the classifier input format.
This design ensures that: any new tool lacking explicit configuration will take the most conservative path—requiring permissions, disallowing concurrency, and no auto-approval.
Tool Directory Structure
Each tool is stored independently in its own directory under src/tools/, following a unified file organization convention:
src/tools/FileEditTool/
├── FileEditTool.ts // Main implementation: call(), validateInput(), checkPermissions()
├── UI.tsx // React rendering: renderToolUseMessage, renderToolResultMessage
├── types.ts // Zod inputSchema + TypeScript types
├── prompt.ts // Tool-specific system prompt injection content
├── constants.ts // Constant definitions
└── utils.ts // Helper functions (e.g., diff generation, quote normalization)The benefits of this separation are:
- Separation of concerns: Execution logic (
.ts) and rendering logic (UI.tsx) are fully decoupled—modifying the UI doesn't affect tool behavior - Schema reusability: The Zod Schema defined in
types.tsis used both for runtime validation and automatically converted to JSON Schema for sending to the API - Prompt injection: Each tool can inject tool-specific usage guidelines into the system prompt via
prompt.ts, for example FileEditTool injects rules about exact matching