refactor(tools): let the terminal marker ride the nested result

The staged-promotion machinery (concludingParents keyed by parent token
plus a pendingParentConclusions staging map) spread one fact — this call
concluded the turn — across three registry-side collections with manual
cleanup. Align it with how additionalContexts already crosses the same
boundary: concludeTurn() marks only its own execution, the marker rides
that execution's successful result (ToolExecutionFailure types
concludesTurn as never, so a policy-converted failure sheds it with the
type), and the composite that owns the nested dispatch forwards it —
Code Mode's binding does so beside its existing context forwarding.

The registry loses both parent-keyed collections and the promotion
block; the propagation decision moves to the owning boundary; the
structured-output consumer's own two-phase commit is untouched.
This commit is contained in:
_Kerman
2026-07-26 21:24:16 +08:00
parent 39c09c9a49
commit 04f0435cc9
4 changed files with 68 additions and 34 deletions
@@ -417,6 +417,46 @@ describe('the run_code dispatch bridge', () => {
expect(result.content).toEqual([{ type: 'text', text: 'done' }])
})
it('forwards a nested terminal conclusion onto the successful run_code result', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
ctx.tools.register(defineTool({
name: 'finalize',
description: 'Terminal tool.',
parameters: {},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
execute(_args, exec) {
exec.concludeTurn()
return Promise.resolve('done')
},
}))
runtime.behavior = async (request) => {
await request.bindings[0]!.functions.finalize!({})
return { logs: [], value: 'program complete' }
}
const concluded = await runCode(ctx, 'await tools.finalize({})')
expect(concluded.isError).toBe(false)
expect(concluded.concludesTurn).toBe(true)
// A policy that converts the nested success into an error strips the
// marker with the result type: the recovering program cannot conclude.
const veto = ctx.on('tools/post-execute', async (exec, _result, next): Promise<PostToolDecision> => {
if (exec.name !== 'finalize') return next()
return { kind: 'block', feedback: [{ type: 'text', text: 'terminal rejected' }] }
})
runtime.behavior = async (request) => {
await request.bindings[0]!.functions.finalize!({}).catch(() => undefined)
return { logs: [], value: 'recovered' }
}
const recovered = await runCode(ctx, 'await tools.finalize({}).catch(() => {})')
veto()
expect(recovered.isError).toBe(false)
expect(recovered.concludesTurn).toBeUndefined()
})
it('serializes Promise.all dispatches: tool executions never overlap, in submission order', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const intervals: [string, string][] = []