fix: reach quiescence even when the runtime rejects (agent review)

[P1] review finding: the run-scoped abort + queue drain ran only after
runtime.run() FULFILLED, so a backend that starts a binding call and
then throws left the sub-dispatch running past run_code's settlement —
its tool/code-dispatch event could append after the parent call
returned, breaking the drain-before-return contract. The quiescence
pair now lives in a finally around runtime.run(); the folded queue tail
keeps the drain from masking the thrown error. Pinned by a test whose
fake runtime fails mid-flight: pre-fix it returns in milliseconds with
the slow tool still running.
This commit is contained in:
Tianyi Cui
2026-07-08 22:03:27 +08:00
parent 35ef649716
commit 1b29273f12
2 changed files with 61 additions and 15 deletions

View File

@@ -385,6 +385,44 @@ describe('the run_code dispatch bridge', () => {
expect(sawAbort).toBe(true)
})
it('a runtime that starts a binding call and then REJECTS still reaches quiescence before returning', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const { agent, events } = fakeAgent()
let sawAbort = false
let started!: () => void
const inFlight = new Promise<void>((resolve) => { started = resolve })
ctx.tools.register(defineTool({
name: 'slow',
description: 'Slow tool observing its signal.',
parameters: { id: { type: 'string', required: true } },
async execute(args, exec) {
started()
await new Promise<void>((resolve) => {
const timer = setTimeout(resolve, 500)
exec.signal?.addEventListener('abort', () => { sawAbort = true; clearTimeout(timer); resolve() }, { once: true })
})
return [{ type: 'text' as const, text: args.id }]
},
}))
runtime.behavior = async (request) => {
// Start a sub-dispatch, keep its rejection held, and fail the run once
// the tool is genuinely in flight — a seam error AFTER work has begun.
// The bridge's settlement still owes quiescence: without the finally,
// run_code would return now and the slow tool would finish (and log)
// afterwards.
request.bindings[0]!.functions.slow!({ id: 'orphan' }).catch(() => 'held')
await inFlight
throw new Error('backend exploded')
}
const result = await runCode(ctx, 'program', { agent })
expect(result.isError).toBe(true)
expect((result.content[0] as { text: string }).text).toContain('backend exploded')
// Quiescence held: the in-flight sub-dispatch was aborted and its event
// logged INSIDE the run_code execution, not after it returned.
expect(sawAbort).toBe(true)
expect(events.filter(event => event.type === 'tool/code-dispatch').map(event => (event.data as { name: string }).name)).toEqual(['slow'])
})
it('runs without an owning agent: dispatches work, event logging is skipped', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const calls = registerEcho(ctx)