diff --git a/packages/codemode/README.md b/packages/codemode/README.md index 1b30549ec8..e8447e09b2 100644 --- a/packages/codemode/README.md +++ b/packages/codemode/README.md @@ -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 diff --git a/packages/codemode/codemode.md b/packages/codemode/codemode.md index 22092c533e..f26c0446ef 100644 --- a/packages/codemode/codemode.md +++ b/packages/codemode/codemode.md @@ -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. diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 0bfef64ef3..aff98d9b49 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -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. diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index 1770c84e94..3b9e18cc9d 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -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 { private readonly invokeSearch: (args: Array) => Effect.Effect private readonly toolKeys: (path: ReadonlyArray) => ReadonlyArray private readonly logs: Array - private readonly callPermits: Semaphore.Semaphore private readonly promises: PromiseRuntime private readonly runner: CallbackRunner = { invokeFunction: (fn, args) => this.invokeFunction(fn, args), @@ -163,7 +162,6 @@ export class Interpreter { toolKeys: (path: ReadonlyArray) => ReadonlyArray, promises: PromiseRuntime, logs: Array = [], - callPermits: Semaphore.Semaphore = Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY), ) { const globalScope = new Map() this.scopes = new ScopeStack([globalScope]) @@ -171,7 +169,6 @@ export class Interpreter { 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 { path: ReadonlyArray, args: Array, ): Effect.Effect { - 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): Effect.Effect { @@ -1496,14 +1493,7 @@ export class Interpreter { } private invokeFunction(fn: CodeModeFunction, args: Array): Effect.Effect { - 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. diff --git a/packages/codemode/src/stdlib/promise.ts b/packages/codemode/src/stdlib/promise.ts index 2bad2caf0c..73d02d6c0c 100644 --- a/packages/codemode/src/stdlib/promise.ts +++ b/packages/codemode/src/stdlib/promise.ts @@ -1,5 +1,3 @@ import type { PromiseMethodName } from "../interpreter/model.js" export const promiseStatics = new Set(["all", "allSettled", "race", "any", "resolve", "reject"]) - -export const TOOL_CALL_CONCURRENCY = 8 diff --git a/packages/codemode/test/promise.test.ts b/packages/codemode/test/promise.test.ts index 5ce4d003d5..db607336de 100644 --- a/packages/codemode/test/promise.test.ts +++ b/packages/codemode/test/promise.test.ts @@ -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 () => {