Skip to content

feat: batched call_tools() for parallel upstream calls in the code-execution sandbox - #989

Merged
github-actions[bot] merged 10 commits into
mainfrom
096-batched-call-tools
Aug 14, 2026
Merged

feat: batched call_tools() for parallel upstream calls in the code-execution sandbox#989
github-actions[bot] merged 10 commits into
mainfrom
096-batched-call-tools

Conversation

@Dumbris

@Dumbris Dumbris commented Aug 14, 2026

Copy link
Copy Markdown
Member

Implements spec 096 (specs/096-batched-call-tools/): a call_tools(requests, options?) host primitive in the code-execution JS sandbox that runs independent upstream calls in parallel.

Related #987

Stacked PR: base is fix/issues-981-985 (PR #988). Merge #988 first; GitHub will retarget this PR to main automatically.

What it does

var slots = call_tools([
  {server: "github", tool: "get_pull_request", args: {owner: "o", repo: "r", pullNumber: 1}},
  {server: "github", tool: "get_pull_request", args: {owner: "o", repo: "r", pullNumber: 2}}
], {max_parallel: 8});
// slots[i] carries the same {ok,result}|{ok,error} envelope call_tool returns, in input order

N independent calls cost ~the slowest call instead of the sum. One failing element never poisons the batch — every slot resolves with a per-slot envelope.

Design (reviewed plan is binding — specs/096-batched-call-tools/plan.md)

  • goja safety: workers produce plain Go values only (dispatch + the [Bug]: call_tool() result exposes Go struct field names (Content/Text) instead of JSON names (content/text) in the JS sandbox #981 JSON round-trip normalization); the single vm.ToValue happens on the script goroutine after an unconditional WaitGroup join.
  • Parity: per-element checks in lone-call order (budget → allow-list → agent-scope → permission-tier), identical error codes (now promoted to constants); pre-dispatch failures consume no budget and produce exactly the records a lone call produces; quarantine surfaces exactly as it does for call_tool.
  • Determinism: pre-dispatch pass runs on the script goroutine in input order — concurrency can never oversubscribe max_tool_calls; every dispatched element yields exactly one ToolCallRecord, including under cancellation.
  • Concurrency: prefilled closed channel + min(max_parallel, dispatchable) workers; Spec-093 per-server admission applies unchanged inside managed.Client.CallTool (queue or shed per that server's config — never bypassed).
  • Cancellation: workers run under the execution's timeout context (new ExecutionContext.ctx); lone call_tool behavior untouched.
  • Bounds: batch cap 100; max_parallel range 1–32; malformed calls return a single INVALID_ARGS envelope naming the first offending element — never a throw.

Config

New code_execution_max_parallel (int, default 8, range 1–32); precedence: per-batch override > config > 8. Full wiring: validation, post-load defaulting, swagger regen, frontend settings field, 7 docs files. Also fixes two pre-existing hot-reload breaks found during research: DetectConfigChanges had no clause for any CodeExecution* field (edits were silently swallowed), and exec defaults were resolved from the construction-time config snapshot instead of currentConfig(). code_execution_pool_size is now honestly reported as restart-required (the pool is sized once at startup).

Review

Spec and plan each went through multi-round cross-model review (opencode gpt-5.6-sol; Codex is out of credits) — 11 spec findings and 7 plan findings resolved before implementation. The implementation diff went through a 9-agent adversarial review (3 lenses × verify-per-finding): 2 confirmed minors, both fixed (pool_size restart honesty, settings help text).

Testing

  • TDD throughout; red observed first for every task; two invariants mutation-tested (budget accounting, parallelism bound).
  • go test -race ./internal/... green (incl. cancellation tests under -race), server-edition green, lint v2 (CI config) 0 issues, e2e 65/65.
  • SC-001 shape pinned in tests: 10-element batch < 35% of serial wall-clock against a latency stub; concurrency high-water mark == bound.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 14, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 51ee14b
Status: ✅  Deploy successful!
Preview URL: https://4f4f9765.mcpproxy-docs.pages.dev
Branch Preview URL: https://096-batched-call-tools.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 91.96787% with 20 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/jsruntime/runtime.go 90.65% 14 Missing and 6 partials ⚠️

📢 Thoughts on this report? Let us know!

@Dumbris
Dumbris changed the base branch from fix/issues-981-985 to main August 14, 2026 18:27
Dumbris added 10 commits August 14, 2026 21:28
…, cancellation, parity-as-today semantics

Related #987

Resolves all 11 cross-model review findings: execution-context cancellation
for batch workers, parity defined as call_tool's current enforcement (gate
expansion out of scope), full max_parallel definition
(code_execution_max_parallel, default 8, range 1-32, per-call override),
input-order pre-dispatch checks so budget cannot race, envelope-based
malformed-call rejection, batch cap 100, timeout semantics reconciled with
SC-003, dropped the incorrect ES5.1 claim, stub-based reproducible SCs.
… quickstart

Related #987

Phase 0/1 artifacts. Key design decisions from code research: workers produce
plain Go values with all goja conversion on the script goroutine after join;
upstreamToolCaller is already concurrency-safe while ec.ToolCalls stays
script-thread-only; Spec-093 admission applies automatically inside
managed.Client.CallTool (queue-vs-shed corrected in the spec); execution ctx
threaded via a new ExecutionContext field so workers cancel at the deadline;
7-point config wiring for code_execution_max_parallel plus two pre-existing
hot-reload breaks (DetectConfigChanges clause, currentConfig() read) fixed as
part of FR-004.
…n, one-record-per-dispatch, prefilled queue, no REST max_parallel

Related #987
…ty; drop stale execution-level max_parallel plan lines

Related #987
…ecution sandbox

Related #987

call_tools(requests, options?) dispatches independent upstream calls
concurrently (bounded worker pool, prefilled closed channel) and returns
per-slot {ok,result}|{ok,error} envelopes in input order. Workers produce
plain Go values; all goja conversion happens on the script goroutine after
an unconditional join. Per-element checks run in input order with lone-call
parity (budget -> allow-list -> agent-scope -> permission-tier); every
dispatched element yields exactly one ToolCallRecord, including under
cancellation. Spec-093 per-server admission applies unchanged inside the
call path.

## Changes
- internal/jsruntime: call_tools binding, batch engine, ExecutionContext
  ctx threading (workers cancel with the execution), error-code constants,
  gate extraction; extensive batch_test.go incl. -race cancellation tests
- internal/config: code_execution_max_parallel (default 8, range 1-32)
- internal/runtime: DetectConfigChanges clause for CodeExecution* fields
  (pre-existing hot-reload break)
- internal/server: defaults resolved via currentConfig() (second
  pre-existing hot-reload break), MaxParallel resolution, deduplicated
  code_execution descriptions into shared constants documenting call_tools
- oas/frontend/docs: swagger regen, settings field, 7 docs files

## Testing
- TDD throughout (red observed first; two invariants mutation-tested)
- go test -race ./internal/... green; server-edition green; lint v2 0
  issues; e2e 65/65
…elp states override semantics

Related #987

Review findings: code_execution_pool_size is sized once at server
construction and never resized in-process, so it now reports its own
changed field with requires_restart instead of riding the hot-applied
code_execution group; the settings help text now says a script can
override (not merely lower) batch concurrency per call within 1-32.
The Unix step has no explicit -timeout (Go default 10m per package); the
Windows step capped at 5m, which internal/runtime and internal/server now
exceed on slow runners as the suites have grown. Two consecutive Windows
failures on this branch were pure 'panic: test timed out after 5m0s' with
different tests holding the bag each time.
@Dumbris
Dumbris force-pushed the 096-batched-call-tools branch from 882a79a to 51ee14b Compare August 14, 2026 18:28
@github-actions

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: 096-batched-call-tools

Available Artifacts

  • archive-darwin-amd64 (29 MB)
  • archive-darwin-arm64 (26 MB)
  • archive-linux-amd64 (17 MB)
  • archive-linux-arm64 (15 MB)
  • archive-windows-amd64 (28 MB)
  • archive-windows-arm64 (25 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (22 MB)
  • installer-dmg-darwin-arm64 (20 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 31828813455 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approved (Model B): Paperclip review verdicts = ACCEPT and qa-gate green at this head SHA. Arming auto-merge; GitHub merges when all required checks pass.

@github-actions
github-actions Bot merged commit 99e5283 into main Aug 14, 2026
65 of 72 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.

2 participants