Skip to content

04 · Tool system: execute calls without losing control

Design tool schemas, dispatch, and policy gates that make parallelism and refusal explicit.

Chapter brief

Question to answer

After a model emits a tool call, where should validation, policy, and dispatch happen?

By the end, you can

  • Write a model-usable tool contract that never widens permissions
  • Separate argument errors, policy refusal, execution failure, and result persistence
  • Decide which calls may run in parallel and which require serial verification
Read this now if
Engineers designing tool schemas, dispatchers, hooks, or MCP adapters
Prerequisites
Be able to read JSON Schema and structured tool calls
Deliverable
A tool-contract and execution-policy pipeline sketch
Evidence boundary
The sources show boundaries and dispatch choices, not one universally superior tool granularity

Scenario: the model emits a valid deploy_service({ env: "prod", version: "v42" }). JSON Schema passes, but the caller has staging-only authority. If the dispatcher treats “valid arguments” as “allowed action,” a perfectly formatted tool call deploys without permission.

Passing conditions: protocol decoding, schema validation, identity resolution, policy, execution, and result persistence are separate stages; model text cannot override policy denial; writes carry operation IDs; results distinguish invalid, denied, failed, committed, and persisted.

For comparison, this chapter models the tool stack as four layers:

Tool system, four layers: definition → registry → dispatch → execution
Outside in: schema definition, registry, dispatch routing, sandbox execution. Each layer can carry permission hooks.

Each layer splits four ways:

Dimension CodexClaude CodeOpenClawHermes
Definition Responses API `function_tool` JSON schema + `apply_patch` inline DSLAnthropic tool spec + built-in Edit / Bash / Read etc.tool-catalog.ts 11 categories + ToolProfileIdregistry single-source definition → adapter for OpenAI / Anthropic / Gemini
Registration Model selection = tool set selection (model + prompt file paired)`canUseTool` hook filters at runtimeToolProfileId: `minimal` / `coding` / `messaging` / `full`All tools registered at startup, runtime filters by user config
Dispatch timing function_call appears → dispatch (serial)After streaming a message, scan `tool_use` blocks, dispatch in parallelpi-agent-core event stream, single session serializedWait for full turn, then dispatch; only subagents run in parallel
Permission layer `execpolicy` + `approval_mode` (auto/on-request/off) + sandbox templates`canUseTool` hook + permission mode + acceptEditstool-policy-pipeline + tool-fs-policy + skill policyper-tool permission check + `skills_guard` hard-deny
MCP `codex-rs/mcp-*` four crates (client / server / protocol / types)Built-in MCP client, tools auto-register as tool_useMCP plugin + tool-display overridesBuilt-in MCP server config, runtime bridges MCP tools to registry tools
Tool system, 4 layers × 4 systems

Compare only implementations that change execution decisions

Section titled “Compare only implementations that change execution decisions”

Codex · Responses function_tool + apply_patch DSL + execpolicy as a three-dimensional permission matrix

Section titled “Codex · Responses function_tool + apply_patch DSL + execpolicy as a three-dimensional permission matrix”

Codex’s starting point on the tool system is: as OpenAI’s own coding agent, it should maximise reuse of OpenAI’s own Responses API capability (function_tool) rather than inventing a new protocol; but function calling has a hard problem in the “large patch” scenario.

Large patches hit provider- and model-specific input limits. The snapshot does not establish a portable token cap, so apply_patch uses a different transport instead of relying on function-call arguments.

Coding agents touch files and processes directly. Command policy, user approval, and sandboxing cover different boundaries; which layers you need depends on the deployment threat model.

Most tools register via the standard Responses function_tool route, each tool a JSON schema (parameters / return values / description all expressed via schema). In this source snapshot, apply_patch is an exception.

It doesn’t go through standard function call but teaches the model the V4A diff format (detailed in ch. 06) to inline output the entire patch in the assistant message, parsed and executed by the Rust apply-patch crate.

This inline DSL moves the patch out of function-call arguments. The boundary changes, but context, output, and provider limits still apply; it is not an unlimited channel.

The permission layer is execpolicy (detailed in ch. 07), one of the more concentrated implementation areas in Codex: every shell command runs through a rule review before execution (allow / ask / deny three tiers); rules are described in Starlark DSL (a Python-like config language), which can be git-tracked and self-tested (match / not_match let CI verify rule correctness).

This command-level review combined with approval_mode (auto auto-approve / on-request ask user when needed / off no review) and sandbox_mode (read-only / workspace-write / danger-full-access) forms a three-dimensional permission matrix (command × approval × sandbox).

Codex users can combine these modes by scenario, for example auto + workspace-write in automation or on-request + read-only in local development. The combinations are broad, but their effect still depends on the deployment boundary.

MCP implementation is split across codex-rs/mcp-client / codex-rs/mcp-server / codex-rs/mcp-protocol / codex-rs/mcp-types, covering client, server, protocol, and types.

