Tool System

The tool system defines how agents interact with the external world. Every capability an agent has -- running shell commands, reading files, calling APIs, delegating to sub-agents -- is expressed as a tool implementing the AgentTool trait. The agent loop discovers tools by name from a registry, executes them with lifecycle events, and feeds results back to the LLM.

Concept Overview

Tool [EXISTS]
├── AgentTool trait [EXISTS] — name, label, description, parameters_schema, execute
├── ToolContext [EXISTS] — tool_call_id, tool_name, cancel, on_update, on_progress
├── ToolResult [EXISTS] — content, details, child_loop_id
├── ToolError [EXISTS] — Failed/NotFound/InvalidArgs/Cancelled
├── ToolExecutionStrategy [EXISTS] — Sequential/Parallel/Batched
├── SubAgentTool [EXISTS] — spawns child agent loop
├── Sources: Built-in [EXISTS] / OpenAPI [EXISTS] / MCP [EXISTS]
└── Callbacks: before/after_tool_execution, before/after_update [EXISTS]

AgentTool Trait [EXISTS]

The core extension point. Implement this trait to create custom tools.

MethodSignatureStatusDescription
name()-> &str[EXISTS]Unique identifier used in LLM tool_use (e.g. "bash")
label()-> &str[EXISTS]Human-readable label for UI display
description()-> &str[EXISTS]Description sent to the LLM so it knows when/how to use the tool
parameters_schema()-> serde_json::Value[EXISTS]JSON Schema for parameters; LLM uses this to format arguments
execute()(params, ctx) -> Result<ToolResult, ToolError>[EXISTS]Execute the tool with LLM-chosen arguments and system-injected context

Design: params (LLM input) and ctx (system environment) are deliberately separate parameters. params varies per call; ctx provides cancellation, streaming callbacks, and correlation IDs that are the same shape for every tool.


ToolContext [EXISTS]

Per-invocation context passed to execute(). Using a struct (rather than individual parameters) future-proofs the trait -- adding fields is non-breaking.

FieldTypeStatusDescription
tool_call_idString[EXISTS]Unique ID for this invocation; correlates Start/Update/End events
tool_nameString[EXISTS]Name of the tool being invoked
cancelCancellationToken[EXISTS]Check is_cancelled() in long-running tools; child token of the parent loop's token
on_updateOption<ToolUpdateFn>[EXISTS]Callback for streaming partial ToolResults (UI/logging only; not sent to LLM)
on_progressOption<ProgressFn>[EXISTS]Callback for user-facing progress text (emits ProgressMessage events)

Callback wiring: The agent loop creates on_update and on_progress closures that capture a cloned tx channel sender. When a tool calls on_update(partial), the closure pushes an AgentEvent::ToolExecutionUpdate into the channel. The tool never touches the event system directly.


ToolResult [EXISTS]

What a tool hands back to the runtime after execution.

FieldTypeStatusDescription
contentVec<Content>[EXISTS]Tool output (text, images, etc.)
detailsserde_json::Value[EXISTS]Freeform metadata (not sent to LLM)
child_loop_idOption<String>[EXISTS]Set by SubAgentTool to the child loop's ID; None for regular tools

Note: The runtime transforms struct ToolResult into Message::ToolResult by enriching it with correlation metadata (tool_call_id, tool_name, is_error, timestamp) before it enters the LLM conversation.


ToolError [EXISTS]

Error taxonomy for tool execution failures. Errors are converted to ToolResult with is_error=true so the LLM sees the failure and can self-correct.

VariantDisplayStatus
Failed(String)"{message}"[EXISTS]
NotFound(String)"Tool not found: {name}"[EXISTS]
InvalidArgs(String)"Invalid arguments: {message}"[EXISTS]
Cancelled"Cancelled"[EXISTS]

ToolExecutionStrategy [EXISTS]

Controls how multiple tool calls from a single LLM response are executed. Set at agent construction time (not a per-turn LLM decision).

VariantStatusBehavior
Sequential[EXISTS]One at a time; checks steering between each. Use for tools with shared mutable state
Parallel (default)[EXISTS]All concurrent via futures::join_all; checks steering after all complete. Best latency for independent tools
Batched { size }[EXISTS]N tools in parallel per batch; checks steering between batches. Balances speed with human-in-the-loop control

Steering: The human-in-the-loop interrupt mechanism. Between tool executions (or batches), the loop checks whether the human has sent a new instruction, cancellation, or correction.


