feat(tools): concurrent read-only tool execution with batch partitioning - #2423
henrypark133 wants to merge 12 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces concurrency-aware batching for tool execution within a single LLM turn by adding a per-tool concurrency classification and partitioning tool calls into parallel (read-only/concurrent-safe) vs serial (mutating) batches.
Changes:
- Add
Tool::is_concurrent_safe(params)(defaultfalse) and implement it across many built-in tools (incl. parameter-dependent behavior forhttp). - Add
src/agent/batch.rswith batching/partitioning logic and update the chat dispatcher to execute concurrent-safe batches viaJoinSetwhile preserving serial order for mutating tools. - Add
AGENT_MAX_CONCURRENT_TOOLS(default 10) and extensive new tests for batching + classifications.
Reviewed changes
Copilot reviewed 34 out of 34 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/concurrent_tool_execution.rs | Adds integration tests for batching behavior, tool result mapping, and rate limiter behavior. |
| src/tools/tool.rs | Adds is_concurrent_safe() to the Tool trait and unit tests for the new API. |
| src/tools/builtin/tool_info.rs | Marks tool_info as concurrent-safe. |
| src/tools/builtin/time.rs | Marks time as concurrent-safe. |
| src/tools/builtin/system.rs | Marks system_tools_list/system_version as concurrent-safe. |
| src/tools/builtin/skill_tools.rs | Marks skill_list/skill_search as concurrent-safe. |
| src/tools/builtin/secrets_tools.rs | Marks secret_list as concurrent-safe. |
| src/tools/builtin/routine.rs | Marks routine_list/routine_history as concurrent-safe. |
| src/tools/builtin/memory.rs | Marks read-only memory tools as concurrent-safe. |
| src/tools/builtin/json.rs | Marks json as concurrent-safe. |
| src/tools/builtin/job.rs | Marks job read-only/status tools as concurrent-safe. |
| src/tools/builtin/image_analyze.rs | Marks image_analyze as concurrent-safe. |
| src/tools/builtin/http.rs | Adds parameter-dependent is_concurrent_safe (GET safe; others serial) + tests. |
| src/tools/builtin/grep_tool.rs | Marks grep as concurrent-safe. |
| src/tools/builtin/glob_tool.rs | Marks glob as concurrent-safe. |
| src/tools/builtin/file.rs | Marks read_file/list_dir as concurrent-safe. |
| src/tools/builtin/extension_tools.rs | Marks listing/search/info extension tools as concurrent-safe. |
| src/tools/builtin/echo.rs | Marks echo as concurrent-safe. |
| src/settings.rs | Removes channels.cli_mode from persisted settings. |
| src/config/profile.rs | Removes the deployment profile system (module deleted). |
| src/config/mod.rs | Stops applying deployment profiles during config resolution; adjusts bootstrap settings loading. |
| src/config/channels.rs | Resolves cli_mode from env only (CLI_MODE). |
| src/config/agent.rs | Adds max_concurrent_tools (env: AGENT_MAX_CONCURRENT_TOOLS). |
| src/bridge/router.rs | Updates test config structs to include max_concurrent_tools. |
| src/agent/thread_ops.rs | Updates test config structs to include max_concurrent_tools. |
| src/agent/scheduler.rs | Updates test config structs to include max_concurrent_tools. |
| src/agent/mod.rs | Exposes new agent::batch module. |
| src/agent/dispatcher.rs | Implements batched execution logic in the chat tool-call execution path. |
| src/agent/batch.rs | New batching/partitioning module with unit tests. |
| profiles/server.toml | Deletes built-in profile file. |
| profiles/server-multitenant.toml | Deletes built-in profile file. |
| profiles/local.toml | Deletes built-in profile file. |
| profiles/local-sandbox.toml | Deletes built-in profile file. |
| docs/plans/2026-04-12-concurrent-readonly-tools.md | Adds design/plan doc for the concurrency batching approach. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Code Review
This pull request implements concurrent execution for read-only tools by introducing a concurrency safety classification to the Tool trait and a batch partitioning system in the agent dispatcher. It also removes the deployment profile system and refactors the cli_mode configuration. Feedback focuses on high-severity regressions in configuration flexibility and backward compatibility, specifically noting that the removal of the profile system and the cli_mode schema field breaks existing configurations and violates the established configuration hierarchy.
I am having trouble creating individual review comments. Click here to see my feedback.
src/config/channels.rs (372)
This change introduces a regression in configuration flexibility. By switching from db_first_optional_string to optional_env, the cli_mode setting can now only be configured via environment variables, breaking the standard hierarchy (Environment > Database > TOML). When resolving environment variables, ensure you use specialized helper functions like parse_option_env<T> to maintain consistency across configuration files.
let cli_mode = db_first_optional_string(&cs.cli_mode, "CLI_MODE")?.unwrap_or_default();
References
- Use specialized helper functions like parse_option_env when resolving environment variables into Option fields to minimize boilerplate and maintain consistency.
src/config/mod.rs (247)
The removal of the profile system (IRONCLAW_PROFILE) is a significant architectural change. This breaks backward compatibility for existing deployments that rely on built-in profiles. Repository guidelines prioritize backward compatibility by retaining established defaults and configuration options.
References
- Prioritize backward compatibility for existing tests and configurations by retaining default values and established options.
src/settings.rs (421-423)
Removing cli_mode from ChannelSettings breaks the ability to configure the terminal interface via the config.toml file. This violates the repository's policy of maintaining backward compatibility for established configuration schemas.
References
- Prioritize backward compatibility for existing configurations by retaining established defaults and schema fields.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
henrypark133
left a comment
There was a problem hiding this comment.
Review: Concurrent read-only tool execution (Risk: Medium)
Clean design with is_concurrent_safe() on the Tool trait, conservative default (false), and well-structured batch partitioning. The parameter-dependent HTTP classification and config escape hatch (max_concurrent_tools) are good patterns.
Positives:
- Batch partitioning algorithm is straightforward and well-tested (18 unit tests in batch.rs)
.max(1)clamping formax_concurrent=0added in fix commit- Design doc included
- Conservative default (all tools serial unless opted in)
Critical: CI is RED — Clippy lifetime error [Blocking]
File: tests/concurrent_tool_execution.rs:363
error[E0597]: `tools` does not live long enough
A reference &tools[pf_idx] is borrowed from a stack-local Vec<Box<dyn Tool>> and captured into JoinSet::spawn() which requires 'static. All 3 Clippy matrix legs fail + Code Style gate.
Fix: Wrap tools in Arc<Vec<Box<dyn Tool>>> or Vec<Arc<dyn Tool>> so ownership can be moved into spawned tasks.
Critical: JoinSet panic-recovery has no caller-level test [Testing]
File: src/agent/dispatcher.rs:1039-1062
The dispatcher fills panicked JoinSet task slots with fabricated error results. No test verifies: (a) the batch continues after a panic, (b) remaining tools complete, (c) the error is correctly attributed to the panicking tool's tool_call_id. Per testing rules ("test through the caller"), this side-effect-gating path needs coverage.
Concerning: HEAD and OPTIONS HTTP methods unclassified [Logic/Testing]
File: src/tools/builtin/http.rs:1043-1046
Only GET is classified as concurrent-safe. HEAD and OPTIONS are also idempotent/read-only but fall through to false. No test asserts this classification either way — the decision should be documented or expanded.
Concerning: Timing test has flaky threshold [Testing]
File: tests/concurrent_tool_execution.rs:243-246
3 tools × 50ms asserts completion in <150ms. On loaded CI runners, tokio scheduling overhead can push this over. The test also uses futures::future::join_all rather than JoinSet::spawn (what the dispatcher actually uses).
Fix: Use a structural test (shared barrier or Mutex<Vec<Instant>>) instead of timing assertions.
Concerning: No max_concurrent=1 regression test [Testing]
File: src/agent/dispatcher.rs:923
max_concurrent_tools=1 forces all tools to single-item Concurrent batches (the regression path against old sequential behavior). No end-to-end test verifies this produces correct ordered results.
Concerning: Revert of profiles (#2203) bundled in this PR [Architecture]
The first commit reverts the profiles feature entirely. Unrelated to concurrent tool execution — should arguably be a separate PR to keep review scope clean.
Convention notes:
- Rate limiter
check()(non-recording) path has no test - Classification tests (
builtin_echo_is_concurrent_safeetc.) are helper-level only, not caller-level per testing rules - Rate limiter concurrency test asserts exact 5/5 split that depends on scheduling order
Verdict: Request Changes — CI is red (blocking), JoinSet panic-recovery path needs test coverage, and the profiles revert should be separated.
Cross-PR note: This PR and #2425 both modify src/agent/dispatcher.rs. Merge #2425 first (smaller, CI green), then rebase this one.
Review fixes (28030b8)All actionable items from the review have been addressed:
Not actionable: Profiles revert + CachedSettingsStore changes are merge artifacts from staging, not from this feature. Test count: 16 integration + 19 batch unit + 9 trait unit + 9 http unit = 53 tests, all passing. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
henrypark133
left a comment
There was a problem hiding this comment.
Review: Concurrent read-only tool execution with batch partitioning (Risk: Medium)
Clean design with is_concurrent_safe() on the Tool trait, conservative default (false), and well-structured batch partitioning. The plan document is thorough. Several wiring and compilation concerns.
Critical: Verify is_concurrent_safe() exists on the Tool trait
File: src/tools/tool.rs
One reviewer found no is_concurrent_safe method on the Tool trait, which would make the test file (tests/concurrent_tool_execution.rs) fail to compile when it calls tool.is_concurrent_safe() on EchoTool and HttpTool. Verify the method was added as a default trait method:
fn is_concurrent_safe(&self, _params: &serde_json::Value) -> bool { false }Critical: Verify pub mod batch is declared in src/agent/mod.rs
File: src/agent/mod.rs
Without the module declaration, src/agent/batch.rs is dead code and partition_tool_calls is unreachable from the dispatcher.
Critical: Verify batch partitioning is wired into dispatcher Phase 2
File: src/agent/dispatcher.rs (~line 915)
The PR should replace the existing all-or-nothing parallel logic with the new batch partitioning path that classifies tools via is_concurrent_safe() and calls partition_tool_calls(). If the old runnable.len() <= 1 branch remains, the feature has no production effect.
Concerning: HTTP GET with save_to correctly classified as not concurrent-safe
File: src/tools/builtin/http.rs
Patch 6 adds if params.get("save_to").is_some_and(|v| !v.is_null()) { return false; } — good catch. A GET request that writes response bytes to disk races with other file-writing tools.
Concerning: HEAD and OPTIONS correctly added as concurrent-safe
File: src/tools/builtin/http.rs
Patch 4 adds HEAD and OPTIONS alongside GET. These are idempotent read-only methods — correct classification.
Concerning: Single-item Concurrent batch optimization removed in Patch 6
File: src/agent/dispatcher.rs
Patch 6 removes the inline optimization for single-item Concurrent batches, routing all batches through JoinSet for consistent panic recovery semantics. This is the right call — behavioral consistency outweighs the minor overhead of a JoinSet for a single task.
Minor: max_concurrent clamped to min 1 (Patch 3)
partition_tool_calls now handles max_concurrent = 0 by clamping to 1 instead of producing surprising behavior. Good defensive coding with test.
Minor: Timing test threshold widened to 150ms (Patch 3)
Reduces CI flakiness. Patch 4 replaces timing-based assertion with barrier-based structural concurrency test — much better approach for proving parallelism.
Minor: Plan document is comprehensive
docs/plans/2026-04-12-concurrent-readonly-tools.md covers classification of all 54+ tools, migration risks, verification strategy, and file-level change plan. Good reference for future maintainers.
Minor: Profile revert (Patch 1) is shared with PR #2429
The first patch reverts IRONCLAW_PROFILE. If both PRs land, there will be a merge conflict. Coordinate merge order or rebase one onto the other.
Positives:
- Conservative
default falseonis_concurrent_safe()— new tools must opt in - Parameter-dependent classification for HTTP (GET vs POST/PUT/DELETE) is well-designed
save_tocheck on HTTP prevents filesystem race conditions- Batch partitioning algorithm is correct with 20+ unit tests covering edge cases
- Integration tests use barrier-based structural concurrency (not flaky timing)
- Panic recovery test verifies error attribution to correct slot
- Rate limiter concurrent contention test validates the write lock ensures correctness
- Tool call ID integrity preserved through partitioning
🤖 Generated with Claude Code
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
ReviewSolid mechanical change with good helper-level tests for the partitioner. Three ship-blockers; otherwise a nice win. Ship blockers1. 2. No caller-level integration test. Per
Add 3. v2 coverage. The v2 engine ( Correctness
ObservabilitySpawned task spans become siblings of the turn span, not children. Wrap Tests — please also add Playwright
Nits
|
serrrfirat
left a comment
There was a problem hiding this comment.
Paranoid Security Review — NEEDS CHANGES
PR: #2423 — Concurrent read-only tool execution with batch partitioning
Reviewer context: Automated deep security review
Critical
Contradictory test will fail CI. test_is_concurrent_safe_default_is_false in src/tools/tool.rs asserts !EchoTool.is_concurrent_safe(...), but this same PR adds is_concurrent_safe() -> true to EchoTool in echo.rs. The integration test builtin_echo_is_concurrent_safe asserts the opposite. Fix: use a stub tool that genuinely relies on the trait default.
Medium
image_analyze marked concurrent-safe makes outbound LLM API calls. ImageAnalyzeTool::is_concurrent_safe returns true, but execute() sends HTTP POST to a vision API. While it doesn't mutate local state, concurrent calls amplify external API cost and could hit provider rate limits. The "read-only" characterization understates the risk.
No upper bound on max_concurrent_tools config. AGENT_MAX_CONCURRENT_TOOLS is parsed as usize with no clamp. A malicious config could set this arbitrarily high. Recommend clamping to a sane maximum (e.g., 50).
TOCTOU gap between classification and execution. Tool lookup for is_concurrent_safe() classification and tool lookup for execution are separate operations. A hot-swapped tool (WASM reinstall) between these lookups could invalidate the concurrency classification.
Low
cached_tool_permissionsremoval is an unrelated behavioral change — should be a separate commit.- No batch size limit independent of
max_concurrent_tools— unbounded tool calls per turn.
Positive
- WASM and MCP tools correctly default to serial (
false). - Safety pipeline preserved — both paths go through
execute_tool_with_safety. - Approval gates unaffected — preflight runs sequentially before any concurrent execution.
- No cross-user context leakage —
JobContextcloned per task from same user session. - No new
.unwrap()/.expect()in production code.
Summary
The partitioning design is sound. Ship-blocker is the contradictory test. After fixing that, address the config upper bound and TOCTOU documentation.
Paranoid Architect Review — Additional Findings1. High Severity —
|
Addressing review — @ilblackdragonAll items addressed in latest push. Here's the disposition: Ship blockers1. 2. Caller-level integration tests added. 4 new tests in
3. v2 parity documented. v2 engine runs all actions in parallel unconditionally via Correctness — all documented
Observability
Tests
Nits
|
Addressing review — @serrrfiratBoth the security review and architect review addressed in latest push. Security ReviewCritical: "Contradictory test will fail CI" — This is actually a false positive. The test Medium: Medium: No upper bound on Medium: TOCTOU gap — Documented as a known limitation in the classification loop. The window is sub-millisecond and WASM reinstalls during a single batch are extremely unlikely. Architect ReviewHigh: Medium: Sequential await for classification — Optimized. New Medium: Execution path asymmetry — Comment added at serial branch. Both paths call |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 28 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
When the LLM returns multiple tool calls, the dispatcher now classifies each tool via is_concurrent_safe() and partitions them into batches: adjacent concurrent-safe tools run in parallel via JoinSet, while mutating tools get their own serial batch preserving call order. - Add is_concurrent_safe() to Tool trait (default: false, conservative) - Add batch partitioning module (src/agent/batch.rs) - Integrate into ChatDelegate dispatcher (Phase 2 execution) - Classify all 54+ built-in tools (24 concurrent-safe, rest serial) - Parameter-dependent classification for http (GET=safe, POST/PUT/DELETE=serial) - Add max_concurrent_tools config (default: 10, per-turn scope) - 46 tests: unit (trait, partitioning, http), integration (timing, ordering, rate limiter) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Clamp max_concurrent to min 1 (guards against AGENT_MAX_CONCURRENT_TOOLS=0) - Replace join_all with JoinSet in mixed_batch test (validates completion-order independence) - Widen timing threshold to 150ms to reduce CI flakiness - Replace empty-loop rate limiter test with meaningful classification check Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ONS, structural concurrency test - Fix JoinSet lifetime error: use Arc<dyn Tool> instead of &dyn Tool borrows - Add panic-recovery test: verify batch continues after tool panic, error attributed to correct slot - Classify HEAD and OPTIONS as concurrent-safe (idempotent read-only) - Replace flaky timing test with barrier-based structural concurrency test - Add max_concurrent=1 regression test - Narrow rate limiter test comment (not all concurrent-safe tools lack rate limits) - Relax rate limiter concurrency assertion (allowed <= 5, not == 5) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…limiter assertion - HTTP GET with save_to is not concurrent-safe (filesystem write race) - Remove single-item Concurrent batch inline optimization — all batches now go through JoinSet for consistent panic recovery semantics - Revert rate limiter assertion to == 5 (write lock makes count deterministic) - Add tests for GET+save_to, HEAD, OPTIONS classifications Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…omment Remove docs/plans/2026-04-12-concurrent-readonly-tools.md from the PR (internal design doc, not needed in repo). Delete misleading comment claiming SettingsStore is wrapped in CachedSettingsStore — no such wrapper exists; store.get_all_settings() hits DB directly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ocs, tests Address all review comments from @ilblackdragon and @serrrfirat: Ship-blockers: - Restore per-turn cached_tool_permissions (stopgap until #2425) - Add caller-level integration tests through execute_tool_with_safety - Document v2 engine parity (runs all actions in parallel, no is_concurrent_safe classification) Observability: - Add .instrument(Span::current()) to JoinSet spawned tasks Optimization: - Add ToolRegistry::classify_concurrent_safety() batch API — single read lock acquisition instead of N sequential lookups Documentation comments: - Execution path symmetry (serial/concurrent both use same pipeline) - Status-update interleaving (nondeterministic for concurrent batches) - TOCTOU gap between classification and execution - JoinSet drop behavior (pre-existing, in-flight I/O not rollbackable) - DB pool + concurrency interaction - HTTP GET-with-body edge case (RFC 9110) - WASM/MCP tools default to false, no opt-in mechanism - image_analyze rationale (no local state mutation) Other: - Warn on max_concurrent_tools=0 before clamping to 1 - Rename confusing test EchoTool to MinimalTestTool - Add Playwright E2E tests for concurrent tool execution - Add mock LLM patterns for multi-tool concurrent triggers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The "concurrent 3 echo tools" trigger was matched by the earlier "echo (.+)" pattern in mock_llm.py, causing a single echo tool call instead of the intended 3-call concurrent batch. Rename triggers to "run 3 concurrent readonly tools" and "run mixed batch with approval gate" to avoid the collision. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update is_concurrent_safe doc to mention max_concurrent_tools cap can split adjacent safe tools into multiple JoinSet batches - Fix E2E test to check history.pending_gate (top-level field) instead of turn.pending_approval for approval detection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
image_analyze sends HTTP POST to a vision API — concurrent calls amplify external API cost and can hit provider rate limits. Rate limiting alone doesn't prevent cost spikes from batch parallelism. Addresses serrrfirat's security review on #2423. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add operator guidance about coordinating max_concurrent_tools with the DB connection pool size to prevent exhaustion under concurrent DB-heavy tool batches. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addressing security review feedback (@serrrfirat)Thanks for the thorough review. Here's the resolution for each item: Critical: Contradictory testNot a real issue — the test Medium:
|
28d482f to
1a9e269
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 29 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ot parallelism The E2E test checks that all 3 tool call results appear in history, not that they ran in parallel. Parallelism is verified by the Rust-level barrier-based structural concurrency test. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| // coordinated with DB connection pool size. A batch of | ||
| // DB-heavy tools can exhaust the pool. In deployment, keep | ||
| // `max_concurrent_tools <= pool_size / 2`. | ||
| let mut join_set = JoinSet::new(); |
There was a problem hiding this comment.
Medium Severity — DB connection pool exhaustion risk is documented but not enforced
The .env.example comment warns to "keep <= DB pool size / 2" but there is no runtime coordination between max_concurrent_tools and the actual pool size. An operator setting AGENT_MAX_CONCURRENT_TOOLS=50 with a default pool of 20 connections will exhaust the pool when multiple DB-heavy tools (memory_search, job_status, etc.) run in a single batch.
Additionally, max_concurrent_tools is per-turn, not global. Two concurrent users can each spawn their own batch, resulting in 2 × max_concurrent_tools parallel DB consumers.
Suggestion: Add a startup validation that warns when max_concurrent_tools > pool_size / 2. Alternatively, share a global semaphore with the DB pool to enforce the limit at runtime.
Paranoid PR Review
| max_llm_concurrent_per_user: parse_option_env("TENANT_MAX_LLM_CONCURRENT")?, | ||
| max_jobs_concurrent_per_user: parse_option_env("TENANT_MAX_JOBS_CONCURRENT")?, | ||
| engine_v2: parse_bool_env("ENGINE_V2", false)?, | ||
| max_concurrent_tools: parse_option_env::<usize>("AGENT_MAX_CONCURRENT_TOOLS")? |
There was a problem hiding this comment.
Medium Severity — max_concurrent_tools is per-turn, not global
Each ChatDelegate reads max_concurrent_tools independently. Two concurrent turns (from different users or the same user with parallel threads from PR #2429) can each spawn up to max_concurrent_tools parallel tasks, for a total of N_turns × max_concurrent_tools concurrent tool executions.
With PR #2429's parallel thread handling (up to 20 threads), the worst case is 20 × 10 = 200 concurrent tool executions system-wide.
Suggestion: Document that this is a per-turn limit and that operators should account for MAX_PARALLEL_THREADS × AGENT_MAX_CONCURRENT_TOOLS as the system-wide upper bound. Consider adding a global tool execution semaphore if resource contention becomes an issue.
Paranoid PR Review
serrrfirat
left a comment
There was a problem hiding this comment.
Paranoid Architect Review — APPROVED ✅
v2 Engine Impact: Not related — v2 runs all actions in parallel unconditionally via its own mechanism; divergence is documented
Well-designed PR. The batch partitioning algorithm is correct (verified against tests), tool_call_id mapping is preserved through JoinSet via pf_idx, and panic recovery properly fills error slots for failed tasks. All 24 tools classified as concurrent-safe were audited — all are genuinely read-only with no local side effects.
Strengths:
- Conservative default (
is_concurrent_safe() = false) — new/WASM/MCP tools are serial unless explicitly opted in - HTTP tool's parameter-dependent classification (GET=safe, POST/PUT/DELETE=serial) is well-thought-out
- Barrier-based structural concurrency test (would deadlock if serial) is a sound testing technique
- V1/V2 divergence clearly documented in module doc
2 inline comments posted (Medium):
- DB pool exhaustion —
max_concurrent_toolsis not coordinated with pool size. A startup warning would be safer. - Per-turn not global — two concurrent turns can each spawn full batches. With PR #2429, worst case is
20 threads × 10 tools = 200concurrent tasks.
Additional notes (non-blocking):
- Rate limiting is not enforced in the chat dispatcher path (pre-existing, not introduced by this PR) — concurrent execution amplifies the gap
- No caller-level test through
ChatDelegate.execute_tool_callswith a mixed batch (per "Test Through the Caller" rule)
These are awareness items for operators, not merge blockers.
Summary
is_concurrent_safe()to theTooltrait (defaultfalse, conservative). When the LLM returns multiple tool calls, the dispatcher now partitions them into batches: adjacent concurrent-safe tools run in parallel viaJoinSet, mutating tools run serially preserving call order.max_concurrent_toolsconfig (AGENT_MAX_CONCURRENT_TOOLS, default 10, per-turn scope) and batch partitioning module (src/agent/batch.rs).Test Plan
is_concurrent_safetrait (default, override, parameter-dependent GET/POST/PUT/DELETE)cargo fmt,cargo clippy --all --tests— zero warnings🤖 Generated with Claude Code