fix(codemode): return promises from combinators

This commit is contained in:
Aiden Cline 2026-07-07 14:01:28 -05:00
parent da68e2865e
commit 326dd7e8e7
3 changed files with 175 additions and 67 deletions

View file

@ -63,8 +63,10 @@ path lookup, namespace browsing, deterministic ranking, and pagination.
### Tool execution
Calling a tool starts its Effect eagerly on a supervised fiber. The returned sandbox promise is run-once and can be
awaited directly or through the supported `Promise` combinators. At most eight tool calls execute concurrently.
Unfinished calls are drained before successful program completion, and an unhandled call failure becomes a diagnostic.
awaited directly or through `Promise.all`, `Promise.allSettled`, and `Promise.race`. These combinators also return eager,
run-once sandbox promises, so independent aggregate batches overlap and rejection is observed at the eventual `await`.
`Promise.resolve` and `Promise.reject` use the same tracked lifecycle. At most eight tool calls execute concurrently.
Unfinished promises are drained before successful program completion, and an unhandled failure becomes a diagnostic.
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

View file

@ -2212,19 +2212,21 @@ class Interpreter<R> {
// Promise.resolve of a promise is that promise (JS flattens); anything else is a
// promise already fulfilled with the value.
const value = args[0]
return Effect.succeed(
value instanceof SandboxPromise ? value : new SandboxPromise(undefined, Effect.succeed(value)),
)
return value instanceof SandboxPromise ? Effect.succeed(value) : this.createPromise(Effect.succeed(value))
}
if (ref.name === "reject") {
return Effect.sync(() => new SandboxPromise(undefined, Effect.fail(new ProgramThrow(args[0]))))
return this.createPromise(Effect.fail(new ProgramThrow(args[0])))
}
const items = Array.isArray(args[0]) ? args[0] : spreadItems(args[0])
if (items === undefined) {
throw new InterpreterRuntimeError(
`Promise.${ref.name} expects an array of promises or plain values (e.g. Promise.${ref.name}(items.map((item) => tools.ns.tool(item)))).`,
node,
return this.createPromise(
Effect.fail(
new InterpreterRuntimeError(
`Promise.${ref.name} expects an array of promises or plain values (e.g. Promise.${ref.name}(items.map((item) => tools.ns.tool(item)))).`,
node,
),
),
)
}
@ -2238,7 +2240,7 @@ class Interpreter<R> {
? Effect.map(this.observePromise(item), (exit) => ({ index, item, exit }))
: Effect.succeed({ index, item: undefined, exit: Exit.succeed(item) }),
)
return Effect.gen(function* () {
const aggregate = Effect.gen(function* () {
const remaining = [...observations]
const values: Array<unknown> = []
values.length = items.length
@ -2250,19 +2252,25 @@ class Interpreter<R> {
values[winner.index] = winner.exit.value
continue
}
yield* self.createPromise(
Effect.asVoid(
Effect.forEach(
items,
(item) => (item instanceof SandboxPromise ? self.observePromise(item) : Effect.void),
{ concurrency: "unbounded" },
),
),
)
return yield* self.unwrapPromiseExit(winner.item, winner.exit, node)
}
return values
})
return Effect.gen(function* () {
const promise = yield* self.createPromise(aggregate)
// Keep observing every member after fail-fast settlement without tying the drain
// fiber to the aggregate fiber, whose completion interrupts its own children.
yield* self.createPromise(
Effect.asVoid(
Effect.forEach(
items,
(item) => (item instanceof SandboxPromise ? self.observePromise(item) : Effect.void),
{ concurrency: "unbounded" },
),
),
)
return promise
})
}
case "allSettled": {
const observations = items.map((item) =>
@ -2270,42 +2278,48 @@ class Interpreter<R> {
? Effect.map(this.observePromise(item), (exit) => ({ promise: item as SandboxPromise | undefined, exit }))
: Effect.succeed({ promise: undefined as SandboxPromise | undefined, exit: Exit.succeed(item as unknown) }),
)
return Effect.gen(function* () {
const outcomes: Array<unknown> = []
for (const observation of observations) {
const { exit, promise } = yield* observation
if (Exit.isSuccess(exit)) {
outcomes.push(
Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }),
)
continue
}
const raceInterrupted = promise?.interrupted === true && Cause.hasInterruptsOnly(exit.cause)
if (Cause.hasInterruptsOnly(exit.cause) && !raceInterrupted) {
// Execution teardown (timeout/host interruption), not a program-level rejection.
return yield* Effect.failCause(exit.cause)
}
const thrown = raceInterrupted
? new InterpreterRuntimeError(
"This tool call was interrupted because another value settled a Promise.race first.",
node,
return this.createPromise(
Effect.gen(function* () {
const outcomes: Array<unknown> = []
for (const observation of observations) {
const { exit, promise } = yield* observation
if (Exit.isSuccess(exit)) {
outcomes.push(
Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }),
)
: Cause.squash(exit.cause)
outcomes.push(
Object.assign(Object.create(null) as SafeObject, {
status: "rejected",
reason: caughtErrorValue(thrown),
}),
)
}
return outcomes
})
continue
}
const raceInterrupted = promise?.interrupted === true && Cause.hasInterruptsOnly(exit.cause)
if (Cause.hasInterruptsOnly(exit.cause) && !raceInterrupted) {
// Execution teardown (timeout/host interruption), not a program-level rejection.
return yield* Effect.failCause(exit.cause)
}
const thrown = raceInterrupted
? new InterpreterRuntimeError(
"This tool call was interrupted because another value settled a Promise.race first.",
node,
)
: Cause.squash(exit.cause)
outcomes.push(
Object.assign(Object.create(null) as SafeObject, {
status: "rejected",
reason: caughtErrorValue(thrown),
}),
)
}
return outcomes
}),
)
}
case "race": {
if (items.length === 0) {
throw new InterpreterRuntimeError(
"Promise.race([]) would never settle; provide at least one promise or value.",
node,
return this.createPromise(
Effect.fail(
new InterpreterRuntimeError(
"Promise.race([]) would never settle; provide at least one promise or value.",
node,
),
),
)
}
const observations = items.map((item, index) =>
@ -2313,22 +2327,24 @@ class Interpreter<R> {
? Effect.map(this.observePromise(item), (exit) => ({ index, exit }))
: Effect.succeed({ index, exit: Exit.succeed(item as unknown) }),
)
return Effect.gen(function* () {
// First settlement (fulfilled OR rejected) wins; the observations never fail, so
// racing them yields exactly that. Losing in-flight calls are then interrupted.
const winner = yield* Effect.raceAll(observations)
for (const [index, item] of items.entries()) {
if (index === winner.index || !(item instanceof SandboxPromise) || item.fiber === undefined) continue
item.interrupted = true
yield* Fiber.interrupt(item.fiber)
}
const winningItem = items[winner.index]
return yield* self.unwrapPromiseExit(
winningItem instanceof SandboxPromise ? winningItem : undefined,
winner.exit,
node,
)
})
return this.createPromise(
Effect.gen(function* () {
// First settlement (fulfilled OR rejected) wins; the observations never fail, so
// racing them yields exactly that. Losing in-flight calls are then interrupted.
const winner = yield* Effect.raceAll(observations)
for (const [index, item] of items.entries()) {
if (index === winner.index || !(item instanceof SandboxPromise) || item.fiber === undefined) continue
item.interrupted = true
yield* Fiber.interrupt(item.fiber)
}
const winningItem = items[winner.index]
return yield* self.unwrapPromiseExit(
winningItem instanceof SandboxPromise ? winningItem : undefined,
winner.exit,
node,
)
}),
)
}
}
}

