Skip to content

perf(agent): concurrent read-only tool batches via capability gate (PR6) - #715

Merged
gnanam1990 merged 1 commit into
mainfrom
perf/concurrent-readonly-tool-batches
Jul 17, 2026
Merged

perf(agent): concurrent read-only tool batches via capability gate (PR6)#715
gnanam1990 merged 1 commit into
mainfrom
perf/concurrent-readonly-tool-batches

Conversation

@gnanam1990

@gnanam1990 gnanam1990 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

PR6 of the performance program: run consecutive capability-safe read-only tool calls concurrently, using the PR5 effect metadata contract merged in #705.

  • Gate: Effect == ReadOnly and ThreadSafe and auto-allowed (PermissionAllow) and no resource-key conflict with earlier calls in the same window
  • Planner: extendParallelRun + resourceKeysConflict (empty keys never conflict; ThreadSafe is the safety gate)
  • Loop: consecutive safe runs still execute via executeParallelReadBatch (≥2 calls), results consumed in original order
  • ThreadSafe=true (audited): read_file, read_minified_file, list_directory, glob, grep, skill, web_fetch
  • Still sequential: mutators, interactive tools, web_search, tool_search, lsp_navigate, anything that would prompt, unknown tools
  • Keys: glob/grep use scopedScanResourceKeysnil for ./empty (no false workspace-wide conflicts); otherwise directory:…. Same-path read_file still serializes via file: keys

Depends on #705 (already on main). Independent of turn-bench / #712.

Behavior / safety

  • Read-after-write ordering preserved: parallel windows never span a mutating call
  • Malformed JSON args fail closed → sequential
  • Permission prompts never enter the concurrent path (auto-allow only)
  • web_fetch is ThreadSafe but still permission-gated (network prompt stays sequential until auto-allowed)
  • FileTracker is mutex-guarded; concurrent distinct-path reads are safe

Test plan

  • make build
  • make lint + golangci unused/ineffassign/staticcheck on agent+tools
  • go test -race ./internal/agent/ ./internal/tools/
  • Focused: parallel safe gate, key conflicts, extendParallelRun, consecutive concurrent reads, write barrier, catalog ThreadSafe audit, scopedScanResourceKeys
  • CI green on this PR
  • Human review (fail-closed gate, ThreadSafe marks, resource-key boundaries)

Stack / ownership

Performance program: PR5 → PR6 → PR11. This is PR6 (concurrent read-only batches).

Summary by CodeRabbit

  • Performance

    • Improved parallel execution of safe, read-only tool calls.
    • Preserved ordering when calls access conflicting resources or perform changes.
    • Enabled more concurrent reads across files, directories, skills, and web content.
  • Reliability

    • Added safeguards to prevent unsafe or conflicting operations from running concurrently.
    • Improved handling of resource-specific access boundaries for search and scan operations.
  • Tests

    • Added coverage for concurrency eligibility, resource conflicts, and execution boundaries.

Upgrade the parallel tool planner from SideEffectRead probes to the PR5
CapabilitiesOf contract: EffectReadOnly + ThreadSafe + auto-allowed, with
resource-key conflict boundaries so same-path reads stay sequential.

Mark audited pure reads ThreadSafe (read_file, read_minified_file,
list_directory, glob, grep, skill, web_fetch). Glob/grep use scopedScan
directory keys (nil for workspace-wide "." so scans do not false-conflict).

Tests cover the capability gate, key conflicts, extendParallelRun windows,
and catalog ThreadSafe audit for concurrent-safe reads.
@gnanam1990
gnanam1990 marked this pull request as ready for review July 17, 2026 12:05
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Changes

Parallel read-ahead execution now relies on capability metadata, thread-safety declarations, decoded permissions, and resource-key conflicts. Tool declarations identify safe concurrent reads, while the agent computes batch boundaries that preserve ordering and avoid conflicting resources.

Parallel read scheduling

