fix: close three seams the message-machine refactor left open
agent-loop lifecycle: dispose drains machine.done to true quiescence. cancel()'s own running-to-idle transition can legitimately re-enter through an automation listener (goal-session's idle drive runs synchronously to its first await) and replace done with a fresh admission after the single capture; teardown now re-cancels and re-awaits until the slot stabilizes, so the scope never unwinds under a live run. tools: a nested concludeTurn() stages on its own execution and promotes to the enclosing composite only on the call's authoritative successful verdict. A post-execute policy that converts the nested success into an error no longer lets a recovering composite stop the turn on a failed terminal operation (the Code Mode structured-output shape). goal-session: the driver owns its round durability barrier again. The loop's persistence is eager write-behind with no turn-end flush, so the old post-turn agent/error signal for flush failures never fires; a settled round now sets needsCheckpoint and re-enters drive, flushing before the next reservation and disarming on failure instead of queueing an autonomous round on state that was never persisted.
This commit is contained in:
@@ -657,6 +657,12 @@ export class ToolRegistry extends Service {
|
||||
private concludingExecutions = new WeakSet<ToolExecution>()
|
||||
/** Enclosing transport tokens marked terminal by a successful nested call. */
|
||||
private concludingParents = new Set<ToolExecutionToken>()
|
||||
/**
|
||||
* Nested conclusions staged until their call's final verdict: a post-execute
|
||||
* policy may still convert the nested success into an error, and a failed
|
||||
* terminal operation must not stop the turn through its composite.
|
||||
*/
|
||||
private pendingParentConclusions = new WeakMap<ToolRunContext, ToolExecutionToken>()
|
||||
/** Original caller cancellation, kept outside the wrapper-mutable execution object. */
|
||||
private cancellationStates = new WeakMap<ToolRunContext, ToolCancellationState>()
|
||||
/** Definition-owned final content transform snapshotted before policy begins. */
|
||||
@@ -979,7 +985,7 @@ export class ToolRegistry extends Service {
|
||||
const definition = this.get(name, agent)
|
||||
const finalizeContent = definition?.finalizeContent?.bind(definition)
|
||||
const concludingExecutions = this.concludingExecutions
|
||||
const concludingParents = this.concludingParents
|
||||
const pendingParentConclusions = this.pendingParentConclusions
|
||||
const base = {
|
||||
token,
|
||||
callId,
|
||||
@@ -992,7 +998,9 @@ export class ToolRegistry extends Service {
|
||||
},
|
||||
concludeTurn(): void {
|
||||
if (parent === undefined) concludingExecutions.add(this as unknown as ToolExecution)
|
||||
else concludingParents.add(parent)
|
||||
// Staged, not propagated: only this nested call's authoritative
|
||||
// successful result promotes the marker onto its parent.
|
||||
else pendingParentConclusions.set(this as unknown as ToolRunContext, parent)
|
||||
},
|
||||
}
|
||||
try {
|
||||
@@ -1206,6 +1214,14 @@ export class ToolRegistry extends Service {
|
||||
} catch (error: unknown) {
|
||||
finalResult = this.materializeFinalResult(toolErrorResult(error))
|
||||
}
|
||||
// Promote a staged nested conclusion only on the call's authoritative
|
||||
// successful verdict — a policy-converted failure must not let the
|
||||
// composite stop the turn on a failed terminal operation.
|
||||
const stagedParent = this.pendingParentConclusions.get(exec)
|
||||
if (stagedParent !== undefined) {
|
||||
this.pendingParentConclusions.delete(exec)
|
||||
if (!finalResult.isError) this.concludingParents.add(stagedParent)
|
||||
}
|
||||
this.notifyResult(exec, finalResult)
|
||||
this.concludingParents.delete(exec.token)
|
||||
return finalResult
|
||||
|
||||
@@ -553,6 +553,54 @@ describe('ToolRegistry', () => {
|
||||
expect(nested.isError ? undefined : nested.value).toBe('')
|
||||
})
|
||||
|
||||
it('propagates a nested concludeTurn only when the nested verdict stays successful', async () => {
|
||||
const ctx = await setup()
|
||||
ctx.tools.register({
|
||||
...echoTool,
|
||||
name: 'terminal-nested',
|
||||
async execute(_args, exec) {
|
||||
exec.concludeTurn()
|
||||
return 'staged'
|
||||
},
|
||||
})
|
||||
// A composite that swallows its nested failure and returns success — the
|
||||
// Code Mode shape the staged propagation exists for.
|
||||
let call = 0
|
||||
ctx.tools.register({
|
||||
...echoTool,
|
||||
name: 'composite',
|
||||
async execute(_args, exec) {
|
||||
call += 1
|
||||
const nested = await ctx.tools.execute({
|
||||
signal: exec.signal, callId: CallId(`nested-${call}`), name: 'terminal-nested', arguments: {}, parent: exec.token,
|
||||
})
|
||||
return nested.isError ? 'nested failed, composite recovered' : 'nested succeeded'
|
||||
},
|
||||
})
|
||||
|
||||
// A policy converts the nested success into an error: the staged
|
||||
// conclusion must NOT reach the composite, or the loop would stop the
|
||||
// turn on a failed terminal operation.
|
||||
const veto = ctx.on('tools/post-execute', async (exec, _result, next): Promise<PostToolDecision> => {
|
||||
if (exec.name !== 'terminal-nested') return next()
|
||||
return { kind: 'block', feedback: [{ type: 'text', text: 'nested success rejected' }] }
|
||||
})
|
||||
const recovered = await ctx.tools.execute({
|
||||
signal: testToolSignal, callId: CallId('composite-vetoed'), name: 'composite', arguments: {},
|
||||
})
|
||||
expect(recovered.isError).toBe(false)
|
||||
expect(recovered.concludesTurn).toBeUndefined()
|
||||
veto()
|
||||
|
||||
// The same nested call succeeding promotes the marker: the composite's
|
||||
// own successful result now carries concludesTurn.
|
||||
const concluded = await ctx.tools.execute({
|
||||
signal: testToolSignal, callId: CallId('composite-ok'), name: 'composite', arguments: {},
|
||||
})
|
||||
expect(concluded.isError).toBe(false)
|
||||
expect(concluded.concludesTurn).toBe(true)
|
||||
})
|
||||
|
||||
it('returns isError results for unknown tools and throwing tools', async () => {
|
||||
const ctx = await setup()
|
||||
ctx.tools.register({
|
||||
|
||||
Reference in New Issue
Block a user