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
+6
View File
@@ -268,6 +268,12 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
for (const context of result.additionalContexts ?? []) {
exec.deferContext(context)
}
// Like the context forwarding above, cross-boundary facts travel on
// the nested result and the composite forwards them: only a
// successful nested result can carry the terminal marker
// (ToolExecutionFailure types it never), so a policy-converted
// failure cannot stop the turn through a recovering program.
if (result.concludesTurn) exec.concludeTurn()
exec.agent?.session.append('tool/code-dispatch', {
parentCallId: exec.callId,
subCallId,
+11 -25
View File
@@ -307,7 +307,14 @@ export interface ToolRunContext extends ToolExecution {
* are emitted in call order.
*/
deferContext(context: UserMessageData): void
/** Mark a successful final result as terminal for the current agent turn. */
/**
* Mark a successful final result as terminal for the current agent turn.
* The marker rides this execution's own result (`concludesTurn` exists only
* on {@link ToolExecutionSuccess}); a composite that dispatches nested
* calls forwards it from the nested result, exactly like
* `additionalContexts`, so only an authoritative nested success can
* conclude the enclosing run.
*/
concludeTurn(): void
}
@@ -653,16 +660,8 @@ export class ToolRegistry extends Service {
/** Context deferred by a running tool body, keyed by its scheduler-owned execution. */
private deferredContexts = new WeakMap<ToolRunContext, UserMessageData[]>()
/** Successful executions whose tool body declared the current turn complete. */
/** Executions whose tool body declared the current turn complete. */
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. */
@@ -985,7 +984,6 @@ export class ToolRegistry extends Service {
const definition = this.get(name, agent)
const finalizeContent = definition?.finalizeContent?.bind(definition)
const concludingExecutions = this.concludingExecutions
const pendingParentConclusions = this.pendingParentConclusions
const base = {
token,
callId,
@@ -997,10 +995,7 @@ export class ToolRegistry extends Service {
deferredContexts.push(context)
},
concludeTurn(): void {
if (parent === undefined) concludingExecutions.add(this as unknown as ToolExecution)
// 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)
concludingExecutions.add(this as unknown as ToolExecution)
},
}
try {
@@ -1214,16 +1209,7 @@ 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
}
@@ -1394,7 +1380,7 @@ export class ToolRegistry extends Service {
}
meta = snapshotProjection(tool.name, 'presentationMeta', projected)
}
const concludesTurn = this.concludingExecutions.has(exec) || this.concludingParents.has(exec.token)
const concludesTurn = this.concludingExecutions.has(exec)
return this.markCanonical(exec, this.materializeFinalResult({
isError: false,
value,