Layer / File(s) Summary
Read capability and resource-key declarations
internal/tools/resource_keys.go, internal/tools/*
Scoped scan tools derive normalized directory keys, and supported read tools are marked thread-safe with appropriate resource-key functions.
Capability-aware parallel run planning
internal/agent/parallel_tools.go, internal/agent/loop.go
Eligibility requires read-only, thread-safe capabilities and auto-allowed permissions; resource conflicts and intercepted tools terminate parallel runs.
Parallel scheduling test coverage
internal/agent/parallel_tools_test.go, internal/tools/capabilities_test.go
Tests cover capability classification, non-thread-safe reads, resource-key conflicts, and parallel run boundaries.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AgentLoop
  participant ParallelPlanner
  participant ToolRegistry
  participant ReadTools
  AgentLoop->>ParallelPlanner: Select consecutive tool calls
  ParallelPlanner->>ToolRegistry: Read capabilities and decode arguments
  ToolRegistry-->>ParallelPlanner: Thread-safety and resource keys
  ParallelPlanner->>ReadTools: Execute non-conflicting read calls concurrently
  ReadTools-->>AgentLoop: Read results
Loading

Possibly related PRs

  • Gitlawb/zero#47: Introduced core read-only tools and their registry capability contracts.
  • Gitlawb/zero#506: Modified agent dispatch and parallel read-ahead grouping.
  • Gitlawb/zero#705: Added capability metadata and resource-key scheduling foundations.

Suggested reviewers: vasanthdev2004, anandh8x

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: capability-gated concurrent batching for read-only agent tools.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/concurrent-readonly-tool-batches

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: 7bce9ad9ff6f
Changed files (12): internal/agent/loop.go, internal/agent/parallel_tools.go, internal/agent/parallel_tools_test.go, internal/tools/capabilities_test.go, internal/tools/glob.go, internal/tools/grep.go, internal/tools/list_directory.go, internal/tools/read_file.go, internal/tools/read_minified_file.go, internal/tools/resource_keys.go, internal/tools/skill.go, internal/tools/web_fetch.go

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@anandh8x anandh8x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 7bce9ad

Substantive review of PR #715 (gnanam's PR6 — concurrent read-only tool batches via capability gate). Scope is just the PR6 commit (12 files, +285/-27); the PR's stacked diff is much larger because the branch is based on a pre-#704 / pre-#606 / pre-#712 main, but those three changes are already on main. Verdict: approve, ready to merge.

What's good

  • The capability gate is the right contract. parallelSafeToolCall checks Effect == EffectReadOnly + ThreadSafe == true + effectivePermission == PermissionAllow + no resource-key conflict with earlier calls in the window. That's the four conditions for safe concurrency and not one more. The fail-closed default (Caps == Unknown → sequential) is the right call — a tool that hasn't been audited can't claim thread-safety by silence.
  • Same-path reads stay sequential. resourceKeysConflict with non-empty key sets sharing any element is the right conflict primitive. TestExtendParallelRunResourceKeyBoundary proves keyed_read on [a, b, a] extends from 0 to 2 (stop before the second file:a). Keyless tools don't false-conflict because ThreadSafe is the eligibility gate and keys only refine same-resource collisions. That distinction matters: glob and grep use scopedScanResourceKeys which returns nil for workspace-wide scans so a missing path arg doesn't force every concurrent scan to serialize.
  • Read-after-write ordering is preserved. extendParallelRun runs per-consecutive-run, not per-batch. A mutating call always breaks the run. TestRunParallelReadsNeverSpanMutatingCall proves this with a shared log: the write starts only after the first read batch's max-end, and the second read batch starts only after the write's end. This is the invariant that makes the optimization safe.
  • Results stay in original call order. The precomputedToolResult slice is indexed index-start, so results[0] is the call at start, regardless of completion order. TestRunExecutesConsecutiveReadsConcurrently asserts both OnToolResult ordering and provider-message ordering — the two places ordering can drift.
  • The callbackMutex is the subtle bit that's right. A sandbox preflight can demand a permission prompt even for an auto-allowed read, and OnPermission event handlers append to shared session-recording state without their own locking. Without the mutex, two batched reads under a granted extra-root would race. The defer-after-lock pattern around both OnPermissionRequest and OnPermission is the right shape.
  • maxParallelReadTools = 8 is a reasonable bound. A turn rarely needs more than 3-4 parallel reads; 8 is a backstop against pathological model outputs without throttling normal cases.
  • decodeCallArgs fail-closed prevents malformed JSON from entering the parallel path. A model that emits broken Arguments falls back to the serial loop rather than panicking inside the goroutine. Good defensive default.
  • NormalizeResourcePath is pure. No EvalSymlinks / Stat so it cannot cause side effects or panic on missing paths. Rejects URL-shaped values (those use endpoint: keys). Case-insensitive on Windows, separator-normalized. The joinUnderResourceCwd helper for apply_patch is the right fix for "same file under different cwd" key collisions.
  • The 7 read tools marked ThreadSafe are the right set. read_file, read_minified_file, list_directory, glob, grep, skill, web_fetch are all pure reads with no shared mutable state (FileTracker is mutex-guarded, scans are read-only filesystem walks, web_fetch is a network GET). No tool that was previously safe-but-unmarked would now be excluded; the gate is strictly additive.
  • Tests cover the right axes. The five new tests gate the capability classification (TestParallelSafeToolCall), the conflict primitive (TestResourceKeysConflict), the run-extension logic with same-path and keyless cases (TestExtendParallelRunResourceKeyBoundary), the end-to-end concurrency with ordering (TestRunExecutesConsecutiveReadsConcurrently), and the read-after-write ordering (TestRunParallelReadsNeverSpanMutatingCall). Plus the catalog ThreadSafe audit in capabilities_test.go. No test overlaps another; the contract is the union of their assertions.

Minor notes (not blockers)

  • The EffectiveEnd() method on keyedProbeTool and the probeTool wrapper in the test file are a bit dense, but they're test scaffolding and the tests pass. The pattern is reusable for future capability-classification tests.
  • The OnPermissionRequest and OnPermission mutex wrap could be slightly more efficient if the callbacks were already mutex-safe (e.g. some front-ends serialize their own), but the defer-after-lock overhead is one mutex acquire per call — negligible against a 60ms read.

One operational note for kevin

The PR's stacked diff is large (2000+ lines) because the branch is based on a pre-#704 / pre-#606 / pre-#712 main. The actual PR6 commit is +285/-27 across 12 files; the rest is aimlapi and other unmerged branches stacked in. If kevin merges via the GitHub button it'll be a rebase-and-merge onto the current main, which should land cleanly. If a merge commit is used, the headline diff will look bigger than the PR6 scope.

Verdict

Approve. This is the right shape for PR6. The gate is the right contract, the conflict primitive is the right granularity, the read-after-write ordering is preserved, results stay in order, and the tests gate every claim. The branch is stale (stacked on pre-#704 / pre-#606 / pre-#712), but the actual PR6 commit is clean. Ready to merge once kevin signs off.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve. I read each of the seven ThreadSafe=true Runs against the actual implementations: read_file/read_minified_file only touch the mutex-guarded FileTracker (same-path calls serialize via file: keys anyway), glob/grep/list_directory are independent FS walks with no shared cache or index, skill is a pure disk read of the skill file (no execution, no registry state), and web_fetch shallow-copies the shared http.Client per call with a fresh Transport (stdlib client is concurrent-safe).

The gate is fail-closed — unknown tools, malformed args, non-ReadOnly, non-ThreadSafe, and non-auto-allowed all stay sequential, and normalizeCapabilities force-clears ThreadSafe for any non-ReadOnly effect so a mis-marked mutator can't slip through later. go build/vet and go test -race ./internal/agent/ ./internal/tools/ pass clean, read-after-write ordering holds (a mutator breaks the run, so a read after a write starts a fresh window), and the scope is tight — only the 7 marks + the gate/planner + tests, no unrelated tool semantics touched.

@gnanam1990
gnanam1990 merged commit 31d45d5 into main Jul 17, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants