Skip to main content

Module executor

Module executor 

Source
Expand description

Tool dispatch: registry, policy, executor, and concurrency configuration.

The executor is the single entry point through which the agent loop invokes tools. It handles three failure modes uniformly — returning a tool_result Content with is_error: true so the LLM can observe and adapt, rather than terminating the loop:

  1. Policy denialToolPolicy::is_allowed returned false.
  2. Missing tool — the LLM invoked a name that is not in the registry.
  3. Tool error — the tool itself returned Err(ToolError).

Separating dispatch from the agent loop lets sub-agents (and future orchestration tools) share the same registry via ToolContext.

§Concurrency model

execute_batch partitions the LLM-issued batch into contiguous runs of the same routing class — ReadOnly, ConcurrentMut, SerialMut, or ShortCircuit. Each run dispatches concurrently via futures::stream::FuturesUnordered; runs are sequentialised against each other. The barrier between class boundaries preserves the LLM’s intended ordering across mixed batches — [Read A, Write A, Read A] always observes the write before the trailing read, because the executor cannot know which path each tool touches and falls back on the LLM’s emitted order.

Admission control is layered: each call acquires permits from at most two semaphores before invoking the tool — an optional per-tool semaphore (cap configured by the consumer for that specific tool name) and a mandatory class semaphore (read pool, serial-mutator pool, or concurrent-mutator pool, chosen by the tool’s ToolClass, is_recursive flag, and whether it was promoted via crate::AgentBuilder::tool_concurrency).

Default-mutating tools that haven’t been promoted route to the serial-mutator pool of width 1 — preserving the pre-concurrency “strict serial” semantics as a special case of the new model. Recursive tools (per crate::Tool::is_recursive) route to the concurrent-mutator pool regardless of explicit promotion, because their execute body parks the dispatcher task while running a nested Agent::run; routing them through the per-level-forked concurrent_mut pool prevents permit-held-during-nested-execute deadlock without breaking the global serialisation contract.

Sub-agents do not simply share the parent’s executor. They call ToolExecutor::fork_for_subagent to obtain a fresh executor with the same registry / policy / approval but with the deadlock-prone pools (concurrent_mut, per-tool) forked to fresh Arc<Semaphore>s — the non-deadlock-prone pools (serial_mut, read) stay shared so default-Mutating tools still serialise globally across the agent tree. See ConcurrencyConfig::fork for the exact policy.

Structs§

AllowAll
Default policy: every tool is allowed.
ConcurrencyConfig
Internal concurrency configuration owned by ToolExecutor.
ToolCall
A single tool invocation decoded from an LLM tool_use block.
ToolConcurrency
Per-tool concurrency configuration.
ToolExecutor
Dispatches tool calls against a registry, gated by a policy and an approval handler, with admission control via ConcurrencyConfig.
ToolRegistry
Name-keyed collection of tools. Construction is one-shot from a Vec<Arc<dyn Tool>> — swap the whole registry if you need to reconfigure.

Traits§

ToolPolicy
Decides whether the agent may invoke a given tool.