MCP tools register as ordinary function tools; local and MCP tools use the same schema at the model interface.

Claude Code · Same-turn parallel multi-tool + canUseTool hook + permission mode 4 modes

Section titled “Claude Code · Same-turn parallel multi-tool + canUseTool hook + permission mode 4 modes”

Claude Code’s starting point on the tool system is that an IDE-integrated coding agent often needs several independent tools for one request, such as reading files, grepping keywords, and finding related patterns. Serial execution adds round-trip wait; parallel execution can reduce it, provided the harness defines how partial failure is represented and recovered.

Anthropic’s tool_use block protocol supports multiple tool_use blocks in one assistant message, which Claude Code uses for parallel dispatch.

Actual implementation uses Anthropic’s native tool_use block protocol: the model outputs multiple tool_use blocks in the assistant message (each block one tool call); after the harness receives the entire message it scans all tool_use blocks, throws them all at dispatchToolUseBlocks using Promise.all for parallel execution.

One engineering detail: the comment at queryLoop line 557 admits “stop_reason === 'tool_use' is unreliable”.

The Anthropic API stop_reason field theoretically should be ‘tool_use’ when a tool needs calling, but in practice sometimes stop_reason is ‘end_turn’ yet the message has tool_use blocks; so Claude Code doesn’t trust stop_reason but counts blocks itself (more reliable).

The permission layer has two coordinating mechanisms. The canUseTool hook lets the runtime filter each call and return a reasoned denial to the model. permission mode provides scenario switches: plan closes the tool channel, acceptEdits auto-approves edits, and default asks per call. bypassPermissions skips permission prompts; enable it only explicitly in an isolated, trusted automation environment, not as a CI default.

Built-in tools such as Edit / Read / Bash / Glob / Grep / Task / TodoWrite / WebFetch / WebSearch and MCP tools share the same tool_use schema. The model can select both through one protocol surface.

OpenClaw · Splits tool stack into 11 categories + 4-tier profile + middleware chain

Section titled “OpenClaw · Splits tool stack into 11 categories + 4-tier profile + middleware chain”

OpenClaw starts from a generic agent control plane spanning coding, messaging, automation, and other workloads, so different scenarios need different tool surfaces. Messaging agents usually do not need filesystem tools, while coding agents usually do not need messaging tools. Exposing everything at once may add selection burden; measure the effect on real task logs.

So OpenClaw classifies tools by function, with each tool clearly belonging to certain scenario profiles, filtered by profile at startup. A messaging agent at startup only sees messaging / web / memory categories.

Actual implementation is tool-catalog.ts organising tools by 11 categories (fs / runtime / web / memory / sessions / ui / messaging / automation / nodes / agents / media); each tool belongs to one or more ToolProfileId:

OpenClaw openclaw/src/agents/tool-catalog.ts:1-39 ToolProfileId + CORE_TOOL_SECTION_ORDER
export type ToolProfileId = "minimal" | "coding" | "messaging" | "full";
const CORE_TOOL_SECTION_ORDER: Array<{ id: string; label: string }> = [
{ id: "fs", label: "Files" },
{ id: "runtime", label: "Runtime" },
{ id: "web", label: "Web" },
{ id: "memory", label: "Memory" },
{ id: "sessions", label: "Sessions" },
{ id: "ui", label: "UI" },
{ id: "messaging", label: "Messaging" },
{ id: "automation", label: "Automation" },
{ id: "nodes", label: "Nodes" },
{ id: "agents", label: "Agents" },
{ id: "media", label: "Media" },
];

ToolProfileId 4 tiers correspond to different agent forms: minimal (smallest tool set, e.g. pure chat agent), coding (opens common coding categories such as fs / runtime / web), messaging (opens messaging / web / memory for customer-service or messaging agents), and full (all open for agents that need a broad tool surface).

This scenario pre-configuration reduces tool-by-tool setup, but a deployment still needs to verify that a profile matches its policy.

tool-policy-pipeline.ts is one of OpenClaw’s more concentrated implementation areas. It organises work before and after a tool call as a middleware chain. The before_tool_call, after_tool_call, and tool_result_persist hook points accept external plugins, putting permission checks, audit, cache, and mocking on one pipeline.

E.g. want to add “check via LLM whether intent matches company policy before calling” check to a tool? Write a plugin registered to before_tool_call.

Beyond the generic middleware chain, OpenClaw has several specialised tool subsystems: tool-loop-detection.ts separately detects “the model is in a dead loop calling the same tool” (avoiding N consecutive same tool calls wasting tokens, on hit forces loop exit); tool-fs-policy.ts is a filesystem-specific second permission layer (detailed in ch. 06 workspaceOnly design, distinct from the generic hook); tool-mutation.ts processes tool results before returning them to the model (e.g. auto-truncating overly long results, masking sensitive fields, adding context hints).

