Tool Use
Tool use is the mechanism by which the LLM takes actions in the real world — editing files, running commands, searching code, calling MCP servers. The lifecycle spans schema definition, prompt injection, model invocation, response parsing, dispatch, execution, result formatting, and feedback into the conversation. Each reference tool approaches this differently, from Aider’s text-based edit parsing to Codex’s trait-based registry to OpenCode’s Zod-typed plugin system.
This page is the orchestration layer (registration, dispatch, lifecycle). Tool-specific contracts live in Bash, File Read, File Edit, Web Search, Planning, and Custom Tools & Marketplace. For prompt injection and context-pressure interactions, see System Prompt and Token Budgeting.
Aider Implementation
Section titled “Aider Implementation”Pinned commit: b9050e1d
Aider has a split personality on tool use. For edit operations, it uses text-based response parsing (SEARCH/REPLACE blocks, unified diffs, whole file content) rather than the tool_use API. For function-calling variants, it defines OpenAI-compatible JSON schemas as class attributes.
Text-Based Edit Parsing (Primary Path)
Section titled “Text-Based Edit Parsing (Primary Path)”Most Aider coders (EditBlockCoder, UnifiedDiffCoder, WholeFileCoder) do not use the tool_use API at all. The model is prompted to produce edit blocks in a specific text format, and the coder subclass parses the response text:
get_edits()— subclass-specific parser extracts(path, content)tuples fromself.partial_response_contentapply_edits_dry_run(edits)— validates edits can be appliedapply_edits(edits)— writes changes to disk
The model never “calls a tool” in the API sense. It produces structured text that Aider interprets.
Function-Calling Variants (Deprecated)
Section titled “Function-Calling Variants (Deprecated)”File: aider/coders/wholefile_func_coder.py
The function-calling coders define tools as class-level functions attributes using JSON Schema:
class WholeFileFunctionCoder(Coder): functions = [ dict( name="write_file", description="create or update one or more files", parameters=dict( type="object", required=["explanation", "files"], properties=dict( explanation=dict(type="string", description="Step by step plan..."), files=dict(type="array", items=dict( type="object", required=["path", "content"], properties=dict( path=dict(type="string"), content=dict(type="string"), ), )), ), ), ), ]These are validated at init time using Draft7Validator (line 534 of base_coder.py).
API Integration
Section titled “API Integration”File: aider/models.py:991
When functions are present, they are wrapped for the API call:
if functions is not None: function = functions[0] kwargs["tools"] = [dict(type="function", function=function)] kwargs["tool_choice"] = {"type": "function", "function": {"name": function["name"]}}This forces the model to call the single defined function. The response is accumulated in self.partial_response_function_call (line 1791 of base_coder.py), with streaming chunks merged incrementally:
func = chunk.choices[0].delta.function_callfor k, v in func.items(): if k in self.partial_response_function_call: self.partial_response_function_call[k] += v else: self.partial_response_function_call[k] = vThe parsed arguments are extracted via self.partial_response_function_call.get("arguments") (line 2341) and applied by the subclass. Note: the function-calling coders are deprecated (WholeFileFunctionCoder.__init__ raises RuntimeError("Deprecated")), confirming Aider’s direction toward text-based parsing.
No Tool Registry
Section titled “No Tool Registry”Aider has no tool registry, no tool dispatch, no tool result feedback loop. Tools in the API sense are a vestigial feature. The model’s agency comes entirely from edit format parsing and slash commands.
Codex Implementation
Section titled “Codex Implementation”Pinned commit: 4ab44e2c5
Codex has a full tool system built on Rust traits, with a registry, router, parallel dispatch, approval integration, and MCP support.
Tool Definition: The ToolHandler Trait
Section titled “Tool Definition: The ToolHandler Trait”File: codex-rs/core/src/tools/registry.rs:32
pub trait ToolHandler: Send + Sync { fn kind(&self) -> ToolKind; async fn is_mutating(&self, _invocation: &ToolInvocation) -> bool { false } async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError>;}Each tool implements this trait. kind() returns metadata for schema generation. is_mutating() determines whether the tool needs a write-lock (serial execution) or can run with a read-lock (parallel). handle() is the execution entry point.
Schema System
Section titled “Schema System”File: codex-rs/core/src/tools/spec.rs:107
Tool parameter schemas use a recursive JsonSchema enum:
pub enum JsonSchema { Boolean { description: Option<String> }, String { description: Option<String> }, Number { description: Option<String> }, Array { items: Box<JsonSchema>, description: Option<String> }, Object { properties: BTreeMap<String, JsonSchema>, required: Option<Vec<String>>, additional_properties: Option<AdditionalProperties>, },}This is serialized to the OpenAI tool schema format for the API request.
Tool Configuration
Section titled “Tool Configuration”File: codex-rs/core/src/tools/spec.rs:34
pub struct ToolsConfig { pub shell_type: ConfigShellToolType, pub apply_patch_tool_type: Option<ApplyPatchToolType>, pub web_search_mode: Option<WebSearchMode>, pub search_tool: bool, pub js_repl_enabled: bool, pub collab_tools: bool, pub collaboration_modes_tools: bool, pub experimental_supported_tools: Vec<String>,}This config determines which tools are available for a given turn, based on model capabilities and user settings.
Built-In Tools
Section titled “Built-In Tools”Individual handler files in codex-rs/core/src/tools/handlers/:
| Handler | Tool | Purpose |
|---|---|---|
shell.rs | shell | Local shell command execution |
apply_patch.rs | apply_patch | File patch application (Codex Patch Format) |
read_file.rs | read_file | File content reading |
grep_files.rs | grep_files | Code search via ripgrep |
js_repl.rs | js_repl | JavaScript REPL execution |
multi_agents.rs | create_agent | Spawning sub-agents |
unified_exec.rs | unified_exec | ConPTY-based command execution |
mcp.rs | mcp__* | MCP server tool calls |
Tool Registry
Section titled “Tool Registry”File: codex-rs/core/src/tools/registry.rs:57
pub struct ToolRegistry { handlers: HashMap<String, Arc<dyn ToolHandler>>,}The registry maps tool names to handler trait objects. Built during session initialization from ToolsConfig.
Dispatch Flow
Section titled “Dispatch Flow”File: codex-rs/core/src/tools/registry.rs:78
- Lookup handler:
self.handler(tool_name)returnsArc<dyn ToolHandler>or error - Check mutating: If
handler.is_mutating()returns true, wait ontool_call_gate(line 152) for serial execution - Execute:
handler.handle(invocation).awaitruns the tool - Post-hook:
dispatch_after_tool_use_hook()fires with output preview, success flag, duration, and mutating flag - Build response:
output.into_response(&call_id, &payload)creates the API response item
Tool Routing and MCP
Section titled “Tool Routing and MCP”File: codex-rs/core/src/tools/router.rs:44
The router sits between the API response parser and the registry. When processing an OutputItemDone event:
build_tool_call()extracts the tool call from the response itembuild_tool_call()askssession.parse_mcp_tool_name(&name)(line 82); MCP-qualified names route to MCP dispatch- Otherwise, it dispatches to the local registry
MCP tool calls go through a separate path in codex-rs/core/src/mcp_tool_call.rs:
- Parse JSON arguments
- Request approval via
maybe_request_mcp_tool_approval() - Execute via
sess.call_tool(&server, &tool_name, arguments) - Emit
McpToolCallBegin/McpToolCallEndevents - Return
ResponseInputItem::McpToolCallOutput
Parallel Execution
Section titled “Parallel Execution”File: codex-rs/core/src/tools/parallel.rs:25
pub(crate) struct ToolCallRuntime { router: Arc<ToolRouter>, session: Arc<Session>, turn_context: Arc<TurnContext>, tracker: SharedTurnDiffTracker, parallel_execution: Arc<RwLock<()>>,}handle_tool_call() spawns a tokio task per tool call, using tokio::select! with a CancellationToken for abort. Results are collected in a FuturesOrdered, maintaining call order while allowing parallel execution. Read-only tools run concurrently; mutating tools acquire a write-lock.
Result Format
Section titled “Result Format”Tool results are returned as ResponseInputItem variants:
FunctionCallOutput— for built-in tools (shell, apply_patch, etc.)McpToolCallOutput— for MCP server toolsCustomToolCallOutput— for dynamic/custom tools
These are appended to the conversation history and included in the next API request.
OpenCode Implementation
Section titled “OpenCode Implementation”Pinned commit: 7ed449974
OpenCode uses a TypeScript-native tool system with Zod schemas, async initialization, automatic output truncation, and deep plugin integration.
Tool Definition
Section titled “Tool Definition”File: packages/opencode/src/tool/tool.ts
export namespace Tool { export interface Info<Parameters extends z.ZodType, M extends Metadata> { id: string init: (ctx?: InitContext) => Promise<{ description: string parameters: Parameters execute( args: z.infer<Parameters>, ctx: Context, ): Promise<{ title: string metadata: M output: string attachments?: Omit<MessageV2.FilePart, "id" | "sessionID" | "messageID">[] }> formatValidationError?(error: z.ZodError): string }> }}The init() function is async, allowing tools to perform setup (check binary availability, read config) before returning their schema and executor. The execute() function receives Zod-validated arguments and a Tool.Context with session state, abort signal, and permission helpers.
The Tool.define() Factory
Section titled “The Tool.define() Factory”File: packages/opencode/src/tool/tool.ts:48
export function define<Parameters extends z.ZodType, Result extends Metadata>( id: string, init: Info<Parameters, Result>["init"],): Info<Parameters, Result>This factory wraps the execute function with:
- Input validation:
toolInfo.parameters.parse(args)— throws formatted error if invalid - Output truncation:
Truncate.output(result.output, {}, agent)— unless the tool handles it itself (checked viaresult.metadata.truncated !== undefined)
Tool Registry
Section titled “Tool Registry”File: packages/opencode/src/tool/registry.ts:84
export async function tools( model: { providerID: string; modelID: string }, agent?: Agent.Info,)The registry:
- Calls
all()to collect all tool definitions (built-in + custom plugins from{tool,tools}/*.{js,ts}directories) - Filters by model:
apply_patchis enabled for selected GPT model IDs (gpt-*excludinggpt-4and*oss), while other models useedit/write(line 143-148) - Initializes each tool:
await t.init({ agent })— lazy, per-request - Fires plugin hook:
Plugin.trigger("tool.definition", { toolID }, output)— allows schema modification - Returns array of initialized tools with descriptions, parameters, and execute functions
Built-In Tools
Section titled “Built-In Tools”Key tools in packages/opencode/src/tool/:
| File | Tool ID | Purpose |
|---|---|---|
bash.ts | bash | Shell command execution |
edit.ts | edit | SEARCH/REPLACE file editing |
write.ts | write | Full file write |
apply_patch.ts | apply_patch | V4A diff application (selected GPT models) |
read.ts | read | File reading with line ranges |
glob.ts | glob | File pattern matching |
grep.ts | grep | Ripgrep code search |
todo.ts | todowrite | Task list writing (todoread exists but is currently disabled in the registry list) |
webfetch.ts | webfetch | Web URL fetching |
task.ts | task | Sub-agent spawning |
codesearch.ts | codesearch | Semantic code search (Exa) |
websearch.ts | websearch | Web search (Exa) |
Tool Dispatch
Section titled “Tool Dispatch”File: packages/opencode/src/session/prompt.ts:783
During resolveTools(), each tool is wrapped in a Vercel AI SDK tool() wrapper:
tools[item.id] = tool({ id: item.id, description: item.description, inputSchema: jsonSchema(schema), async execute(args, options) { const ctx = context(args, options)
// Plugin hooks await Plugin.trigger("tool.execute.before", { toolID: item.id, ... }, { args })
const result = await item.execute(args, ctx)
await Plugin.trigger("tool.execute.after", { toolID: item.id, ... }, output) return output },})The context() factory (line 748) creates a Tool.Context with:
sessionID,messageID,callID— for correlationabort—AbortSignalfrom the AI SDKmessages— full session historymetadata(val)— async callback to update tool title/metadataask(req)— permission request wrapper
MCP Tool Integration
Section titled “MCP Tool Integration”File: packages/opencode/src/session/prompt.ts:830
MCP tools are fetched via MCP.tools() and wrapped similarly:
- Schema transformed via
ProviderTransform.schema()for provider compatibility - Permission check injected:
ctx.ask({ permission: key, ... }) - Output converted: text content to strings, images and resources to
FilePartattachments - Output truncated via
Truncate.output()
Tool Call Lifecycle in the Processor
Section titled “Tool Call Lifecycle in the Processor”File: packages/opencode/src/session/processor.ts
The stream processor tracks tool calls through their full lifecycle:
tool-input-start(line 111): CreatesToolPartwith status"pending", persists viaSession.updatePart()(upsert)tool-call(line 134): Updates status to"running"and performs doom loop check (3x same call)tool-result(line 180): Updates status to"completed"with output, metadata, title, timing, and attachmentstool-error(line 204): Updates status to"error"with error string. IfPermissionNext.RejectedError, setsblocked = true
Each state transition is persisted to SQLite via Session.updatePart().
Plugin Hooks
Section titled “Plugin Hooks”Tools have four hook points:
"tool.definition"— modify schema before AI sees it"tool.execute.before"— intercept before execution (can modify args)"tool.execute.after"— observe results after execution"shell.env"— inject environment variables for shell tools
Pitfalls & Hard Lessons
Section titled “Pitfalls & Hard Lessons”Text Parsing vs API Tool Use
Section titled “Text Parsing vs API Tool Use”Aider’s text-based approach is surprisingly robust — the model produces SEARCH/REPLACE blocks more reliably than structured function calls for many models. But it requires format-specific prompting and fragile regex parsing. Codex and OpenCode use the native tool_use API, which is cleaner but ties them to providers that support it.
Schema Validation at the Boundary
Section titled “Schema Validation at the Boundary”OpenCode validates tool inputs with Zod before execution, providing formatted error messages the model can understand and retry. Codex validates via the handler’s type system but has no pre-execution schema check. Aider validates function schemas at init time but not arguments at call time.
Tool Output Truncation is Critical
Section titled “Tool Output Truncation is Critical”Without truncation, a single grep over a large codebase can return megabytes and blow the context window. OpenCode truncates by default in Tool.define() unless the tool opts out. Codex caps output at 1 MiB in the exec handler. Aider has no systematic truncation.
Parallel vs Serial Tool Execution
Section titled “Parallel vs Serial Tool Execution”Codex explicitly distinguishes mutating from non-mutating tools via is_mutating(). Read-only tools run in parallel; file-writing tools run serially. OpenCode has no such distinction — all tools run sequentially as the AI SDK processes them one at a time in the stream. This means Codex can execute multiple grep searches simultaneously, while OpenCode cannot.
MCP Tool Namespacing Collisions
Section titled “MCP Tool Namespacing Collisions”Both systems namespace MCP tools, but with different tradeoffs. Codex uses sanitized mcp__{server}__{tool} names and enforces a 64-character limit by appending a SHA-1 suffix when needed. OpenCode uses sanitizedClientName + "_" + sanitizedToolName, which is simple but can overwrite on key collisions after sanitization.
Permission Timing
Section titled “Permission Timing”OpenCode’s normal permission checks happen inside tool execution via ctx.ask() in resolveTools() wrappers, not in the processor’s tool-call transition. The processor-level permission call is for doom-loop protection only. Codex checks at dispatch/handler boundaries inside the registry path. Both approaches deny before tool side effects, but they surface the wait state in different layers.
Custom Tool Discovery
Section titled “Custom Tool Discovery”OpenCode scans {tool,tools}/*.{js,ts} directories for custom tool plugins, which is flexible but creates a surface for injection. Codex uses DynamicToolSpec in the session configuration, requiring explicit registration. Aider has no custom tool mechanism.
OpenOxide Blueprint
Section titled “OpenOxide Blueprint”Architecture: Trait-Based Registry with Typed Schemas
Section titled “Architecture: Trait-Based Registry with Typed Schemas”OpenOxide combines Codex’s ToolHandler trait with OpenCode’s async initialization and Zod-style validation:
#[async_trait]pub trait Tool: Send + Sync { fn id(&self) -> &str;
/// Async init -- check binary availability, read config, etc. async fn init(&self, ctx: &ToolInitContext) -> Result<ToolSpec>;
/// Whether this tool modifies files (controls parallel vs serial execution) fn is_mutating(&self) -> bool { false }
/// Execute the tool with validated arguments async fn execute( &self, args: serde_json::Value, ctx: ToolContext, ) -> Result<ToolOutput>;}
pub struct ToolSpec { pub description: String, pub parameters: JsonSchema,}
pub struct ToolOutput { pub title: String, pub content: String, pub metadata: HashMap<String, serde_json::Value>, pub attachments: Vec<FileAttachment>,}Registry
Section titled “Registry”pub struct ToolRegistry { tools: HashMap<String, Arc<dyn Tool>>,}
impl ToolRegistry { /// Initialize all tools for a given model and agent config pub async fn init(&self, model: &ModelInfo, agent: &AgentConfig) -> Vec<ToolSpec>;
/// Dispatch a tool call pub async fn dispatch(&self, call: &ToolCall) -> Result<ToolOutput>;}Schema Validation
Section titled “Schema Validation”Pre-execution validation using jsonschema crate, with error messages formatted for the model:
fn validate_args(schema: &JsonSchema, args: &serde_json::Value) -> Result<(), String> { // Returns human-readable error string on failure // Model can read this and retry with corrected arguments}Parallel Execution
Section titled “Parallel Execution”Adopt Codex’s FuturesOrdered pattern with RwLock gating:
is_mutating() == false— acquire read-lock, run in parallelis_mutating() == true— acquire write-lock, run serially
Output Truncation
Section titled “Output Truncation”Default truncation in the dispatch layer (configurable per-tool):
- Max output: 100k characters by default
- Tools can opt out by setting
metadata.truncated - Truncated output includes a note:
"[output truncated, {N} characters omitted]"
MCP Integration
Section titled “MCP Integration”MCP tools are registered in the same ToolRegistry with namespaced IDs (mcp__{server}__{tool}). The dispatch path checks for the MCP prefix and routes to the MCP client crate.
Hook System
Section titled “Hook System”Pre/post execution hooks as trait:
pub trait ToolHook: Send + Sync { async fn before_execute(&self, tool_id: &str, args: &mut serde_json::Value) -> Result<()>; async fn after_execute(&self, tool_id: &str, output: &ToolOutput) -> Result<()>;}Crates
Section titled “Crates”| Crate | Responsibility |
|---|---|
openoxide-tools | Tool trait, ToolRegistry, dispatch, parallel execution, truncation |
openoxide-exec | Shell tool handler, sandbox integration |
openoxide-edit | File editing tool handlers (edit, write, apply_patch) |
openoxide-search | Grep, glob, file-search tool handlers |
openoxide-mcp | MCP client, MCP tool adapter |
Key Design Decisions
Section titled “Key Design Decisions”- Async init per tool. Tools can check preconditions (binary exists, config valid) at startup.
- Pre-execution schema validation. Catch malformed arguments before running potentially destructive tools.
- Mutating flag for parallelism. Simple, explicit control over concurrent execution.
- Default truncation with opt-out. Prevents context window blowout from tool output.
- Unified registry for local and MCP tools. Single dispatch path simplifies the agent loop.