Chapter 44: Parallel Tool Execution

"Read-only tools can run in parallel" — read-only and concurrency-safe are not the same property, and the gap between them is where the corruption lives.


Quick Track (Master the core in 5 minutes)

  1. Batch by concurrency safety, not by read-only-ness — they are different questions
  2. Safety is a property of the invocation, not of the tool name
  3. bash is the hard case: classify the command string, and fail closed on anything unrecognized
  4. Any shell metacharacter — including a newline — disqualifies a command
  5. Concurrent results need tool_use_id pairing, or your UI attributes output to the wrong call

10-Minute Path: 44.1-44.3 -> 44.5 -> Kocoro OSS Lab


Public-source snapshot: implementation details are examples from public Kocoro origin/main at commit 4ec6772, reviewed 2026-07-27. Treat the invariant as the lesson and re-check constants against current source.

44.1 Starting with One Word That Changes Everything

The model emits four bash calls in one turn. All four are reads. Here is how the classifier actually treats them:

bash("git log --oneline -10")              parallel batch
bash("cat config.yaml")                    parallel batch
bash("git stash list")                     parallel batch
bash("git config --global core.editor")    serial, batch of one

The fourth one really is a read. git config --global core.editor with no value queries the setting; it changes nothing. So why did it get pulled out?

Because the classifier cannot prove it's a read. git config becomes a write the moment you append a value — git config --global user.email me@example.com differs from the query above by one token — and the classifier only recognizes the explicitly read-only forms (--get, --list, and their variants). Anything else in the config subcommand is refused.

That's a false negative. It cost you a little latency and nothing else. Now imagine the classifier had been generous with the config family instead: git config --global user.email x slips into a parallel batch alongside a git stash pop, and two git operations mutate shared state concurrently. The asymmetry is the whole design. Being wrong toward "serial" costs milliseconds. Being wrong toward "parallel" costs your repository.

Underneath this is a question people rarely separate: "read-only" answers the wrong question. Read-only asks does this change state? Concurrency safety asks can this run at the same time as its siblings without them interfering? Those diverge in both directions: a read that acquires an exclusive lock is read-only but not concurrency-safe, and two writes to entirely separate resources are not read-only but are perfectly safe together.

So the dispatcher should batch on the question it actually cares about.

44.2 Concurrency Safety Is a Trait of the Invocation

The public runtime partitions tool calls by a concurrency-safety check rather than a read-only flag. Tools that don't implement the concurrency trait fall back to their read-only value — so existing tools keep their previous behaviour, and adding the trait to one tool changes nothing for any other.

That fallback design is worth noticing on its own. A new classification axis should default to the old behaviour, or introducing it becomes a system-wide risk instead of a per-tool improvement.

The crucial word is invocation. file_read is safe concurrently for every argument. bash is safe for some argument strings and catastrophic for others. A per-name allowlist cannot express that, which is why the check takes the arguments, not just the tool.

44.3 Bash, and the Discipline of Failing Closed

Classifying arbitrary shell is the hard case, and the public implementation states its posture directly:

It is intentionally conservative: any shell metacharacter, any unknown leading token, or any unrecognized subcommand pattern returns false.

Three filters, all of which must pass:

No shell metacharacters. Pipes, redirects, &&, command substitution — and, importantly, newlines and carriage returns. A newline is a command separator, so a "single command" containing one is actually two commands, the second of which you never classified. This is the kind of gap that only shows up when a model formats a command across lines.

Known leading token. A strict read-only whitelist. git push, npm install, curl, rm — anything not on the list is serial, no argument. Unknown means unsafe.

Recognized subcommand shape. For a multi-purpose binary like git, the leading token isn't enough. Global options get skipped, the subcommand is checked against a safe set, and argument patterns that imply output or external modification disqualify the call.

Everything else runs in a batch of size one. Not rejected — just serial. That distinction matters: the conservative classifier never blocks work, it only declines to parallelize it. The cost of a false negative is latency; the cost of a false positive is corruption. Tune accordingly.

44.4 Concurrent Results Need Identity

There's a second-order problem that bites the moment parallelism works.

With serial execution, results arrive in call order, so a UI can pair them by position. Run four calls concurrently and they complete out of order — the git log finishes while the cat is still going. If your event stream just says "a tool finished," the UI has no way to know which card to update.