Together these files make up the tool-middleware surface in this source snapshot.

Tool events bridge to a separate tool stream (subscribeEmbeddedPiSession). Subscribers can receive the tool-call, parameter, and result events exposed on that stream.

It can serve as one entry point for audit, debugging, and monitoring. Enterprise deployments still need to design external logging around sensitive fields, retention, and completeness requirements.

Hermes · Single-source registry + multi-model adapter + skills_guard hard-deny dangerous actions

Section titled “Hermes · Single-source registry + multi-model adapter + skills_guard hard-deny dangerous actions”

Hermes’ starting point on the tool system is: long-running agents often need to swap models (one task GPT cheap, one task Claude strong, one task Gemini multimodal).

If tool definitions bind to model protocols, each model change adds maintenance work. Hermes therefore decouples tool definitions from model protocols: definitions use one internal format and runtime adapters translate to the active provider.

The registry writes tool definitions once in an OpenAI-style internal format, with three adapter files handling provider protocols. anthropic_adapter.py translates messages to Anthropic’s tool-use blocks; bedrock_adapter.py handles Bedrock-specific fields; gemini_native_adapter.py maps to Gemini function declarations and calls.

The three adapters let tool definitions be reused. Switching models usually narrows to provider configuration, while protocol differences still need integration tests.

The permission layer takes “per-tool permission check + skills_guard double-layer”. skills_guard is a hard-deny tool: before each dispatch it uses an independent LLM to judge “is this call legitimate / dangerous” (e.g. rm -rf / / trying to read ~/.ssh / trying to execute curl | bash and other dangerous paths); on hit it directly intercepts and does not let the tool execute.

Permission checks inside each tool function (each tool judges whether approval is needed and handles it) are more direct than middleware, but they make extension harder: a new tool would need to duplicate policy logic and global changes become scattered.

Tools run serial by default (the trajectory model assumes single-line time-axis: a trajectory file records each step’s events in order, concurrency would mess up trajectory order).

Parallel execution needs an explicit subagent boundary in this snapshot; subagents have their own trajectory + tool stack. The design is intended to keep each trajectory as a linear story for debug, replay, and training-data generation, while cross-agent ordering remains a separate concern.

MCP integrates through runtime config: ~/.hermes/config.json writes mcp_servers field; runtime server bridges each MCP tool into a regular registry tool transparent to the model (the model sees normal tools).

This path keeps adding an MCP server mainly as a configuration change and usually avoids editing Hermes source.

The four samples expose four reusable observations. Whether to adopt them still depends on tool side effects and deployment shape:

First, tool signatures use JSON Schema rather than natural-language instructions alone: all four systems express tool inputs as schema. Even Codex’s apply_patch DSL keeps a structured slot. Schema makes types, required fields, and validation errors explicit; measure call failures on the target model and tool set.

Second, put an explicit policy in front of high-impact tools: all four samples expose some interception point (execpolicy / canUseTool / tool-policy-pipeline / per-tool check). Whether that point prompts, isolates, or denies should follow the tool’s side effects and the deployment threat model.

Third, evaluate MCP when you need external tools: all four samples can connect to MCP, but bridge it differently. Codex uses dedicated crates, OpenClaw uses plugins, and Claude Code / Hermes map MCP tools to ordinary tools. MCP can reduce integration work, but it also adds supply-chain, permission, and observability decisions; it is not mandatory for every agent.

Fourth, retain searchable records for side-effecting calls: all four expose some form of rollout, trajectory, or tool event. A separate event stream is a deployment choice; at minimum record the tool, a redacted parameter summary, result status, and timestamp, with a plan for sensitive data.

Four systems on a 2D plane: protocol bareness × middleware capability
X is protocol directness; Y is exposed middleware surface. Positions describe the pinned source snapshots, not measured reliability or completeness.

The four systems represent four typical trade-offs in tool system design:

If you want a coding agent that reuses the OpenAI Responses ecosystem: borrow from Codex’s function_tool + apply_patch DSL + execpolicy route. It connects directly to the Responses API, and the DSL changes how large diffs are transported. The execpolicy matrix covers command-execution risks but does not replace other security boundaries. The cost is that extension hooks stop at execpolicy, so a custom verifier or middleware usually needs a fork or wrapper; non-coding scenarios do not have an equivalent command-level gate.

If you need same-turn multi-tool dispatch and several permission modes: borrow from Claude Code’s tool_use + parallel dispatch + permission mode route. Independent tools can run in parallel to reduce waiting; canUseTool filters calls at runtime; plan / acceptEdits / bypassPermissions / default switch policy by scenario. The cost is that stop_reason requires block counting, and the hook surface is smaller than OpenClaw’s middleware chain. This route fits IDE, desktop, and tool-oriented agents.

If you need a multi-tenant control plane with several tool profiles: examine OpenClaw’s catalog, policy pipeline, and profile route. Its categories and profiles separate workloads, while middleware carries permission, audit, cache, and mocking. The cost is a longer debugging chain, and four profiles may still be too coarse for custom deployments.

If you want multi-model compatibility (same set of tools running OpenAI / Anthropic / Gemini): borrow from Hermes’ registry + adapter route. One registry definition runs three protocols. skills_guard uses an LLM to judge dangerous actions, which can express semantic rules but also introduces model error. MCP bridging shortens the external-tool path. The cost is serial execution by default and permission checks spread across tool functions.

Tighten policy before exposing high-risk tools

Section titled “Tighten policy before exposing high-risk tools”
Tool constraintRoute to borrowCost or boundary
Independent, side-effect-free calls share a turnClaude Code parallel tool_useDefine failure aggregation and order semantics before relying on parallel dispatch
Multiple model protocols need one registryHermes adapters and schema translationThe lowest common denominator loses provider features
Many channels need policy middlewareOpenClaw tool-policy pipelineLong chains require durable traces
Coding tools need strict patch and shell gatesCodex tools plus execpolicyThe abstraction stays coding-specific

When building a Tool System, separate declaration, execution results, and policy first. Add observability, loop detection, and provider adapters after those contracts are stable.

Building a tool system

Minimal viable

  • Define tools with JSON Schema (OpenAI function-calling style is a reasonable starting point). Schema makes parameter types and required fields machine-checkable; record parse errors, missing fields, and wrong-tool calls on real tasks
  • Apply permission checks by tool risk. Read-only queries, workspace writes, and external side effects should not share one default rule; ask, deny, or allow according to the threat model and deployment boundary.
  • Log every tool call (name, args, result, timestamp). Issues should be traceable (when a user reports "the agent changed my file", you can identify which tool changed what)
  • Provide a dry-run mode for users to preview commands before execution. First-time risky commands run dry-run first, avoiding the "model momentarily confused and runs rm -rf /" disaster