SubAgentTool [EXISTS]

A tool that delegates work to a child agent loop. When the parent LLM calls it, a fresh agent_loop() runs with its own system prompt, tools, and provider. The child loop's final text output is returned as the tool result.

AttributeStatusDescription
tool_name[EXISTS]Unique name for the sub-agent tool
tool_description[EXISTS]Description for the parent LLM
system_prompt[EXISTS]Child agent's system prompt
model_config[EXISTS]Child agent's model configuration
provider_override[EXISTS]Optional custom provider (testing)
tools[EXISTS]Tools available to the child agent
thinking_level[EXISTS]Thinking level for the child loop

Design constraints: Sub-agents are NOT given other SubAgentTools (static depth prevention). Cancellation propagates from parent to child. Events stream back to the parent via on_update.


Built-in Tools [EXISTS]

Six tools returned by default_tools():

ToolFileStatusDescription
BashTooltools/bash.rs[EXISTS]Run shell commands
ReadFileTooltools/file.rs[EXISTS]Read file contents
WriteFileTooltools/file.rs[EXISTS]Write or overwrite a file
EditFileTooltools/edit.rs[EXISTS]Precise text replacement within a file
ListFilesTooltools/list.rs[EXISTS]List directory contents
SearchTooltools/search.rs[EXISTS]Grep / content search across files

OpenAPI Tools [EXISTS]

OpenApiToolAdapter parses an OpenAPI 3.0 spec and creates one AgentTool per operation. Each adapter makes HTTP requests to the API endpoint when executed. Feature-gated behind the openapi Cargo feature.

Factory methods: from_str, from_file, from_url, from_spec.


MCP Tools [EXISTS]

McpToolAdapter bridges MCP server tools to the AgentTool trait using the Adapter pattern. All adapters for the same server share one McpClient (via Arc<Mutex<McpClient>>). Name collision prevention uses an optional prefix namespace (e.g. "filesystem__read_file").


Tool Callbacks [EXISTS]

Lifecycle hooks that fire around tool execution. All are Option<Arc<dyn Fn(...)>> on AgentLoopConfig.

HookSignatureStatusFires When
before_tool_execution(tool_name, tool_call_id, args) -> bool[EXISTS]Before ToolExecutionStart; return false to skip the call
after_tool_execution(tool_name, tool_call_id, is_error)[EXISTS]After ToolExecutionEnd
before_tool_execution_update(tool_name, tool_call_id, text) -> bool[EXISTS]Before each ToolExecutionUpdate; return false to suppress the event
after_tool_execution_update(tool_name, tool_call_id, text)[EXISTS]After each ToolExecutionUpdate (only if not suppressed)

Hook ordering: Hooks fire strictly before their paired event is emitted. When before_tool_execution returns false, no ToolExecutionStart/End events are emitted; a synthetic error ToolResult is sent to the LLM so it knows the call was skipped.


Code Reference

ConceptFile
AgentTool trait, ToolContext, ToolResult, ToolError, ToolExecutionStrategysrc/types/tool.rs
ToolUpdateFn, ProgressFn type aliasessrc/types/tool.rs
Tool dispatch, execute_tool_calls, execute_single_tool, skip_tool_callsrc/agent_loop/tools.rs
SubAgentToolsrc/agents/sub_agent.rs
Built-in tools (BashTool, ReadFileTool, etc.)src/tools/
OpenApiToolAdaptersrc/openapi/adapter.rs
McpToolAdaptersrc/mcp/tool_adapter.rs
Tool callback type aliases (BeforeToolExecutionFn, etc.)src/agent_loop/config.rs
ToolDefinition (schema sent to LLM, not executable)src/provider/traits.rs

Conceptual Notes

  • Tool Permission System [CONCEPTUAL] -- The plan includes an Agent-level Permissions tab with include/exclude rules for allowed/denied actions. This would gate tool execution at a higher level than the before_tool_execution hook.
  • Tool Result Streaming to LLM [CONCEPTUAL] -- Currently on_update partial results are UI-only. A future design could allow streaming tool results to the LLM mid-execution for real-time reasoning.
  • ToolDefinition vs AgentTool Split -- ToolDefinition (in provider/traits.rs) is the schema half sent to the LLM; AgentTool (in types/tool.rs) is the executable half. The agent loop bridges them: converts AgentTool to ToolDefinition before streaming, then matches ToolCall content back to AgentTool by name for execution.