So tool lifecycle events carry a tool_use_id, and clients pair on it rather than on arrival order. The daemon advertises this on the handshake as a capability token, which is the pattern from Chapter 33: a wire-contract change that clients must detect gets a token, never a version sniff.

Notice the sequencing in the public config comment for enabling concurrent bash by default:

Phase C: Desktop now consumes tool_use_id on tool_status events, safe to enable concurrent bash batches by default.

The feature was built, the client learned to pair by id, and then the default flipped. Enabling parallelism before the consumer can attribute results produces a UI that shows the right data against the wrong call — which is worse than no parallelism, because it's wrong rather than slow.

44.5 Snapshot Evidence

ObservationSource at 4ec6772
Batching decided by concurrency safety, not read-onlypartition.go L41
Read-only fallback preserves prior behaviourpartition.go L28
Partitioning and batch executionpartition.go L51
Conservative bash classifierbash_concurrency.go L61
Git subcommand safety checkbash_concurrency.go L166
Output/modification argument patterns disqualifybash_concurrency.go L115
Default enabled after clients consume tool_use_idconfig.go L449

These describe one dated implementation, not a universal contract.

44.6 What Parallelism Costs

Concurrency is not free, and three of its costs are easy to miss.

Approval ordering. If two calls in a batch both need approval, the user sees two prompts with no guaranteed order. Approvals should be resolved before the batch executes, not raced inside it.

Result size. Four concurrent tools can return four large results simultaneously, and the per-turn aggregate cap from Chapter 36 now binds four times faster. Parallelism accelerates context pressure.

Debuggability. Serial execution gives you a clean causal trace. Concurrent execution interleaves logs from four calls, and the failure you're chasing is somewhere in the interleaving. This is the real reason to keep batches small and classification conservative — not just safety, but the ability to understand what happened afterwards.

Note also what this chapter does not claim. Chapter 32 describes an earlier admission-then-parallel-execution model; the current classification is finer-grained and per-invocation. Treat that chapter as the historical snapshot it says it is.

44.7 Common Pitfalls

Using read-only as a proxy for safe. The two properties diverge in both directions.

Classifying by tool name. bash is safe for cat and unsafe for git push. Name-level decisions cannot express that.

Forgetting newline is a metacharacter. A multi-line "single command" is multiple commands, only the first of which you inspected.

Allowlisting a multi-purpose binary wholesale. git is not one command. Neither is npm, docker, or kubectl.

Shipping parallelism before result identity. The UI attributes output to the wrong call, and users lose trust in the whole display.

Assuming a passing test proves safety. Concurrency bugs are timing-dependent; a green test means it didn't happen this time. Conservative classification is the actual defence.

Kocoro OSS Lab (10 minutes)

  1. Read IsCommandConcurrencySafe and list every way a command can be rejected. Count how many are "we don't recognize this" versus "we know this is unsafe."
  2. Look at gitSubcommandSafe and consider what a similar function would need to cover for docker or kubectl.
  3. Take your own dispatcher: does it batch on read-only or on concurrency safety? If read-only, find one tool where the two answers differ — every codebase has one.

Key Points

  1. Batch on concurrency safety, not read-only. They answer different questions and diverge in both directions.
  2. Safety belongs to the invocation. The same tool is safe with some arguments and not others.
  3. Fail closed on anything unrecognized. A false negative costs latency; a false positive costs data.
  4. A newline is a metacharacter. A multi-line command is multiple commands.
  5. Ship result identity before parallelism. Without tool_use_id pairing, concurrent output lands on the wrong card.

Next: Chapter 45 applies the same budgeting discipline to the most expensive result type of all — screenshots.

Cite this article
Zhang, Wayland (2026). Chapter 44: Parallel Tool Execution. In AI Agent Architecture: From Single Agent to Enterprise Multi-Agent Systems. https://waylandz.com/ai-agent-book-en/chapter-44-parallel-tool-execution/
@incollection{zhang2026aiagent_en_chapter-44-parallel-tool-execution,
  author = {Zhang, Wayland},
  title = {Chapter 44: Parallel Tool Execution},
  booktitle = {AI Agent Architecture: From Single Agent to Enterprise Multi-Agent Systems},
  year = {2026},
  url = {https://waylandz.com/ai-agent-book-en/chapter-44-parallel-tool-execution/}
}