refactor(codemode): remove tool concurrency cap (#36545)
This commit is contained in:
@@ -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
|
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
|
`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
|
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
|
already returned survives a cleanup timeout as a success with a `TimeoutExceeded` warning. CodeMode does not limit
|
||||||
constants, not knobs: at most 8 concurrent tool calls, and 32 levels of data nesting at boundaries.
|
tool-call concurrency. Data nesting at boundaries is limited to 32 levels.
|
||||||
|
|
||||||
## Boundaries and Non-Goals
|
## Boundaries and Non-Goals
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
defaults because budgets are host policy. The interpreter also enforces a fixed internal data nesting depth.
|
||||||
concurrency and data nesting depth. `maxOutputBytes` bounds retained payload bytes, not the complete rendered message;
|
`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
|
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.
|
host-added framing are intentionally outside the budgets.
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ ultimate source of truth.
|
|||||||
- [x] Tool calls through the host-provided `tools` tree only.
|
- [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
|
- [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.
|
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.
|
- [ ] Full JavaScript or TypeScript compatibility. CodeMode is a bounded orchestration language.
|
||||||
|
|
||||||
## Values and literals
|
## 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.
|
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`.
|
- [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.
|
- [ ] 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
|
- [ ] Define safe outbound handling for non-finite numbers and `undefined` so invalid values cannot silently become
|
||||||
`null` in render-only or OpenAPI tool calls.
|
`null` in render-only or OpenAPI tool calls.
|
||||||
|
|||||||
@@ -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 { isBlockedMember, ToolReference, ToolRuntimeError, type SafeObject } from "../tool-runtime.js"
|
||||||
import {
|
import {
|
||||||
type AstNode,
|
type AstNode,
|
||||||
@@ -50,7 +50,7 @@ import { dateMethods } from "../stdlib/date.js"
|
|||||||
import { mathConstants } from "../stdlib/math.js"
|
import { mathConstants } from "../stdlib/math.js"
|
||||||
import { numberConstants, numberMethods, numberStatics } from "../stdlib/number.js"
|
import { numberConstants, numberMethods, numberStatics } from "../stdlib/number.js"
|
||||||
import { objectMethodsPreservingIdentity } from "../stdlib/object.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 { escapeRegexHint, regexpMethods, regexpProperties, regexFailureReason } from "../stdlib/regexp.js"
|
||||||
import { stringMethods, stringStatics } from "../stdlib/string.js"
|
import { stringMethods, stringStatics } from "../stdlib/string.js"
|
||||||
import {
|
import {
|
||||||
@@ -150,7 +150,6 @@ export class Interpreter<R> {
|
|||||||
private readonly invokeSearch: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
|
private readonly invokeSearch: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
|
||||||
private readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
|
private readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
|
||||||
private readonly logs: Array<string>
|
private readonly logs: Array<string>
|
||||||
private readonly callPermits: Semaphore.Semaphore
|
|
||||||
private readonly promises: PromiseRuntime<R>
|
private readonly promises: PromiseRuntime<R>
|
||||||
private readonly runner: CallbackRunner<R> = {
|
private readonly runner: CallbackRunner<R> = {
|
||||||
invokeFunction: (fn, args) => this.invokeFunction(fn, args),
|
invokeFunction: (fn, args) => this.invokeFunction(fn, args),
|
||||||
@@ -163,7 +162,6 @@ export class Interpreter<R> {
|
|||||||
toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>,
|
toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>,
|
||||||
promises: PromiseRuntime<R>,
|
promises: PromiseRuntime<R>,
|
||||||
logs: Array<string> = [],
|
logs: Array<string> = [],
|
||||||
callPermits: Semaphore.Semaphore = Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY),
|
|
||||||
) {
|
) {
|
||||||
const globalScope = new Map<string, Binding>()
|
const globalScope = new Map<string, Binding>()
|
||||||
this.scopes = new ScopeStack([globalScope])
|
this.scopes = new ScopeStack([globalScope])
|
||||||
@@ -171,7 +169,6 @@ export class Interpreter<R> {
|
|||||||
this.invokeSearch = invokeSearch
|
this.invokeSearch = invokeSearch
|
||||||
this.toolKeys = toolKeys
|
this.toolKeys = toolKeys
|
||||||
this.logs = logs
|
this.logs = logs
|
||||||
this.callPermits = callPermits
|
|
||||||
this.promises = promises
|
this.promises = promises
|
||||||
globalScope.set("tools", { mutable: false, value: new ToolReference([]) })
|
globalScope.set("tools", { mutable: false, value: new ToolReference([]) })
|
||||||
globalScope.set("search", { mutable: false, value: new SearchFunction() })
|
globalScope.set("search", { mutable: false, value: new SearchFunction() })
|
||||||
@@ -239,7 +236,7 @@ export class Interpreter<R> {
|
|||||||
path: ReadonlyArray<string>,
|
path: ReadonlyArray<string>,
|
||||||
args: Array<unknown>,
|
args: Array<unknown>,
|
||||||
): Effect.Effect<SandboxPromise, never, R> {
|
): 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> {
|
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> {
|
private invokeFunction(fn: CodeModeFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
|
||||||
const invocation = new Interpreter(
|
const invocation = new Interpreter(this.invokeTool, this.invokeSearch, this.toolKeys, this.promises, this.logs)
|
||||||
this.invokeTool,
|
|
||||||
this.invokeSearch,
|
|
||||||
this.toolKeys,
|
|
||||||
this.promises,
|
|
||||||
this.logs,
|
|
||||||
this.callPermits,
|
|
||||||
)
|
|
||||||
invocation.scopes = new ScopeStack([...fn.capturedScopes, new Map()])
|
invocation.scopes = new ScopeStack([...fn.capturedScopes, new Map()])
|
||||||
const run = Effect.gen(function* () {
|
const run = Effect.gen(function* () {
|
||||||
// Seed all parameters first so defaults cannot fall through to same-named outer bindings.
|
// Seed all parameters first so defaults cannot fall through to same-named outer bindings.
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { PromiseMethodName } from "../interpreter/model.js"
|
import type { PromiseMethodName } from "../interpreter/model.js"
|
||||||
|
|
||||||
export const promiseStatics = new Set<PromiseMethodName>(["all", "allSettled", "race", "any", "resolve", "reject"])
|
export const promiseStatics = new Set<PromiseMethodName>(["all", "allSettled", "race", "any", "resolve", "reject"])
|
||||||
|
|
||||||
export const TOOL_CALL_CONCURRENCY = 8
|
|
||||||
|
|||||||
@@ -560,7 +560,7 @@ describe("Promise.all over arbitrary arrays", () => {
|
|||||||
expect(trace.maxActive).toBeGreaterThan(1)
|
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 trace = makeTrace()
|
||||||
const result = await value(
|
const result = await value(
|
||||||
`
|
`
|
||||||
@@ -572,8 +572,7 @@ describe("Promise.all over arbitrary arrays", () => {
|
|||||||
{ trace },
|
{ trace },
|
||||||
)
|
)
|
||||||
expect(result).toBe(20)
|
expect(result).toBe(20)
|
||||||
expect(trace.maxActive).toBeGreaterThan(1)
|
expect(trace.maxActive).toBe(20)
|
||||||
expect(trace.maxActive).toBeLessThanOrEqual(8)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("resolves the empty array", async () => {
|
test("resolves the empty array", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user