View file

@ -270,6 +270,59 @@ describe("promises at data boundaries", () => {
})
describe("Promise.all over arbitrary arrays", () => {
test("combinators return promises that can be assigned and awaited later", async () => {
expect(
await value(`
const all = Promise.all([Promise.resolve(1)])
const settled = Promise.allSettled([Promise.reject("no")])
const race = Promise.race([Promise.resolve(2)])
const promises = [all instanceof Promise, settled instanceof Promise, race instanceof Promise]
return [promises, await all, await settled, await race]
`),
).toEqual([[true, true, true], [1], [{ status: "rejected", reason: "no" }], 2])
})
test("separately-created aggregate batches overlap before either is awaited", async () => {
const trace = makeTrace()
expect(
await value(
`
const first = Promise.all([tools.host.sleepy({ id: 1, ms: 40 })])
const second = Promise.all([tools.host.sleepy({ id: 2, ms: 40 })])
return [await first, await second]
`,
{ trace },
),
).toEqual([[1], [2]])
expect(trace.starts).toEqual([1, 2])
expect(trace.maxActive).toBeGreaterThan(1)
})
test("an aggregate created before a try block rejects at its later await", async () => {
expect(
await value(`
const aggregate = Promise.all([tools.host.fail({})])
try {
await aggregate
return "no"
} catch (error) {
return error.message
}
`),
).toBe("Lookup refused")
})
test("awaiting an aggregate repeatedly does not rerun its members", async () => {
const result = await run(`
const aggregate = Promise.all([tools.host.sleepy({ id: 7 })])
return [await aggregate, await aggregate]
`)
expect(result.ok).toBe(true)
if (!result.ok) return
expect(result.value).toEqual([[7], [7]])
expect(result.toolCalls).toStrictEqual([{ name: "host.sleepy" }])
})
test("mixes promises and plain values, preserving order", async () => {
expect(
await value(`
@ -374,6 +427,30 @@ describe("Promise.all over arbitrary arrays", () => {
expect(trace.interrupted).toBe(0)
})
test("drains a later sibling rejection after failing fast", async () => {
const trace = makeTrace()
expect(
await value(
`
const failLater = async () => {
await tools.host.sleepy({ id: 1, ms: 40 })
throw new Error("later")
}
const aggregate = Promise.all([Promise.reject(new Error("first")), failLater()])
try {
await aggregate
return "no"
} catch (error) {
return error.message
}
`,
{ trace },
),
).toBe("first")
expect(trace.completed).toBe(1)
expect(trace.interrupted).toBe(0)
})
test("a non-collection argument is a clear error", async () => {
const diagnostic = await error(`return await Promise.all(42)`)
expect(diagnostic.message).toContain("Promise.all expects an array")
@ -484,6 +561,9 @@ describe("Promise.resolve / Promise.reject", () => {
expect(await value(`return await Promise.resolve(42)`)).toBe(42)
expect(await value(`return await Promise.resolve(Promise.resolve("nested"))`)).toBe("nested")
expect(await value(`return await Promise.resolve(tools.host.sleepy({ id: 3 }))`)).toBe(3)
expect(await value(`const promise = Promise.resolve(1); return [promise].includes(Promise.resolve(promise))`)).toBe(
true,
)
})
test("reject produces a promise whose await throws the reason", async () => {
@ -498,6 +578,16 @@ describe("Promise.resolve / Promise.reject", () => {
`),
).toBe("nope")
})
test("an abandoned rejected promise is reported as unhandled", async () => {
const diagnostic = await error(`
Promise.reject(new Error("abandoned"))
return "done"
`)
expect(diagnostic.kind).toBe("ExecutionFailure")
expect(diagnostic.message).toContain("Unhandled rejection from an un-awaited promise")
expect(diagnostic.message).toContain("abandoned")
})
})
describe("timeout interruption of forked calls", () => {