class ToolCall
Executes pending tool calls from the LLM response.
Existing features (ref: opencode tool.ts wrap / truncate.ts):
- Universal output truncation — after every tool.call(), pass the result string through Brute::Truncation.truncate() which enforces a 2000-line / 50 KB cap. This is a safety net so no single tool result can blow up the context window, regardless of whether the tool itself has internal limits.
- Overflow to disk — when truncating, the full output is saved to a temp file under the truncation directory. The path is included in the truncated result with a hint.
- Configurable limits — MAX_LINES / MAX_BYTES default to 2000 / 50 KB.
- Skip truncation when tool already truncated — if the tool result already contains the truncation marker (e.g. Shell or FSSearch truncated internally), don't double-truncate.
== Concurrency model (Async)
Tool calls are executed concurrently using the async gem's fiber-based
scheduler. Each tool call is dispatched as an Async::Task inside an
Async::Barrier, so all tools run in parallel and we wait for every task
to complete before moving on.
Key design decisions:
-
Sync
(not Async.wait) — reuses an existing event loop if one is already running, or creates one on demand. Blocks the caller until all inner work completes, which is what the middleware stack requires. -
Async::Barrier — the idiomatic fan-out / join primitive. Each tool call becomes a child task via barrier.async; barrier.wait blocks until every task finishes. This is preferable to Async::Queue for a fixed batch of work with no producer/consumer relationship.
-
Deterministic result ordering — tool results are collected into an array during concurrent execution, then sorted back into the original tools_to_run key order before appending to env[:messages]. This ensures the LLM always sees results in a stable order regardless of which tool finishes first.
-
Fiber-safe shared state — appending to the results array from multiple fibers is safe because Async fibers are cooperatively scheduled (only one fiber runs at a time within a Sync block). No mutex needed.
-
FileMutationQueue compatibility — tools that mutate files use Brute::Queue::FileMutationQueue.serialize, which uses Ruby 3.4's fiber-scheduler-aware Mutex. Operations on the same file are serialized; operations on different files proceed in parallel.