refactor(subagent): stage structured captures in a WeakMap keyed by execution

Supersedes the single-slot staging the execution-identity fix (06c5f17e)
kept: the one pending slot needed a mismatch-drop branch plus a defensive
coverage-ignored finally to manage orphans, and it carried a latent trap —
under the loop's documented parallel-execution TODO, two in-flight capture
trips would overwrite the slot and BOTH be dropped.

Staging in a WeakMap<ToolExecution, {value}> makes the stale-stage class
structurally impossible instead of managed: an entry orphaned by an outer
short-circuiting listener can never match a different execution's lookup
(whatever call id that execution carries), needs no drop bookkeeping (the
map reclaims it with the execution object), and staging cannot cross-clobber
under parallel execution. Staging is the only layer this future-proofs — a
parallel cut would still owe its own single-accept rule for the captured
value, which is documented rather than claimed. Behavior is pinned by the
existing orphan/call-id-reuse regression tests, which pass unchanged; the
commit listener loses two branches and the v8-ignore.
This commit is contained in:
Tianyi Cui
2026-07-09 12:30:27 +08:00
parent 96c3c94f85
commit 6f4ea8a260
2 changed files with 39 additions and 42 deletions
@@ -34,14 +34,14 @@
* lists `structured_output` before further tool calls cannot run side
* effects after the final answer was accepted.
* - `tools/post-execute` (prepend, scoped): the capture COMMIT. The tool body
* only STAGES the validated value, KEYED BY THE EXECUTION OBJECT'S
* IDENTITY; it becomes the run's captured result only when the final
* only STAGES the validated value, KEYED BY THE EXECUTION OBJECT in a
* WeakMap; it becomes the run's captured result only when the final
* post-execute decision accepts THAT SAME pipeline trip. Execution-keyed
* staging closes the stale-stage hole unconditionally: an outer
* short-circuiting listener (post-execute block, or a pre-execute deny
* whose call never dispatched) can orphan a staged value, and neither a
* later call nor one REUSING the same adapter-minted call id can ever
* promote it — only the execution whose own body staged can commit.
* staging makes the stale-stage class structurally impossible: a value
* orphaned by an outer short-circuiting listener (a post-execute block, or
* a pre-execute deny whose call never dispatched) can never match another
* execution's lookup — whatever call id that execution carries — and is
* reclaimed with the execution object itself.
*
* @module @deepseek-ai/dsh-subagent-inprocess/structured
*/
@@ -89,14 +89,20 @@ export interface StructuredAttachment {
*/
export function attachStructuredRuntime(childCtx: Context, schema: StructuredOutputSchema): StructuredAttachment {
/**
* A validated value staged by the capture tool body, awaiting ITS OWN
* call's post-execute verdict — keyed by the {@link ToolExecution} OBJECT
* identity, the one token that provably ties a stage to one trip through
* the pipeline. A call id cannot key this: ids are adapter-minted and may
* repeat across steps, and a denied/failed later call REUSING an orphaned
* stage's id must never promote it.
* Validated values staged by the capture tool body, awaiting THEIR OWN
* call's post-execute verdict — keyed by the {@link ToolExecution} OBJECT,
* the one token that provably ties a stage to one trip through the
* pipeline. A call id cannot key this: ids are adapter-minted and may
* repeat across steps. Keying by execution makes the stale-stage class
* structurally impossible — an entry orphaned by an outer short-circuiting
* listener can never match a different execution's lookup, needs no drop
* bookkeeping (the WeakMap reclaims it with the execution object), and two
* in-flight captures can never cross-clobber each other's STAGE should
* tool execution ever go parallel (the loop's documented TODO). Staging is
* the only layer this future-proofs: a parallel-execution cut would still
* owe its own single-accept rule for `captured` itself.
*/
let pending: { exec: ToolExecution; value: unknown } | undefined
const staged = new WeakMap<ToolExecution, { value: unknown }>()
let captured: { value: unknown } | undefined
const schemaEntry: ToolSchema = {
@@ -119,7 +125,7 @@ export function attachStructuredRuntime(childCtx: Context, schema: StructuredOut
// Two-phase commit, KEYED BY THIS EXECUTION: the body only stages; the
// post-execute listener promotes exactly this pipeline trip's entry
// when the final decision accepts it.
pending = { exec, value: args }
staged.set(exec, { value: args })
return Promise.resolve([{ type: 'text', text: 'Structured output recorded.' }])
},
})
@@ -204,35 +210,26 @@ export function attachStructuredRuntime(childCtx: Context, schema: StructuredOut
return next()
}, { prepend: true })
// The capture COMMIT: promote the staged value only when the final
// post-execute decision accepts THE SAME CALL that staged it. The staging
// slot clears on every path for that call; a stale entry from an outer
// short-circuited chain (its verdict never reached us) is dropped when any
// later call reaches the commit, never promoted.
// The capture COMMIT: promote a staged value only when the final
// post-execute decision accepts THE SAME EXECUTION that staged it — the
// lookup key IS the execution, so a stale entry from a different pipeline
// trip (its own chain short-circuited past this commit by an outer
// post-execute block, or an outer pre-execute deny whose call never
// dispatched) is unreachable here by construction, whatever the current
// call's id.
childCtx.on('tools/post-execute', async function (
this: unknown, exec: ToolExecution, _result: ToolExecutionResult, next: () => Promise<PostToolDecision>,
): Promise<PostToolDecision> {
if (exec.name !== STRUCTURED_OUTPUT_TOOL || pending === undefined) return next()
if (pending.exec !== exec) {
// A stale stage from a DIFFERENT pipeline trip: its own chain was
// short-circuited past this commit (an outer post-execute block, or an
// outer pre-execute deny whose call never dispatched), so its verdict
// never reached us. Whatever the current call's id, the orphan must
// never ride its acceptance — drop it.
pending = undefined
return next()
}
const staged = pending
try {
const decision = await next()
if (decision.kind === 'accept') captured = { value: staged.value }
return decision
} finally {
/* v8 ignore next -- defensive false branch: a concurrent re-stage
* would need a second capture call INSIDE the first's post-execute
* chain */
if (pending === staged) pending = undefined
}
if (exec.name !== STRUCTURED_OUTPUT_TOOL) return next()
const entry = staged.get(exec)
if (entry === undefined) return next()
// Single-shot per execution: this trip's verdict is decided by the chain
// below, never revisited (the WeakMap would reclaim the entry either way;
// deleting states the intent).
staged.delete(exec)
const decision = await next()
if (decision.kind === 'accept') captured = { value: entry.value }
return decision
}, { prepend: true })
return { captured: () => captured }
@@ -578,7 +578,7 @@ describe('in-process structured output', () => {
expect(result.error?.code).toBe('UNKNOWN_TOOL')
})
it('drops a stale stage from a short-circuited chain: a later call never promotes it (call-keyed commit)', async () => {
it('a stale stage from a short-circuited chain is never promoted by a later call (execution-keyed commit)', async () => {
const { ctx, parent } = await setup([
toolCallResponse('c1', STRUCTURED_OUTPUT_TOOL, { answer: 1 }),
])