When the evidence justifies it

  • Abstract an adapter layer so one tool definition runs many protocols (borrow from Hermes' anthropic_adapter / bedrock_adapter / gemini_native_adapter). Model swap without rewriting tools is the key to multi-model agents
  • before/after tool-call hooks system allowing external middleware (borrow from OpenClaw's tool-policy-pipeline). Verifier / audit / cache / mocking can share one pipeline, making extra logic easier to insert
  • Tool profiles (borrow from OpenClaw's minimal / coding / messaging / full). Switch tools by scenario to remove irrelevant choices. Calibrate tool count and wrong-tool rate on your own task logs instead of copying a fixed threshold
  • Emit tool-call events to a separate stream (borrow from OpenClaw's subscribeEmbeddedPiSession). External audit / real-time monitoring / training data collection all flow through this stream without polluting the main conversation

Avoid

  • Permission checks inside every tool function. This repeats policy and makes global changes harder. If policy spans several tools, consider centralising it in a middleware chain
  • Treating `stop_reason === "tool_use"` as the only signal. Anthropic API's stop_reason is unreliable (in practice sometimes stop_reason is end_turn but tool_use blocks exist); code counting blocks itself is the stable approach
  • Built-in tools and MCP tools on two dispatch paths. Model decisions handle two tool types separately (increased complexity), and UI rendering also needs two implementations; unify on one path for transparency
  • Tool parallelism on day one. Parallel handles failure / state isolation / ordering issues all far more complex than serial; stabilize serial dispatch first (including timeout, retry, error handling), then consider parallel optimisation
Full tool-call round trip: model → before → execute → after → tool_result, with deny short-circuit
OpenClaw's four hook points. Codex uses static execpolicy rules; Claude Code gives canUseTool one hook; Hermes scatters checks inside tool functions.

What to carry forward and the next experiment

Section titled “What to carry forward and the next experiment”

A schema proves shape, not authority. Tool systems separate what the model can express from what the host permits, and expose every stage failure as observable state.

Next experiment: issue five calls—invalid arguments, valid-but-denied, allowed read, duplicated write, and two independent parallel calls. Verify distinct states, exactly-once write behavior, and no shared-state race under parallelism. Emit each result into the event stream instead of returning only an error string.

Open the exercises and ten review questions
  1. 🟢 Beginner: Add a before_tool_call hook to your agent. Minimal implementation: print [tool] {name}({args}), with no mutation or blocking. Collect enough real calls to decide which tools belong in the default profile.
  2. 🟠 Intermediate: Build a minimal apply_patch DSL. The model emits *** Begin Patch ... blocks in plain text. Your code parses and applies them. Compare with function-call JSON: how large a diff can you fit?
  3. 🔴 Challenge: Implement a small tool-loop-detection experiment. Start with five repeated calls and low argument variance as test parameters, then tune them against normal retries and labelled loops. Report both the blocking step and false positives.
Q1 · Concept: Disambiguate tool / function call / MCP server / skill.

Tool is the lowest concept: code that “the model triggers, the harness executes, the result returns.” Every other term is a concrete shape of tool.

Function call is the protocol layer. OpenAI standardized tool calls as function call schema (name + parameters JSON schema) in 2023; Anthropic uses tool_use blocks, Gemini uses function_call proto.

Same idea: the model writes I want to call X(args) in a structured field. Codex names it function_tool at the protocol layer (a Responses API wrapper).

MCP server is Anthropic’s 2024 Model Context Protocol. One MCP server exposes a group of tools (via stdio / SSE); the harness bridges them into its tool registry.

All four support it: Codex uses 4 separate crates (mcp-client / mcp-server / mcp-protocol / mcp-types); Claude Code and Hermes feed MCP tools to the model as ordinary tools; OpenClaw goes through plugins.

MCP solves “same set of tools for different agents.”

Skill (chapter 17 covers it) is Anthropic’s higher-level package: a skill = SKILL.md (description) + scripts/ + references/ + assets/, essentially a tool collection with docs and resources.

Hermes and Claude Code both implement skills, lazy-loadable.

Relationships: tool is core, function call is the serialization protocol, MCP is the tool distribution protocol, skill is a tool + resource bundle.

Source: codex/codex-rs/codex-mcp/, claude-code/src/tools/, openclaw/src/agents/tool-catalog.ts, hermes-agent/skills/. Follow-up: “Where does LangChain’s Tool abstraction sit?” Tool + framework-level binding. It has its own protocol layer (not OpenAI function call) and can auto-convert across models. Essentially a mini-MCP, but Python-locked.

Q2 · Architecture: Claude Code dispatches multiple tool_use blocks per turn in parallel; Codex dispatches one function_call at a time. Which is better?

Each fits its scenarios. Decision factors: are tools independent + does parallelism break trajectory semantics.

Claude Code style (multi-tool parallel within a turn):

  • Saves round-trips: the model emits 3 Read tool_uses, three files read in parallel, 2 token round-trips fewer than serial.
  • Maps to natural language: “open A, B, and C” naturally produces three tool_uses; parallel dispatch matches user intent.
  • Downside: a single tool failure can leave three partial results at once; the model and host then need an explicit partial-failure policy.

Codex style (dispatch on function_call appearance):

  • Simple state: one tool finishes, model continues; trajectory is monotonic and traceable.
  • Plays well with verifiers (chapter 05): one verifier check per step, no “which of the 3 parallel calls got verified”.
  • Downside: slow. Three independent reads serially = 3× round-trip.

OpenClaw / Hermes lean Codex style (serial), because the trajectory model assumes a single monotonic timeline; parallelism breaks the invariant “all prior tools completed before this step”.

Practical: start serial (Codex style), get it stable, then add a parallel whitelist (only read-only tools). Day-one full parallelism makes race conditions hell to debug.

Source: claude-code/src/query.ts:440-680 (dispatchToolUseBlocks uses Promise.all); codex/codex-rs/core/src/session/turn.rs (single-function_call dispatch). Follow-up: “What about cross-turn parallel?” That’s subagents (chapter 10). Intra-trajectory parallelism vs cross-trajectory parallelism are two different problems; don’t conflate them.

Q3 · Engineering: Claude Code’s comment says stop_reason === 'tool_use' is unreliable. Why?

The comment lives at query.ts:557: “stop_reason === 'tool_use' is not reliable; count blocks instead”. The root issue: the streaming API may set stop_reason mid-stream when multiple tool_use blocks appear, or it may not appear at all.

Concrete cases:

  1. Mixed tool_use + text: model emits thinking text, then tool_use, then more text, then more tool_use. stop_reason could end up end_turn or tool_use depending on the final block.
  2. Network resume: Anthropic’s streaming may re-send message_stop after a fallback, overwriting stop_reason.
  3. Historical messages: when reconstructing from storage, stop_reason can be dropped.

The reliable approach: walk the content array and count blocks with type === 'tool_use'. Dispatch as many as you found; don’t trust stop_reason. The Claude Code comment tells you Anthropic itself hit this bug internally.

This “don’t trust metadata, recompute from data” pattern shows up everywhere: Codex doesn’t trust finish_reason and re-parses content; Hermes doesn’t trust done and detects trajectory termination itself.

Protocol fields are a fallback; business logic recomputes.

Source: claude-code/src/query.ts:557 (the comment); additional robustness logic in dispatchToolUseBlocks. Follow-up: “Is OpenAI’s finish_reason reliable?” Comparatively, but the same advice applies: count tool_calls array length yourself. Anthropic has shipped more bugs around this field than OpenAI.

Q4 · Architecture: Codex’s apply_patch doesn’t go through function call; it has the model emit V4A diff inline in assistant text. Why?

Core constraint: function_call arguments have a size cap. The exact limit depends on the provider, model, and SDK configuration; a fixed cross-provider number would mislead. Large patches need chunking, another transport, or a host-side upload path.

A large code patch can cross that boundary once serialized as JSON; measure the current API rather than assuming a line or token range.

The apply_patch DSL puts patches directly in assistant text content (not tool_use args). The model emits *** Begin Patch ... *** End Patch blocks, and Codex’s apply-patch crate parses them.

This moves the patch out of function-call arguments. Context, output, and provider limits still apply, so it is not an unlimited channel.

Costs:

  1. The model needs to learn a new DSL. Codex’s prompt includes V4A examples; measure the prompt and output overhead on the model you deploy.
  2. The parser should cover the malformed cases you expect. Models may omit *** End Patch or misplace patch-line spaces; Codex’s apply-patch crate has several tolerance paths in this snapshot, whose coverage should be tested.
  3. Observability suffers. Function_call is grep-able in trajectory log via apply_patch(; a DSL embedded in text needs a custom parser.

Claude Code goes another way: built-in Edit and MultiEdit tools where each edit is a separate tool_use. Edit size depends on the current tool implementation and provider payload limits. Large refactors often need several calls; integration tests should establish the practical chunk size.

Practical:

  • Early project: Claude Code style Edit (small patches, many calls).
  • Mature project needing large refactors: borrow from Codex apply_patch DSL, but keep Edit as fallback.

Source: codex/codex-rs/apply-patch/src/lib.rs (DSL implementation); codex/codex-rs/apply-patch/apply_patch_tool_instructions.md (the teaching prompt). Follow-up: “Is Aider’s diff format the same as V4A?” No. Aider uses unified diff (standard git diff style); V4A is OpenAI’s internal design, structurally stricter for easier parsing. Both move patch content out of function-call arguments; which fits better depends on payload limits, parse failures, and audit needs.

Q5 · Concept: What is tool middleware? What does OpenClaw’s tool-policy-pipeline add over Claude Code’s canUseTool?

Tool middleware = processing chain inserted before/after a tool call, similar to a web framework’s request middleware. Between model emits tool_use and result returns, you can insert arbitrary hook layers.

Claude Code’s canUseTool is a single-point hook: before tool execution, ask “is this call allowed?” and it returns yes/no. Simple, easy, but it can only decide permission. It can’t rewrite args, can’t log, can’t mutate result.

In the pinned OpenClaw snapshot, tool-policy-pipeline is a multi-stage middleware chain:

  1. before_tool_call: deny, rewrite args, inject metadata, trigger confirmation.
  2. tool-mutation: rewrite result post-execution (truncate large results, base64 binary).
  3. after_tool_call: write audit logs, report telemetry, fire webhooks.
  4. tool_result_persist: write the event record to persistent storage.
  5. tool-loop-detection: detect consecutive identical calls and inject a “you’re looping” signal.

The difference is composability. Claude Code’s canUseTool is a single stop and done; OpenClaw stacks 5 middlewares running in registration order.

Whether audit logs, telemetry, rate limits, and human approval all belong in the chain depends on the data, tenant boundary, and compliance obligations.

Cost: debug chains get long; tracing one tool call now spans 5 middlewares. OpenClaw exposes a more detailed trace in dev mode and trims it to essentials in prod in this snapshot; deployments may differ.

Start from a threat model, then configure permission, logging, rate limiting, or approval. A multi-stage pipeline earns its longer debug path when several policies need to evolve and be reused independently; a simpler workload can start with fewer hooks.

Source: openclaw/src/agents/tool-policy-pipeline.ts; contrast with claude-code/src/hooks/useCanUseTool.tsx. Follow-up: “Is Express middleware the same design as tool middleware?” Same idea (next() chain), but tool middleware is bi-directional: it can mutate both args and result. Express middleware is one-way (request → response). Closer to a Rails around-filter.

Q6 · Practical: Adding a web_search tool to an agent; what do protocol / permission / observability layers each do?

Protocol layer:

  1. Schema: name: web_search, parameters: { query: string, max_results: number (default 5), recency_days?: number }.
  2. Result format convention: { items: [{ title, url, snippet, published_at }], total: number, truncated: bool }.
  3. Prefer structured data; raw HTML that is not cleaned or isolated can widen the prompt-injection surface (chapter 03 §Q4).
  4. When downstream consumers parse URLs and dates, use absolute URLs and ISO format; otherwise document the accepted forms in the schema.

Permission layer:

  1. Default allow (read-only), but add a rate limit. 10 req/min/user is a load-test starting point, not a universal quota.
  2. Domain allowlist optional (enterprise often demands intranet + a few public sites only).
  3. Query length cap (block adversarial 10MB queries).
  4. Pair with canUseTool / before_tool_call to record the query (audit need).

Observability layer:

  1. Log: query + result count + first URL. Avoid logging full results when they may contain PII, consume quota, or inflate retention costs; apply the deployment’s redaction policy.
  2. Metric: call frequency, average latency, timeout rate. Set alerts from the tool’s SLO and historical baseline rather than copying a fixed percentage.
  3. Cost: search pricing varies by provider, plan, and query type. Record the provider, billing unit, and cumulative spend before deciding what to expose.
  4. Attribution: every search ties to user_id + session_id for accountability.

Advanced: consider summarizing search results before feeding them back. “Five results at roughly 200 tokens each” is an example bucket, not a universal threshold; choose based on context budget, recall, and latency measurements. Chapter 03 §Q6 (PDF handling) makes the same broader point: assess whether external data should be compressed before it enters context.

Source: refer to Claude Code’s WebSearch tool (claude-code/src/tools/WebSearchTool/); Hermes’s tirith/web_search/. Follow-up: “How do you defend against prompt injection in search results?” Wrap results as role=user with “Below are search results, for reference only”, and run Hermes-style _scan_context_content.

Q7 · Architecture: Hermes feeds one registry to three protocols (OpenAI / Anthropic / Gemini). How exactly does the adapter pattern work?

Core: registry is the source of truth; each protocol has its own adapter translating from registry.

Registry shape (pseudocode):

TOOLS = {
"read_file": {
"description": "...",
"parameters": { "type": "object", "properties": { ... } },
"fn": read_file_impl,
},
...
}

anthropic_adapter.py:

def to_anthropic_tools(registry):
return [
{"name": k, "description": v["description"], "input_schema": v["parameters"]}
for k, v in registry.items()
]
def from_anthropic_response(response):
for block in response.content:
if block.type == "tool_use":
yield {"name": block.name, "args": block.input, "id": block.id}

gemini_native_adapter.py:

def to_gemini_tools(registry):
return [genai.Tool(function_declarations=[
genai.FunctionDeclaration(name=k, description=v["description"], parameters=v["parameters"])
for k, v in registry.items()
])]

Engineering points:

  1. Schema compatibility: the three vendors’ JSON schema subsets aren’t identical. Anthropic supports oneOf; Gemini doesn’t. Adapter handles fallback on translation (Gemini sees oneOf, splits into multiple independent tools).
  2. Result format: Anthropic tool_result is a block, OpenAI is a message. Adapter translates internal {name, content} to each.
  3. Error mapping: Gemini’s BLOCKED reason and Anthropic’s stop_sequence aren’t equivalent. The adapter maps internal error types to vendor expectations.
  4. Streaming differences: OpenAI/Anthropic stream protocols differ significantly (OpenAI: delta + tool_calls increments; Anthropic: event-based). Adapter normalizes stream events into internal {type, content} events.

Cost: each new model provider needs an adapter for message, streaming, and error semantics. Estimate it from the provider contract and your test matrix.

Source: hermes-agent/agent/anthropic_adapter.py, bedrock_adapter.py, gemini_native_adapter.py. Follow-up: “Is LiteLLM the same approach as Hermes adapter?” Yes. LiteLLM is the OSS adapter layer covering 100+ providers. If you don’t want to write your own, plug LiteLLM, but you give up precise control over protocol details (like prompt caching configuration).

Q8 · Engineering: How do you actually decide a “loop” in tool-loop-detection? How do you avoid false positives?

Detection logic (OpenClaw tool-loop-detection.ts):

  1. Maintain a sliding window over recent tool calls; N is a parameter to calibrate.
  2. Compute the share of the same tool name and choose the trigger from labelled trajectories.
  3. Compare argument similarity within repeated calls. Trigger on sustained repetition with little argument change; tune the window and distance per tool type.
  4. On hit, inject a signal in the next tool result: replace or append “[loop detected] you called X in N consecutive steps, try a different approach.”

Why not just deny? Deny forces the model to give up, but sometimes consecutive calls are legitimate (transient API failure retry, re-reading a file after a write). Injecting a signal lets the model decide to pivot, gentler.

Three tricks to avoid false positives:

  1. Tool name alone is insufficient: repeated Read calls may cover different files. A stronger loop signal combines repeated name, near-identical arguments, and no change in results.
  2. Tune argument similarity per tool: a path change in a file reader and a text change in a search tool mean different things. Pick thresholds from labelled normal retries and loops.
  3. Tune the window per tool: N=5 is the default in the pinned OpenClaw snapshot and a reasonable experiment seed, not a universal sweet spot. Compare false positives, false negatives, and detection delay on labelled retries and loops.

Common false positives observed:

  • Data-crawling tasks: 10 consecutive web_search calls with different queries. Fix: exclude search from detection (or rely on args-similarity).
  • TodoWrite: model spamming status updates. Fix: exclude status-update tools.

Run read-only first: detect and log, but do not inject. Enable intervention after representative trajectories cover the main tools and failure modes and human review has estimated false positives and misses. The required observation period depends on traffic.

Source: openclaw/src/agents/tool-loop-detection.ts; Hermes has agent/loop_guard.py with a similar implementation. Follow-up: “Token-budget would catch loops too, right?” A budget limits damage: “we stop when the allowance is gone.” The detector catches drift earlier and can still change course.

Q9 · Concept: What is a tool profile? When do you use OpenClaw’s minimal/coding/messaging/full?

Tool profile = “same agent, different scenarios expose different tool subsets”. Essentially named subsets of the tool set.

OpenClaw’s 4 tiers:

  • minimal: only read-only tools like Read / TodoWrite. For subagents (chapter 10): no file writes, no shell.
  • coding: adds Edit / Bash / Grep / Glob, for the main coding agent.
  • messaging: swaps to SendMessage / ReadMessages / Schedule for customer-support agents: no coding tools.
  • full: everything on, for trusted advanced users.

Why not give every agent full? Three reasons:

  1. Prompt size: tool schemas consume context. Remove tools that a profile cannot use, then measure cache hits and wrong-tool calls on the target model instead of assuming a fixed token cost.
  2. Decision accuracy: more tools can create more confusion, but the size of the effect depends on model, descriptions, and workload. Compare wrong-tool rate on the same evaluation set before and after pruning.
  3. Permission scoping: subagents don’t need to write files; giving them Edit opens attack surface. Least-privilege principle.

Practical:

  • Start with just full (one tier); split when real subagent / messaging scenarios show up.
  • When splitting, don’t split by “technical tool category” (“all fs tools = one tier”); split by “scenario task” (“this agent does what”). The first is technically tidy, the second works in practice.

Codex / Claude Code / Hermes don’t have a profile abstraction, but they have equivalents: Codex uses model + matched prompt files; Claude Code uses canUseTool filters; Hermes uses skills_guard denylists.

Source: openclaw/src/agents/tool-catalog.ts:1-39. Follow-up: “Can you switch profiles at runtime?” Not in OpenClaw (set at startup). Runtime switching is usually done via dynamic skill loading (chapter 17), which is more flexible.

Q10 · Open-ended: Designing a tool system for an open-source agent framework, which features would you mix?

My mix (with rationale for not adopting one system wholesale):

Core layer (required):

  1. Registry single source + multi-protocol adapters (borrow from Hermes). Define an internal representation, then add adapters for the providers you actually need; integration time depends on protocol differences and test coverage.
  2. tool_use parallel dispatch (borrow from Claude Code, but allowlist-only). All read-only tools auto-parallel; all write tools serial. Need a parallel_safe: bool metadata flag.
  3. canUseTool single hook (borrow from Claude Code) + after_tool_call hook (borrow from OpenClaw). Two hooks form the minimum useful middleware set: enough extensibility, debug chain stays manageable.

Middleware layer (site proposal; select by risk):

  1. tool-loop-detection (borrow from OpenClaw). Begin read-only; use the snapshot’s N=5 only as an experiment seed and calibrate against labelled trajectories.
  2. apply_patch DSL fallback (borrow from Codex). Switch transport when integration tests reveal a stable payload truncation or parse-failure boundary; do not assume a universal 8K threshold.
  3. execpolicy static rules (borrow from Codex). Each tool declares risk_level: low/medium/high in registry; auto-apply deny rules.

MCP layer:

  1. Bridge as regular tools (borrow from Claude Code / Hermes; skip Codex). Codex’s 4 crates are too heavy for OSS. An open framework should put MCP tools on the same dispatch path as built-ins; model can’t tell the difference.

Observability layer:

  1. Separate tool event stream (borrow from OpenClaw’s subscribeEmbeddedPiSession). Beyond trajectory, emit to a tool.* topic; external observers subscribe and see all tool activity.
  2. Per-tool token budget. Each tool declares max_tokens in registry; auto-truncate + warn when exceeded. Chapter 15 covers this.

What I’d skip:

  • OpenClaw’s 4-tier profile. OSS users have diverse scenarios; letting them filter themselves beats four fixed tiers.
  • Hermes’s per-tool permission checks. Centralize in canUseTool, easier to maintain.

Rollout cadence:

  • First make core layers 1-3 work on the target task set.
  • Add middleware and adapters only for measured audit, caching, or provider needs.
  • Decide whether MCP and a separate observability stream justify their operational cost.

Effort depends on the existing runtime, provider count, and isolation requirements. Split milestones against the target task set instead of promising a fixed schedule.

Source: synthesized from all four systems; paths are in the SourceTrail at the end. Follow-up: “Why not directly adopt LangChain’s Tool?” LangChain Tool is too thick (each tool is a class), bad for fast iteration. An OSS framework should let a tool be a simple dict; users wrap into a class only when needed.