-
Notifications
You must be signed in to change notification settings - Fork 4.4k
07.2 Tool Execution
Relevant source files
The following files were used as context for generating this wiki page:
This document describes the mechanics of tool execution in the ZeroClaw agent loop: how tools are discovered, validated, executed with security enforcement, and their results formatted for the conversation history. This covers the implementation details of the tool execution phase within a single agent turn.
For information about the overall agent turn cycle and tool call loop iteration, see Agent Turn Cycle. For tool definitions and available tools, see Tools and its subsections. For security policy configuration, see Security Model.
All tools implement the Tool trait defined in src/tools/traits.rs. The trait requires four methods:
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn parameters_schema(&self) -> serde_json::Value;
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult>;
}The ToolSpec structure packages tool metadata for LLM consumption:
Sources: src/tools/traits.rs
Tools are registered into a Vec<Box<dyn Tool>> during agent initialization. The registry is constructed in layers:
- Default tools (shell, file_read, file_write): src/tools/mod.rs:72-86
- All tools (includes memory, cron, git, browser, HTTP, etc.): src/tools/mod.rs:121-238
The registry construction checks feature flags and configuration to conditionally include tools:
| Tool Category | Condition | Source Lines |
|---|---|---|
| Core tools | Always included | src/tools/mod.rs:136-138 |
| Memory tools | Always included | src/tools/mod.rs:145-147 |
| Cron tools | Always included | src/tools/mod.rs:139-144 |
| Browser tools | browser_config.enabled |
src/tools/mod.rs:160-185 |
| HTTP tools | http_config.enabled |
src/tools/mod.rs:187-194 |
| Web search | web_search.enabled |
src/tools/mod.rs:196-204 |
| Composio |
composio_key present |
src/tools/mod.rs:210-218 |
| Delegation | !agents.is_empty() |
src/tools/mod.rs:220-235 |
Tool Registry Construction Flow
graph TB
subgraph "all_tools_with_runtime"
Init["Initialize tools Vec"]
CoreTools["Add core tools<br/>(shell, file_read, file_write)"]
CronTools["Add cron tools<br/>(add, list, remove, update, run, runs)"]
MemoryTools["Add memory tools<br/>(store, recall, forget)"]
UtilTools["Add utility tools<br/>(schedule, proxy_config, git, pushover)"]
VisionTools["Add vision tools<br/>(screenshot, image_info)"]
CheckBrowser{"browser_config.enabled?"}
AddBrowser["Add browser_open + browser"]
CheckHttp{"http_config.enabled?"}
AddHttp["Add http_request"]
CheckWebSearch{"web_search.enabled?"}
AddWebSearch["Add web_search"]
CheckComposio{"composio_key present?"}
AddComposio["Add composio"]
CheckAgents{"agents.is_empty()?"}
AddDelegate["Add delegate"]
Return["Return Vec<Box<dyn Tool>>"]
end
Init --> CoreTools
CoreTools --> CronTools
CronTools --> MemoryTools
MemoryTools --> UtilTools
UtilTools --> VisionTools
VisionTools --> CheckBrowser
CheckBrowser -->|Yes| AddBrowser
CheckBrowser -->|No| CheckHttp
AddBrowser --> CheckHttp
CheckHttp -->|Yes| AddHttp
CheckHttp -->|No| CheckWebSearch
AddHttp --> CheckWebSearch
CheckWebSearch -->|Yes| AddWebSearch
CheckWebSearch -->|No| CheckComposio
AddWebSearch --> CheckComposio
CheckComposio -->|Yes| AddComposio
CheckComposio -->|No| CheckAgents
AddComposio --> CheckAgents
CheckAgents -->|No| AddDelegate
CheckAgents -->|Yes| Return
AddDelegate --> Return
Sources: src/tools/mod.rs:121-238
During execution, tools are discovered by name using find_tool:
This performs a linear search through the tools registry, returning Option<&dyn Tool>. Tool names must match exactly (case-sensitive).
Tool Name Resolution
graph LR
LLMResponse["LLM Response<br/>with tool_calls"]
ParsedCall["ParsedToolCall<br/>{name, arguments}"]
FindTool["find_tool(tools, name)"]
ToolRef["&dyn Tool"]
Execute["tool.execute(args)"]
NotFound["Tool not found<br/>error in result"]
LLMResponse --> ParsedCall
ParsedCall --> FindTool
FindTool -->|Found| ToolRef
FindTool -->|None| NotFound
ToolRef --> Execute
Sources: src/agent/loop_.rs:275-278
The agent loop supports multiple tool call formats to accommodate different LLM providers and prompting strategies. All formats are parsed into a unified ParsedToolCall structure:
struct ParsedToolCall {
name: String,
arguments: serde_json::Value,
}The parser attempts formats in order of specificity:
-
Native function calling (OpenAI/Anthropic format with structured
tool_callsarray) -
XML-style tags (
<tool_call>,<toolcall>,<tool-call>,<invoke>) -
Markdown code blocks (
```tool_calllanguage specifier) -
GLM-style proprietary format (
tool_name/param>value)
Tool Call Parsing Flow
graph TB
Response["LLM Response Text"]
TryJSON{"Parse as JSON with<br/>tool_calls array?"}
JSONSuccess["parse_tool_calls_from_json_value"]
TryXML{"Find XML tags?<br/><tool_call>, <invoke>, etc."}
XMLParse["Extract JSON from tag body"]
TryMarkdown{"Find ```tool_call<br/>code blocks?"}
MarkdownParse["Extract JSON from block"]
TryGLM{"Find GLM format?<br/>tool/param>value"}
GLMParse["parse_glm_style_tool_calls"]
NoCalls["No tool calls found<br/>Return empty Vec"]
ReturnCalls["Return Vec<ParsedToolCall>"]
Response --> TryJSON
TryJSON -->|Yes| JSONSuccess
TryJSON -->|No| TryXML
JSONSuccess --> ReturnCalls
TryXML -->|Yes| XMLParse
TryXML -->|No| TryMarkdown
XMLParse --> ReturnCalls
TryMarkdown -->|Yes| MarkdownParse
TryMarkdown -->|No| TryGLM
MarkdownParse --> ReturnCalls
TryGLM -->|Yes| GLMParse
TryGLM -->|No| NoCalls
GLMParse --> ReturnCalls
NoCalls --> ReturnCalls
Sources: src/agent/loop_.rs:595-748
Providers that support native tool calling (OpenAI, Anthropic, OpenRouter, etc.) return structured ToolCall objects:
src/providers/traits.rs (referenced from src/agent/loop_.rs:750-759)
These are parsed by parse_structured_tool_calls:
For providers using prompt-guided tool calling, the system prompt instructs the LLM to wrap tool calls in XML tags:
<tool_call>
{"name": "shell", "arguments": {"command": "ls -la"}}
</tool_call>Supported tag variants: <tool_call>, <toolcall>, <tool-call>, <invoke> src/agent/loop_.rs:349
The parser extracts JSON from within the tags using extract_json_values src/agent/loop_.rs:412-448
Sources: src/agent/loop_.rs:349-748
GLM models use proprietary tool call formats:
shell/command>ls -la
browser_open/url>https://example.com
http_request/url>https://api.example.com
These are parsed by parse_glm_style_tool_calls src/agent/loop_.rs:512-580, which:
- Maps tool name aliases (
browser_open→shell) - Extracts parameter name and value from the
param>valuesyntax - Constructs appropriate JSON arguments for the target tool
Sources: src/agent/loop_.rs:491-580
The parser deliberately refuses to extract arbitrary JSON from response text. Tool calls must be explicitly marked with one of the supported formats. This prevents prompt injection attacks where malicious content (in emails, files, or web pages) could include JSON that mimics a tool call.
Sources: src/agent/loop_.rs:732-740
Every tool execution passes through security checks before execution. The checks occur at two levels:
- Security policy enforcement (autonomy level, rate limiting)
- Tool-specific validation (workspace restrictions, domain allowlists, etc.)
The SecurityPolicy::enforce_tool_operation method blocks execution based on autonomy level:
| Autonomy Level | Behavior |
|---|---|
ReadOnly |
Blocks all ToolOperation::Act operations |
Supervised |
Allows actions (may prompt for approval in CLI) |
Full |
Allows all actions |
Tools call this before performing actions:
if let Err(error) = self.security.enforce_tool_operation(ToolOperation::Act, "tool_name") {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some(error),
});
}Examples:
- src/tools/composio.rs:491-500 (Composio execute action)
- src/tools/delegate.rs:174-183 (Delegate to sub-agent)
- src/tools/pushover.rs:115-121 (Send notification)
Sources: src/tools/composio.rs:491-500, src/tools/delegate.rs:174-183, src/tools/pushover.rs:115-121
After autonomy checks, tools call security.record_action() to enforce rate limits:
if !self.security.record_action() {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some("Action blocked: rate limit exceeded".into()),
});
}This increments the action counter and checks against max_actions_per_hour from the security policy.
Example: src/tools/pushover.rs:123-129
Sources: src/tools/pushover.rs:123-129
In CLI mode with supervised autonomy, tools that require approval trigger an interactive prompt before execution:
The approval manager checks needs_approval(tool_name), prompts the user in CLI mode (or auto-approves in channel mode), and records the decision. If denied, the tool is not executed and a "Denied by user" result is returned.
Sources: src/agent/loop_.rs:996-1024
Tool Execution Sequence
sequenceDiagram
participant Loop as run_tool_call_loop
participant Provider as LLM Provider
participant Parser as parse_tool_calls
participant Approval as ApprovalManager
participant Security as SecurityPolicy
participant Tool as Tool::execute
participant Scrubber as scrub_credentials
Loop->>Provider: chat(messages, tools, model, temp)
Provider-->>Loop: response with tool_calls
Loop->>Parser: parse_tool_calls(response_text)
alt Native tool_calls present
Parser-->>Loop: parse_structured_tool_calls
else XML/Markdown/GLM format
Parser-->>Loop: ParsedToolCall Vec
end
loop For each tool_call
alt Approval needed (CLI + Supervised)
Loop->>Approval: needs_approval(tool_name)?
Approval-->>Loop: prompt_cli(request)
alt User denied
Loop->>Loop: Record "Denied by user"
Note over Loop: Skip execution, continue to next
end
end
Loop->>Loop: find_tool(tools_registry, name)
alt Tool not found
Loop->>Loop: Append error to results
else Tool found
Loop->>Tool: execute(arguments)
Tool->>Security: enforce_tool_operation(Act, tool_name)
alt Security denied
Security-->>Tool: Err(reason)
Tool-->>Loop: ToolResult{success: false}
else Security allowed
Tool->>Security: record_action() for rate limit
alt Rate limit exceeded
Security-->>Tool: false
Tool-->>Loop: ToolResult{success: false}
else Within limits
Tool->>Tool: Perform actual operation
Tool-->>Loop: ToolResult{success, output, error}
end
end
Loop->>Scrubber: scrub_credentials(result.output)
Scrubber-->>Loop: sanitized_output
Loop->>Loop: Format as XML or native role:tool message
end
end
Loop->>Loop: Append tool results to history
Loop->>Provider: chat(updated_history) for next iteration
Sources: src/agent/loop_.rs:851-1132
All tool output passes through scrub_credentials before being added to the conversation history:
This function:
- Matches credential patterns (token, api_key, password, secret, etc.)
- Preserves first 4 characters for context
- Redacts the remainder with
*[REDACTED]
Example transformations:
-
api_key: sk_live_abcdef123456→api_key: sk_l*[REDACTED] -
"password": "hunter2secret"→"password": "hunt*[REDACTED]"
Sources: src/agent/loop_.rs:45-77
Tool results are formatted differently depending on whether the provider uses native function calling:
For providers with supports_native_tools(), results use role: tool messages with the original tool call ID:
The assistant's response is recorded as JSON with both content and tool_calls:
For prompt-guided providers, results are wrapped in XML tags:
<tool_result name="shell">
stdout content here
</tool_result>Sources: src/agent/loop_.rs:764-787, src/agent/loop_.rs:1046-1089
All tools return a ToolResult structure:
pub struct ToolResult {
pub success: bool,
pub output: String,
pub error: Option<String>,
}src/tools/traits.rs (referenced from tool implementations)
| Field | Purpose |
|---|---|
success |
Boolean indicating whether the operation succeeded |
output |
Primary output text (stdout, API response, etc.) |
error |
Optional error message (populated when success: false) |
Tools should:
- Return
success: truewith meaningfuloutputfor successful operations - Return
success: falsewitherror: Some(message)for failures - Keep
outputempty whensuccess: false(error details go inerrorfield)
Successful execution:
Ok(ToolResult {
success: true,
output: "File written successfully: 42 bytes".into(),
error: None,
})Validation failure:
Ok(ToolResult {
success: false,
output: String::new(),
error: Some("Missing required parameter 'path'".into()),
})Security denial:
Ok(ToolResult {
success: false,
output: String::new(),
error: Some("Action blocked: autonomy is read-only".into()),
})Examples from codebase:
- Success: src/tools/pushover.rs:199-206
- Failure: src/tools/composio.rs:482-486
- Security denial: src/tools/delegate.rs:178-183
Sources: src/tools/traits.rs, src/tools/pushover.rs:199-206, src/tools/composio.rs:482-486, src/tools/delegate.rs:178-183
The Pushover tool demonstrates the minimal implementation pattern:
- Check security policy src/tools/pushover.rs:115-121
- Record action for rate limiting src/tools/pushover.rs:123-129
- Validate parameters src/tools/pushover.rs:131-156
- Load credentials from environment src/tools/pushover.rs:157
- Execute external operation src/tools/pushover.rs:159-182
- Return ToolResult src/tools/pushover.rs:198-213
Sources: src/tools/pushover.rs:114-214
The Composio tool shows advanced patterns:
- Multi-action dispatch (list, execute, connect) src/tools/composio.rs:449-583
- Dual API version support (v2 fallback for v3) src/tools/composio.rs:49-137
- Nested parameter extraction src/tools/composio.rs:502-512
- External API error handling src/tools/composio.rs:634-650
Sources: src/tools/composio.rs:438-584
The delegate tool demonstrates recursive agent patterns:
- Depth limit enforcement src/tools/delegate.rs:161-172
- Provider creation for sub-agent src/tools/delegate.rs:193-206
- Timeout wrapping src/tools/delegate.rs:218-227
- Credential inheritance src/tools/delegate.rs:186-191
Sources: src/tools/delegate.rs:104-265