refactor(codemode): remove tool concurrency cap (#36545)

This commit is contained in:
Aiden Cline 2026-07-12 16:07:53 -05:00 committed by GitHub
commit 3c60470269
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 13 additions and 27 deletions

View file

@ -177,8 +177,8 @@ No limit has a default, on purpose: execution budgets are host policy. A host wi
interruption should set `maxOutputBytes` and `timeoutMs`. Limits are safe integers; invalid configuration throws a
`RangeError` at construction. Exceeding `maxOutputBytes` never fails the execution - oversized output is truncated
with an in-band marker. The timeout interrupts in-flight tool fibers and pure busy loops alike; a value the program
already returned survives a cleanup timeout as a success with a `TimeoutExceeded` warning. Two internals are fixed
constants, not knobs: at most 8 concurrent tool calls, and 32 levels of data nesting at boundaries.
already returned survives a cleanup timeout as a success with a `TimeoutExceeded` warning. CodeMode does not limit
tool-call concurrency. Data nesting at boundaries is limited to 32 levels.
## Boundaries and Non-Goals

View file

@ -77,15 +77,15 @@ order, and a combinator settles one reaction turn after its deciding member - wi
parity beyond that. At normal completion CodeMode interrupts everything still running - race losers,
fail-fast `Promise.all` stragglers, and fire-and-forget calls alike: the program has returned, so no future await can
exist, and work whose completion matters must be awaited by the program. Waiting for any class of leftover instead
would let it hold the execution open, or deadlock it when queued work needs tool-call permits the leftovers occupy.
would let it hold the execution open indefinitely.
Rejections that settled un-awaited before the return become `Success.warnings` diagnostics. A fatal program failure or
host interruption closes the execution promise scope and interrupts its active fibers instead. A timeout does the
same, except that a value the program already returned is preserved alongside a `TimeoutExceeded` warning rather than
discarded. At most eight tool calls execute concurrently.
discarded. CodeMode does not limit tool-call concurrency.
The public execution-policy knobs are `timeoutMs`, `maxToolCalls`, and `maxOutputBytes`. The package supplies no
defaults because budgets are host policy. The interpreter also enforces fixed internal boundaries for tool-call
concurrency and data nesting depth. `maxOutputBytes` bounds retained payload bytes, not the complete rendered message;
defaults because budgets are host policy. The interpreter also enforces a fixed internal data nesting depth.
`maxOutputBytes` bounds retained payload bytes, not the complete rendered message;
warning diagnostics have an equal separate budget so a large value cannot starve them, and fixed truncation notices and
host-added framing are intentionally outside the budgets.

View file

@ -24,7 +24,7 @@ ultimate source of truth.
- [x] Tool calls through the host-provided `tools` tree only.
- [x] The global `search(...)` built-in: synchronous tool discovery that counts as an admitted tool call and is
shadowable by program declarations like other globals.
- [x] Cooperative timeout, tool-call accounting, output bounding, and a maximum of eight concurrent tool calls.
- [x] Cooperative timeout, an optional total tool-call limit, output bounding, and unrestricted tool-call concurrency.
- [ ] Full JavaScript or TypeScript compatibility. CodeMode is a bounded orchestration language.
## Values and literals
@ -291,7 +291,6 @@ ultimate source of truth.
These are actionable implementation items. Check them off only when behavior and direct tests land.
- [x] Return real promises from `Promise.all`, `Promise.allSettled`, and `Promise.race`.
- [ ] Bound pending tool-call admission/allocation in addition to execution concurrency.
- [ ] Guarantee every advertised tool path is executable, including dotted and blocked path segments.
- [ ] Define safe outbound handling for non-finite numbers and `undefined` so invalid values cannot silently become
`null` in render-only or OpenAPI tool calls.

View file

@ -1,4 +1,4 @@
import { Cause, Effect, Semaphore } from "effect"
import { Cause, Effect } from "effect"
import { isBlockedMember, ToolReference, ToolRuntimeError, type SafeObject } from "../tool-runtime.js"
import {
type AstNode,
@ -50,7 +50,7 @@ import { dateMethods } from "../stdlib/date.js"
import { mathConstants } from "../stdlib/math.js"
import { numberConstants, numberMethods, numberStatics } from "../stdlib/number.js"
import { objectMethodsPreservingIdentity } from "../stdlib/object.js"
import { promiseStatics, TOOL_CALL_CONCURRENCY } from "../stdlib/promise.js"
import { promiseStatics } from "../stdlib/promise.js"
import { escapeRegexHint, regexpMethods, regexpProperties, regexFailureReason } from "../stdlib/regexp.js"
import { stringMethods, stringStatics } from "../stdlib/string.js"
import {
@ -150,7 +150,6 @@ export class Interpreter<R> {
private readonly invokeSearch: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
private readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
private readonly logs: Array<string>
private readonly callPermits: Semaphore.Semaphore
private readonly promises: PromiseRuntime<R>
private readonly runner: CallbackRunner<R> = {
invokeFunction: (fn, args) => this.invokeFunction(fn, args),
@ -163,7 +162,6 @@ export class Interpreter<R> {
toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>,
promises: PromiseRuntime<R>,
logs: Array<string> = [],
callPermits: Semaphore.Semaphore = Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY),
) {
const globalScope = new Map<string, Binding>()
this.scopes = new ScopeStack([globalScope])
@ -171,7 +169,6 @@ export class Interpreter<R> {
this.invokeSearch = invokeSearch
this.toolKeys = toolKeys
this.logs = logs
this.callPermits = callPermits
this.promises = promises
globalScope.set("tools", { mutable: false, value: new ToolReference([]) })
globalScope.set("search", { mutable: false, value: new SearchFunction() })
@ -239,7 +236,7 @@ export class Interpreter<R> {
path: ReadonlyArray<string>,
args: Array<unknown>,
): Effect.Effect<SandboxPromise, never, R> {
return this.createPromise(this.callPermits.withPermit(Effect.suspend(() => this.invokeTool(path, args))))
return this.createPromise(Effect.suspend(() => this.invokeTool(path, args)))
}
private createPromise(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<SandboxPromise, never, R> {
@ -1496,14 +1493,7 @@ export class Interpreter<R> {
}
private invokeFunction(fn: CodeModeFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
const invocation = new Interpreter(
this.invokeTool,
this.invokeSearch,
this.toolKeys,
this.promises,
this.logs,
this.callPermits,
)
const invocation = new Interpreter(this.invokeTool, this.invokeSearch, this.toolKeys, this.promises, this.logs)
invocation.scopes = new ScopeStack([...fn.capturedScopes, new Map()])
const run = Effect.gen(function* () {
// Seed all parameters first so defaults cannot fall through to same-named outer bindings.

View file

@ -1,5 +1,3 @@
import type { PromiseMethodName } from "../interpreter/model.js"
export const promiseStatics = new Set<PromiseMethodName>(["all", "allSettled", "race", "any", "resolve", "reject"])
export const TOOL_CALL_CONCURRENCY = 8

View file

@ -560,7 +560,7 @@ describe("Promise.all over arbitrary arrays", () => {
expect(trace.maxActive).toBeGreaterThan(1)
})
test("caps live tool-call concurrency at the fixed internal constant (8)", async () => {
test("does not cap live tool-call concurrency", async () => {
const trace = makeTrace()
const result = await value(
`
@ -572,8 +572,7 @@ describe("Promise.all over arbitrary arrays", () => {
{ trace },
)
expect(result).toBe(20)
expect(trace.maxActive).toBeGreaterThan(1)
expect(trace.maxActive).toBeLessThanOrEqual(8)
expect(trace.maxActive).toBe(20)
})
test("resolves the empty array", async () => {