feat(x): execute sync tools concurrently within a turn batch

Sync tools in one assistant batch now run via Promise.all instead of
sequentially — a tool pending on I/O no longer blocks its siblings.
Three coordinated changes keep the event-sourced runtime sound:

- executeAllowedTools is two-phase: invocation events are appended
  serially in source order (durable before any side effect, deterministic
  log prefix), then all sync executions run concurrently, each appending
  progress/results as they land. Per-call error and cancel semantics are
  unchanged (moved to executeSyncTool).
- append() commits through an internal queue: persist → reduce → stream
  runs to completion per batch, so file order, in-memory order, and
  stream order stay identical even while executions overlap. A failed
  commit rejects only its caller; the chain survives for siblings.
- Abort-registry state is scoped per tool call (turnId:toolCallId) via a
  wrapper, fixing two latent races: createForRun destroying a running
  sibling's tracked processes, and cleanup tearing down the turn-wide
  force-kill scope when the first tool finished.

Wire ordering is untouched: model requests already reference tool
results by the assistant message's source order, pinned by a new test.
No concurrency cap and no per-tool serialization by design; tools that
share state must tolerate racing (file edits already reject stale
writes via their search/replace precondition).

Spec §4.5/§10.5 updated. New runtime tests cover overlap (deadlock
unless concurrent), progress interleaving, sibling failure isolation,
mid-batch cancellation, and crash recovery with multiple open
invocations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-07-08 22:51:51 +05:30
parent 4c4488c3e7
commit d2b68a4684
5 changed files with 502 additions and 65 deletions

View file

@ -154,9 +154,18 @@ semantics after ambiguous interruptions.
Tool calls may complete in any order. The next model request must always contain
tool results in the original order emitted by the model.
The initial implementation may execute sync tools sequentially for simplicity.
Async tools naturally complete independently. No behavior may rely on physical
completion order.
Sync tools in one batch execute concurrently: invocation events are appended
serially in source order before any execution starts, then all sync executions
run at once, each appending its progress and result as it lands. Result order
in the log is therefore physical completion order and is not deterministic
across runs; any given log still replays deterministically. Async tools
naturally complete independently. No behavior may rely on physical completion
order.
Durable appends are serialized through a single internal queue per invocation:
the persist → reduce → stream ritual runs to completion for one batch of
events before the next begins, so file order, in-memory order, and stream
order are identical by construction even while executions overlap.
## 5. Storage design
@ -904,7 +913,12 @@ For one completed assistant response:
2. Determine permission requirements.
3. Apply automatic permission decisions when enabled.
4. Advance each tool independently as its permission is resolved.
5. Execute allowed sync tools using a simple sequential policy initially.
5. Record invocations for allowed tools serially in source order (sync and
async alike), then execute all allowed sync tools concurrently. There is
no concurrency cap and no per-tool serialization; tools that share state
must tolerate racing (or reject stale operations, as file edits do via
their search/replace precondition). Secondary kill-path state (the abort
registry) is scoped per tool call, never per turn.
6. Expose allowed async tool requests.
7. Suspend when any permissions or async results remain outstanding.
8. Once all calls have terminal results, build the next model request with