Skip to content

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.


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.

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:

  1. get_edits() — subclass-specific parser extracts (path, content) tuples from self.partial_response_content
  2. apply_edits_dry_run(edits) — validates edits can be applied
  3. apply_edits(edits) — writes changes to disk

The model never “calls a tool” in the API sense. It produces structured text that Aider interprets.

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).

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_call
for 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] = v

The 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.

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.


Pinned commit: 4ab44e2c5

Codex has a full tool system built on Rust traits, with a registry, router, parallel dispatch, approval integration, and MCP support.

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.

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.

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.

Individual handler files in codex-rs/core/src/tools/handlers/:

HandlerToolPurpose
shell.rsshellLocal shell command execution
apply_patch.rsapply_patchFile patch application (Codex Patch Format)
read_file.rsread_fileFile content reading
grep_files.rsgrep_filesCode search via ripgrep
js_repl.rsjs_replJavaScript REPL execution
multi_agents.rscreate_agentSpawning sub-agents
unified_exec.rsunified_execConPTY-based command execution
mcp.rsmcp__*MCP server tool calls

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.

File: codex-rs/core/src/tools/registry.rs:78

  1. Lookup handler: self.handler(tool_name) returns Arc<dyn ToolHandler> or error
  2. Check mutating: If handler.is_mutating() returns true, wait on tool_call_gate (line 152) for serial execution
  3. Execute: handler.handle(invocation).await runs the tool
  4. Post-hook: dispatch_after_tool_use_hook() fires with output preview, success flag, duration, and mutating flag
  5. Build response: output.into_response(&call_id, &payload) creates the API response item

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:

  1. build_tool_call() extracts the tool call from the response item
  2. build_tool_call() asks session.parse_mcp_tool_name(&name) (line 82); MCP-qualified names route to MCP dispatch
  3. Otherwise, it dispatches to the local registry

MCP tool calls go through a separate path in codex-rs/core/src/mcp_tool_call.rs:

  1. Parse JSON arguments
  2. Request approval via maybe_request_mcp_tool_approval()
  3. Execute via sess.call_tool(&server, &tool_name, arguments)
  4. Emit McpToolCallBegin / McpToolCallEnd events
  5. Return ResponseInputItem::McpToolCallOutput

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.

Tool results are returned as ResponseInputItem variants:

  • FunctionCallOutput — for built-in tools (shell, apply_patch, etc.)
  • McpToolCallOutput — for MCP server tools
  • CustomToolCallOutput — for dynamic/custom tools

These are appended to the conversation history and included in the next API request.


Pinned commit: 7ed449974

OpenCode uses a TypeScript-native tool system with Zod schemas, async initialization, automatic output truncation, and deep plugin integration.

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.

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:

  1. Input validation: toolInfo.parameters.parse(args) — throws formatted error if invalid
  2. Output truncation: Truncate.output(result.output, {}, agent) — unless the tool handles it itself (checked via result.metadata.truncated !== undefined)

File: packages/opencode/src/tool/registry.ts:84

export async function tools(
model: { providerID: string; modelID: string },
agent?: Agent.Info,
)

The registry:

  1. Calls all() to collect all tool definitions (built-in + custom plugins from {tool,tools}/*.{js,ts} directories)
  2. Filters by model: apply_patch is enabled for selected GPT model IDs (gpt-* excluding gpt-4 and *oss), while other models use edit/write (line 143-148)
  3. Initializes each tool: await t.init({ agent }) — lazy, per-request
  4. Fires plugin hook: Plugin.trigger("tool.definition", { toolID }, output) — allows schema modification
  5. Returns array of initialized tools with descriptions, parameters, and execute functions

Key tools in packages/opencode/src/tool/:

FileTool IDPurpose
bash.tsbashShell command execution
edit.tseditSEARCH/REPLACE file editing
write.tswriteFull file write
apply_patch.tsapply_patchV4A diff application (selected GPT models)
read.tsreadFile reading with line ranges
glob.tsglobFile pattern matching
grep.tsgrepRipgrep code search
todo.tstodowriteTask list writing (todoread exists but is currently disabled in the registry list)
webfetch.tswebfetchWeb URL fetching
task.tstaskSub-agent spawning
codesearch.tscodesearchSemantic code search (Exa)
websearch.tswebsearchWeb search (Exa)

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 correlation
  • abortAbortSignal from the AI SDK
  • messages — full session history
  • metadata(val) — async callback to update tool title/metadata
  • ask(req) — permission request wrapper

File: packages/opencode/src/session/prompt.ts:830

MCP tools are fetched via MCP.tools() and wrapped similarly:

  1. Schema transformed via ProviderTransform.schema() for provider compatibility
  2. Permission check injected: ctx.ask({ permission: key, ... })
  3. Output converted: text content to strings, images and resources to FilePart attachments
  4. Output truncated via Truncate.output()

File: packages/opencode/src/session/processor.ts

The stream processor tracks tool calls through their full lifecycle:

  1. tool-input-start (line 111): Creates ToolPart with status "pending", persists via Session.updatePart() (upsert)
  2. tool-call (line 134): Updates status to "running" and performs doom loop check (3x same call)
  3. tool-result (line 180): Updates status to "completed" with output, metadata, title, timing, and attachments
  4. tool-error (line 204): Updates status to "error" with error string. If PermissionNext.RejectedError, sets blocked = true

Each state transition is persisted to SQLite via Session.updatePart().

Tools have four hook points:

  1. "tool.definition" — modify schema before AI sees it
  2. "tool.execute.before" — intercept before execution (can modify args)
  3. "tool.execute.after" — observe results after execution
  4. "shell.env" — inject environment variables for shell tools

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.

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.

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.

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.

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.

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.

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.


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>,
}
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>;
}

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
}

Adopt Codex’s FuturesOrdered pattern with RwLock gating:

  • is_mutating() == false — acquire read-lock, run in parallel
  • is_mutating() == true — acquire write-lock, run serially

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 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.

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<()>;
}
CrateResponsibility
openoxide-toolsTool trait, ToolRegistry, dispatch, parallel execution, truncation
openoxide-execShell tool handler, sandbox integration
openoxide-editFile editing tool handlers (edit, write, apply_patch)
openoxide-searchGrep, glob, file-search tool handlers
openoxide-mcpMCP client, MCP tool adapter
  1. Async init per tool. Tools can check preconditions (binary exists, config valid) at startup.
  2. Pre-execution schema validation. Catch malformed arguments before running potentially destructive tools.
  3. Mutating flag for parallelism. Simple, explicit control over concurrent execution.
  4. Default truncation with opt-out. Prevents context window blowout from tool output.
  5. Unified registry for local and MCP tools. Single dispatch path simplifies the agent loop.