2026-07-06 23:29:08 +08:00
import { describe , expect , it } from 'vitest'
import { Context } from 'cordis'
2026-07-28 13:55:59 +08:00
import { createUserMessage , CallId , type ContentBlock , type GenerateOptions } from '@deepseek-ai/dsh-llm'
2026-07-18 12:21:15 +08:00
import { SessionId } from '@deepseek-ai/dsh-session'
2026-07-06 23:29:08 +08:00
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
2026-07-16 17:39:53 +08:00
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
2026-07-19 19:19:57 +08:00
import InvariantService from '@deepseek-ai/dsh-invariants'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
2026-07-06 23:29:08 +08:00
import SubagentService , { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
2026-07-21 01:11:55 +08:00
import type { Config as ToolConfig , ObjectJsonSchema } from '@deepseek-ai/dsh-tools'
2026-07-21 03:08:35 +08:00
import { defineContentToolFixture , RUN_CODE_NAME } from '@deepseek-ai/dsh-tools'
2026-07-06 23:29:08 +08:00
import { MockAdapter , textResponse , toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
2026-07-07 21:08:12 +08:00
import { startInProcessRun } from '../src/index.ts'
2026-07-06 23:29:08 +08:00
import {
STRUCTURED_OUTPUT_INSTRUCTION ,
STRUCTURED_OUTPUT_TOOL ,
} from '../src/structured.ts'
2026-07-19 23:38:54 +08:00
const testToolSignal = new AbortController ( ) . signal
2026-07-06 23:29:08 +08:00
type Script = ConstructorParameters < typeof MockAdapter > [ 0 ]
2026-07-19 19:19:57 +08:00
async function mountInvariants ( ctx : Context ) : Promise < void > {
await ctx . plugin ( InvariantService )
await ctx . plugin ( SessionInvariant )
await ctx . plugin ( AgentInvariant )
await ctx . plugin ( AgentLoopInvariant )
}
2026-07-11 22:55:26 +08:00
interface CodeRunRequestLike {
bindings : { global : string ; functions : Record < string , ( args : unknown ) = > Promise < unknown > > } [ ]
}
interface SetupOptions {
toolMode? : ToolConfig [ 'mode' ]
codeRun ? : ( request : CodeRunRequestLike ) = > Promise < { logs : never [ ] ; value? : unknown } >
}
2026-07-21 01:11:55 +08:00
const SCHEMA : ObjectJsonSchema = {
2026-07-06 23:29:08 +08:00
type : 'object' ,
properties : { answer : { type : 'number' } , note : { type : 'string' } } ,
required : [ 'answer' ] ,
}
/**
2026-07-13 23:27:00 +08:00
* Real loop, scripted model, and inline fresh-conversation provider over the shared driver. Loading
* spawn/fork here would create a dev-dependency cycle; their specs cover plugin integration while
* this fixture isolates driver behavior and scripts the child's `structured_output` calls.
2026-07-06 23:29:08 +08:00
*/
2026-07-11 22:55:26 +08:00
async function setup ( script : Script , options : SetupOptions = { } ) {
2026-07-06 23:29:08 +08:00
const ctx = new Context ( )
const adapter = new MockAdapter ( script )
2026-07-16 17:39:53 +08:00
await mountAgentLoopTestDependencies ( ctx , {
tools : { mode : options.toolMode ? ? 'native' } ,
} )
2026-07-11 22:55:26 +08:00
if ( options . toolMode === 'code' || options . toolMode === 'both' ) {
ctx . provide ( 'codeRuntime' , {
language : 'typescript' ,
isolation : 'test' ,
run : options.codeRun ? ? ( ( ) = > Promise . resolve ( { logs : [ ] } ) ) ,
} as never )
}
2026-07-19 19:19:57 +08:00
await mountInvariants ( ctx )
2026-07-06 23:29:08 +08:00
await ctx . plugin ( AgentLoop , { agents : [ ] } )
await ctx . plugin ( SubagentService )
2026-07-07 21:08:12 +08:00
const disposeProvider = ctx . subagents . registerProvider ( {
name : 'spawn' ,
2026-07-09 02:10:06 +08:00
capabilities : { outputSchema : true , depthLimit : true , toolFilter : false , persona : false } ,
2026-07-07 21:08:12 +08:00
inheritsParentContext : false ,
2026-07-12 22:41:59 +08:00
start : ( request : SubagentStartRequest ) = > startInProcessRun ( request , { } ) ,
2026-07-07 21:08:12 +08:00
} )
2026-07-06 23:29:08 +08:00
ctx . llm . registerAdapter ( [ 'mock' ] , adapter )
2026-07-18 12:21:15 +08:00
const parent = ctx . agentLoop . create ( SessionId ( 'parent' ) , { provider : 'mock' , model : 'mock' } )
2026-07-07 21:08:12 +08:00
return { ctx , parent , adapter , disposeProvider }
2026-07-06 23:29:08 +08:00
}
function structuredRequest ( parent : SubagentStartRequest [ 'parent' ] , extra? : Partial < SubagentStartRequest > ) : SubagentStartRequest {
2026-07-12 22:41:59 +08:00
return {
prompt : [ { type : 'text' , text : 'produce the answer' } ] ,
parent ,
signal : new AbortController ( ) . signal ,
outputSchema : SCHEMA ,
. . . extra ,
}
2026-07-06 23:29:08 +08:00
}
/** The tool names of one recorded model request. */
function toolNames ( request : GenerateOptions ) : string [ ] {
return ( request . tools ? ? [ ] ) . map ( tool = > tool . name )
}
describe ( 'in-process structured output' , ( ) = > {
it ( 'captures a valid structured_output call and surfaces result.structured' , async ( ) = > {
const { ctx , parent } = await setup ( [
toolCallResponse ( 'c1' , STRUCTURED_OUTPUT_TOOL , { answer : 42 , note : 'done' } ) ,
] )
2026-07-21 03:08:35 +08:00
let acknowledgement : unknown
ctx . on ( 'tools/result' , ( exec , toolResult ) = > {
if ( exec . name === STRUCTURED_OUTPUT_TOOL && ! toolResult . isError ) acknowledgement = toolResult . value
} )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-06 23:29:08 +08:00
const result = await run . result
expect ( result . stopReason ) . toBe ( 'completed' )
expect ( result . structured ) . toEqual ( { answer : 42 , note : 'done' } )
2026-07-21 03:08:35 +08:00
expect ( acknowledgement ) . toEqual ( { recorded : true } )
2026-07-06 23:29:08 +08:00
await run . dispose ( )
} )
it ( 'stops the turn after a successful capture — no extra model step is spent' , async ( ) = > {
const { ctx , parent , adapter } = await setup ( [
toolCallResponse ( 'c1' , STRUCTURED_OUTPUT_TOOL , { answer : 1 } ) ,
textResponse ( 'MUST NOT BE CONSUMED' ) ,
] )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-06 23:29:08 +08:00
await run . result
2026-07-24 21:18:48 +08:00
// The structured tool marks its successful result as turn-concluding.
2026-07-06 23:29:08 +08:00
expect ( adapter . requests . length ) . toBe ( 1 )
await run . dispose ( )
} )
2026-07-07 00:11:55 +08:00
it ( 'denies tool calls that FOLLOW the capture in the same response — terminal means terminal' , async ( ) = > {
// One model response carrying structured_output FIRST and a side-effecting
// call after it: the continuation veto only fires at step end, so without
// the pre-execute deny the trailing call would still run after the final
// answer was accepted.
const response = [
. . . toolCallResponse ( 'c1' , STRUCTURED_OUTPUT_TOOL , { answer : 5 } ) . slice ( 0 , - 2 ) ,
{ type : 'block-start' , index : 1 , blockType : 'tool-call' } ,
{ type : 'block-end' , index : 1 , block : { type : 'tool-call' , id : CallId ( 'c2' ) , name : 'side_effect' , arguments : '{}' } } ,
{ type : 'usage' , usage : { inputTokens : 10 , outputTokens : 5 } } ,
{ type : 'finish' , reason : { kind : 'tool-calls' } } ,
] as Script [ number ]
const { ctx , parent } = await setup ( [ response ] )
let sideEffectRan = false
2026-07-21 03:08:35 +08:00
ctx . tools . register ( defineContentToolFixture ( {
2026-07-07 00:11:55 +08:00
name : 'side_effect' ,
description : 'probe' ,
2026-07-21 03:08:35 +08:00
parameters : { } ,
2026-07-07 00:11:55 +08:00
execute ( ) : Promise < ContentBlock [ ] > {
sideEffectRan = true
return Promise . resolve ( [ { type : 'text' , text : 'ran' } ] )
} ,
2026-07-21 03:08:35 +08:00
} ) )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-07 00:11:55 +08:00
const result = await run . result
expect ( result . stopReason ) . toBe ( 'completed' )
expect ( result . structured ) . toEqual ( { answer : 5 } )
// The deny skipped dispatch entirely: the probe body never ran.
expect ( sideEffectRan ) . toBe ( false )
await run . dispose ( )
} )
2026-07-11 22:55:26 +08:00
it ( 'a later prepended pre-execute listener cannot resurrect dispatch after capture' , async ( ) = > {
const response = [
. . . toolCallResponse ( 'c1' , STRUCTURED_OUTPUT_TOOL , { answer : 5 } ) . slice ( 0 , - 2 ) ,
{ type : 'block-start' , index : 1 , blockType : 'tool-call' } ,
{ type : 'block-end' , index : 1 , block : { type : 'tool-call' , id : CallId ( 'c2' ) , name : 'side_effect' , arguments : '{}' } } ,
{ type : 'usage' , usage : { inputTokens : 10 , outputTokens : 5 } } ,
{ type : 'finish' , reason : { kind : 'tool-calls' } } ,
] as Script [ number ]
const { ctx , parent } = await setup ( [ response ] )
let sideEffectRan = false
2026-07-21 03:08:35 +08:00
ctx . tools . register ( defineContentToolFixture ( {
2026-07-11 22:55:26 +08:00
name : 'side_effect' ,
description : 'probe' ,
2026-07-21 03:08:35 +08:00
parameters : { } ,
2026-07-11 22:55:26 +08:00
execute ( ) : Promise < ContentBlock [ ] > {
sideEffectRan = true
return Promise . resolve ( [ { type : 'text' , text : 'ran' } ] )
} ,
2026-07-21 03:08:35 +08:00
} ) )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-11 22:55:26 +08:00
// Registered after the child and prepended: this listener returns allow
// after every downstream pre-execute decision. The service-owned guard
// runs after the waterfall and can only deny, so the body still cannot run.
ctx . on ( 'tools/pre-execute' , async ( _exec , next ) = > {
await next ( )
return { kind : 'allow' as const }
} , { prepend : true } )
const result = await run . result
expect ( result . structured ) . toEqual ( { answer : 5 } )
expect ( sideEffectRan ) . toBe ( false )
const child = ctx . agents . get ( run . id )
const sideEffectResult = child ? . session . events . find ( event = >
2026-07-28 13:55:59 +08:00
event . type === 'tool/result' && event . data . message . source . callId === 'c2' )
expect ( sideEffectResult ? . type === 'tool/result' && sideEffectResult . data . message . content [ 0 ] . isError ) . toBe ( true )
2026-07-11 22:55:26 +08:00
await run . dispose ( )
} )
2026-07-07 00:11:55 +08:00
it ( 'leaves tool calls that PRECEDE the capture in the same response untouched' , async ( ) = > {
const response = [
{ type : 'block-start' , index : 0 , blockType : 'tool-call' } ,
{ type : 'block-end' , index : 0 , block : { type : 'tool-call' , id : CallId ( 'c1' ) , name : 'side_effect' , arguments : '{}' } } ,
. . . toolCallResponse ( 'c2' , STRUCTURED_OUTPUT_TOOL , { answer : 6 } ) . map ( chunk = >
'index' in chunk ? { . . . chunk , index : 1 } : chunk ) ,
] as Script [ number ]
const { ctx , parent } = await setup ( [ response ] )
let sideEffectRan = false
2026-07-21 03:08:35 +08:00
ctx . tools . register ( defineContentToolFixture ( {
2026-07-07 00:11:55 +08:00
name : 'side_effect' ,
description : 'probe' ,
2026-07-21 03:08:35 +08:00
parameters : { } ,
2026-07-07 00:11:55 +08:00
execute ( ) : Promise < ContentBlock [ ] > {
sideEffectRan = true
return Promise . resolve ( [ { type : 'text' , text : 'ran' } ] )
} ,
2026-07-21 03:08:35 +08:00
} ) )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-07 00:11:55 +08:00
const result = await run . result
// The call ran BEFORE captured was set: the deny gate only guards the
// window after the terminal answer landed.
expect ( sideEffectRan ) . toBe ( true )
expect ( result . structured ) . toEqual ( { answer : 6 } )
await run . dispose ( )
} )
2026-07-06 23:29:08 +08:00
it ( 'an invalid call gets an INVALID_ARGS isError result and the model retries in-turn' , async ( ) = > {
const { ctx , parent } = await setup ( [
toolCallResponse ( 'c1' , STRUCTURED_OUTPUT_TOOL , { answer : 'not-a-number' } ) ,
toolCallResponse ( 'c2' , STRUCTURED_OUTPUT_TOOL , { answer : 7 } ) ,
] )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-06 23:29:08 +08:00
const result = await run . result
expect ( result . structured ) . toEqual ( { answer : 7 } )
expect ( result . stopReason ) . toBe ( 'completed' )
// The child's log carries the isError tool/result for the invalid call.
const child = ctx . agents . get ( run . id ) !
const results = child . session . events . filter ( e = > e . type === 'tool/result' )
expect ( results . length ) . toBe ( 2 )
2026-07-28 14:15:23 +08:00
expect ( results [ 0 ] ! . data . message . content [ 0 ] . isError ) . toBe ( true )
2026-07-06 23:29:08 +08:00
await run . dispose ( )
} )
2026-07-07 09:14:48 +08:00
it ( 'a clean finish without a capture is an immediate error to the parent — deliberately NO re-prompt' , async ( ) = > {
const { ctx , parent , adapter } = await setup ( [
2026-07-06 23:29:08 +08:00
textResponse ( 'here is my answer in prose' ) ,
2026-07-07 09:14:48 +08:00
textResponse ( 'MUST NOT BE CONSUMED' ) ,
2026-07-06 23:29:08 +08:00
] )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-06 23:29:08 +08:00
const result = await run . result
expect ( result . stopReason ) . toBe ( 'error' )
expect ( result . structured ) . toBeUndefined ( )
2026-07-07 09:14:48 +08:00
// Exactly one model request and one user message: no nudge turn exists.
2026-07-06 23:29:08 +08:00
expect ( adapter . requests . length ) . toBe ( 1 )
2026-07-07 09:14:48 +08:00
const child = ctx . agents . get ( run . id ) !
expect ( child . session . events . filter ( e = > e . type === 'user/message' ) . length ) . toBe ( 1 )
2026-07-06 23:29:08 +08:00
await run . dispose ( )
} )
2026-07-07 09:14:48 +08:00
it ( 'an errored child keeps its honest error result (no capture expected)' , async ( ) = > {
2026-07-06 23:29:08 +08:00
// Script exhaustion on the first call → the child turn errors.
2026-07-07 09:14:48 +08:00
const { ctx , parent , adapter } = await setup ( [ ] )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-06 23:29:08 +08:00
const result = await run . result
expect ( result . stopReason ) . toBe ( 'error' )
expect ( adapter . requests . length ) . toBe ( 1 )
await run . dispose ( )
} )
2026-07-07 09:14:48 +08:00
it ( 'a cancel landing after a clean capture-less turn settles aborted, not error' , async ( ) = > {
const { ctx , parent } = await setup ( [ textResponse ( 'prose, no capture' ) ] )
2026-07-12 22:41:59 +08:00
const controller = new AbortController ( )
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent , { signal : controller.signal } ) )
2026-07-07 09:14:48 +08:00
// Cancel synchronously inside the turn's end recording: the cancel
// contract outranks the schema shortfall, so the result maps to aborted.
2026-07-06 23:29:08 +08:00
ctx . on ( 'session/event' , ( session , event ) = > {
2026-07-11 22:55:26 +08:00
const child = ctx . agents . get ( run . id )
2026-07-12 22:41:59 +08:00
if ( session === child ? . session && event . type === 'turn/end' ) controller . abort ( 'cancelled at turn end' )
2026-07-06 23:29:08 +08:00
} )
const result = await run . result
expect ( result . stopReason ) . toBe ( 'aborted' )
await run . dispose ( )
} )
it ( 'rejects a schema outside the subset loud, before any child exists' , async ( ) = > {
const { ctx , parent } = await setup ( [ ] )
2026-07-12 22:41:59 +08:00
await expect ( ctx . subagents . start ( 'spawn' , structuredRequest ( parent , {
2026-07-21 01:11:55 +08:00
outputSchema : { type : 'object' , oneOf : [ ] } as unknown as ObjectJsonSchema ,
} ) ) ) . rejects . toThrow ( /unsupported JSON schema/ )
2026-07-14 01:59:21 +08:00
expect ( ctx . agents . get ( SessionId ( 'parent' ) ) ) . toBeDefined ( )
2026-07-06 23:29:08 +08:00
} )
2026-07-21 01:11:55 +08:00
it ( 'a schema carrying non-JSON values fails as JsonSchemaError at the validation boundary' , async ( ) = > {
2026-07-07 21:08:12 +08:00
const { ctx , parent } = await setup ( [ ] )
2026-07-12 22:41:59 +08:00
// Semantic assertion runs before provider startup.
await expect ( ctx . subagents . start ( 'spawn' , structuredRequest ( parent , {
2026-07-21 01:11:55 +08:00
outputSchema : { type : 'object' , default : ( ) = > { } } as unknown as ObjectJsonSchema ,
} ) ) ) . rejects . toThrow ( /unsupported JSON schema.*annotation must be lossless JSON data/ )
2026-07-07 21:08:12 +08:00
} )
it ( 'a post-execute BLOCK on the capture call denies the capture: log and result agree on failure' , async ( ) = > {
const { ctx , parent , adapter } = await setup ( [
toolCallResponse ( 'c1' , STRUCTURED_OUTPUT_TOOL , { answer : 7 } ) ,
textResponse ( 'continues after the blocked capture' ) ,
] )
2026-07-11 22:55:26 +08:00
// A PostToolUse-style hook turns the tool body's provisional success into
// the authoritative final error observed by the commit notification.
2026-07-07 21:08:12 +08:00
ctx . on ( 'tools/post-execute' , ( exec , _result , next ) = > {
if ( exec . name === STRUCTURED_OUTPUT_TOOL ) {
return Promise . resolve ( { kind : 'block' as const , feedback : [ { type : 'text' as const , text : 'capture rejected by hook' } ] } )
}
return next ( )
} )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-07 21:08:12 +08:00
const result = await run . result
// No capture was committed: the run reports the schema shortfall...
expect ( result . structured ) . toBeUndefined ( )
expect ( result . stopReason ) . toBe ( 'error' )
// ...the logged tool result is the blocked isError with the feedback...
const child = ctx . agents . get ( run . id ) !
const results = child . session . events . filter ( e = > e . type === 'tool/result' )
2026-07-28 13:55:59 +08:00
expect ( results [ 0 ] ! . data . message . content [ 0 ] . isError ) . toBe ( true )
expect ( JSON . stringify ( results [ 0 ] ! . data . message . content ) ) . toContain ( 'capture rejected by hook' )
2026-07-07 21:08:12 +08:00
// ...and the turn CONTINUED past the blocked call (no captured veto):
// the model got to react to the failure with a second step.
expect ( adapter . requests . length ) . toBe ( 2 )
await run . dispose ( )
} )
it ( 'a post-execute accept-with-replacement still commits the capture' , async ( ) = > {
const { ctx , parent } = await setup ( [
toolCallResponse ( 'c1' , STRUCTURED_OUTPUT_TOOL , { answer : 8 } ) ,
] )
ctx . on ( 'tools/post-execute' , ( exec , _result , next ) = > {
if ( exec . name === STRUCTURED_OUTPUT_TOOL ) {
return Promise . resolve ( { kind : 'accept' as const , content : [ { type : 'text' as const , text : 'recorded (rewritten)' } ] } )
}
return next ( )
} )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-07 21:08:12 +08:00
const result = await run . result
expect ( result . stopReason ) . toBe ( 'completed' )
expect ( result . structured ) . toEqual ( { answer : 8 } )
await run . dispose ( )
} )
2026-07-11 22:55:26 +08:00
it ( 'commits only after a later prepended post-execute wrapper returns the authoritative result' , async ( ) = > {
const { ctx , parent } = await setup ( [
toolCallResponse ( 'c1' , STRUCTURED_OUTPUT_TOOL , { answer : 8 } ) ,
textResponse ( 'capture was rejected' ) ,
] )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-11 22:55:26 +08:00
// Registered after attachment and prepended, so it wraps every listener
// the child installed. It delegates first, then converts the apparent
// capture success into the pipeline's authoritative failure.
ctx . on ( 'tools/post-execute' , async ( exec , _result , next ) = > {
const downstream = await next ( )
if ( exec . name !== STRUCTURED_OUTPUT_TOOL ) return downstream
return { kind : 'block' as const , feedback : [ { type : 'text' as const , text : 'rejected after downstream' } ] }
} , { prepend : true } )
const result = await run . result
expect ( result . structured ) . toBeUndefined ( )
expect ( result . stopReason ) . toBe ( 'error' )
const child = ctx . agents . get ( run . id )
const captureResult = child ? . session . events . find ( event = >
2026-07-28 13:55:59 +08:00
event . type === 'tool/result' && event . data . message . source . callId === 'c1' )
expect ( captureResult ? . type === 'tool/result' && captureResult . data . message . content [ 0 ] . isError ) . toBe ( true )
2026-07-11 22:55:26 +08:00
await run . dispose ( )
} )
2026-07-06 23:29:08 +08:00
it ( 'appends the structured instruction to the child REQUEST\'s system text (base prompt preserved)' , async ( ) = > {
const { ctx , parent , adapter } = await setup ( [ toolCallResponse ( 'c1' , STRUCTURED_OUTPUT_TOOL , { answer : 1 } ) ] )
// A context-wide section stands in for the deployment persona: the
2026-07-13 13:09:41 +08:00
// instruction must APPEND to the other scoped and global sections, not
// replace them (AgentOptions has no prompt field — the instruction is an
// ordinary child-scoped prompt registration).
2026-07-06 23:29:08 +08:00
ctx . systemPrompt . section ( { name : 'test:persona' , order : 10 , text : 'You are a counter.' } )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-06 23:29:08 +08:00
await run . result
const childRequest = adapter . requests . at ( - 1 ) !
expect ( childRequest . system ) . toContain ( 'You are a counter.' )
expect ( childRequest . system ! . endsWith ( STRUCTURED_OUTPUT_INSTRUCTION ) ) . toBe ( true )
expect ( childRequest . system ! . indexOf ( STRUCTURED_OUTPUT_INSTRUCTION ) ) . toBeGreaterThan ( 0 )
await run . dispose ( )
} )
2026-07-11 22:55:26 +08:00
it ( 'keeps pure Code Mode at one wire tool and exposes structured capture through the SDK only' , async ( ) = > {
const { ctx , parent , adapter } = await setup ( [
2026-07-26 02:43:34 +08:00
toolCallResponse ( 'c1' , RUN_CODE_NAME , { code : 'return await tools.structured_output({ answer: 12 })' , description : 'Capture the structured answer' } ) ,
2026-07-11 22:55:26 +08:00
] , {
toolMode : 'code' ,
codeRun : async ( request ) = > {
const capture = request . bindings . at ( 0 ) ? . functions [ STRUCTURED_OUTPUT_TOOL ]
if ( ! capture ) throw new Error ( 'structured_output binding missing' )
await capture ( { answer : 12 } )
return { logs : [ ] , value : 'captured' }
} ,
} )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-11 22:55:26 +08:00
const result = await run . result
expect ( result . structured ) . toEqual ( { answer : 12 } )
const request = adapter . requests [ 0 ] !
expect ( toolNames ( request ) ) . toEqual ( [ RUN_CODE_NAME ] )
2026-07-21 04:34:14 +08:00
expect ( request . system ) . toContain ( 'interface ToolArgsMap' )
expect ( request . system ) . toContain ( 'interface ToolOutputMap' )
expect ( request . system ) . toContain ( 'recorded: true;' )
expect ( request . system ) . toContain ( 'Promise<ToolOutputMap[K]>' )
2026-07-11 22:55:26 +08:00
expect ( request . system ) . toContain ( STRUCTURED_OUTPUT_INSTRUCTION )
await run . dispose ( )
} )
it ( 'discards a nested capture when the enclosing run_code execution fails' , async ( ) = > {
const { ctx , parent , adapter } = await setup ( [
2026-07-26 02:43:34 +08:00
toolCallResponse ( 'c1' , RUN_CODE_NAME , { code : 'await tools.structured_output({ answer: 12 }); throw new Error("boom")' , description : 'Capture then fail the program' } ) ,
2026-07-11 22:55:26 +08:00
textResponse ( 'outer code failed' ) ,
] , {
toolMode : 'code' ,
codeRun : async ( request ) = > {
const capture = request . bindings . at ( 0 ) ? . functions [ STRUCTURED_OUTPUT_TOOL ]
if ( ! capture ) throw new Error ( 'structured_output binding missing' )
await capture ( { answer : 12 } )
return {
logs : [ ] ,
error : { kind : 'runtime' , message : 'boom after capture' } ,
} as never
} ,
} )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-11 22:55:26 +08:00
const result = await run . result
expect ( result . structured ) . toBeUndefined ( )
expect ( result . stopReason ) . toBe ( 'error' )
expect ( adapter . requests ) . toHaveLength ( 2 )
const child = ctx . agents . get ( run . id ) !
const outer = child . session . events . find ( event = >
2026-07-28 13:55:59 +08:00
event . type === 'tool/result' && event . data . message . source . callId === CallId ( 'c1' ) )
expect ( outer ? . type === 'tool/result' && outer . data . message . content [ 0 ] . isError ) . toBe ( true )
2026-07-11 22:55:26 +08:00
await run . dispose ( )
} )
it ( 'discards a nested capture when post-policy blocks the enclosing run_code result' , async ( ) = > {
const { ctx , parent , adapter } = await setup ( [
2026-07-26 02:43:34 +08:00
toolCallResponse ( 'c1' , RUN_CODE_NAME , { code : 'return await tools.structured_output({ answer: 12 })' , description : 'Capture the structured answer' } ) ,
2026-07-11 22:55:26 +08:00
textResponse ( 'outer code was blocked' ) ,
] , {
toolMode : 'code' ,
codeRun : async ( request ) = > {
const capture = request . bindings . at ( 0 ) ? . functions [ STRUCTURED_OUTPUT_TOOL ]
if ( ! capture ) throw new Error ( 'structured_output binding missing' )
await capture ( { answer : 12 } )
return { logs : [ ] , value : 'captured' }
} ,
} )
ctx . on ( 'tools/post-execute' , ( exec , _result , next ) = > exec . name === RUN_CODE_NAME
? Promise . resolve ( { kind : 'block' as const , feedback : [ { type : 'text' as const , text : 'outer blocked' } ] } )
: next ( ) )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-11 22:55:26 +08:00
const result = await run . result
expect ( result . structured ) . toBeUndefined ( )
expect ( result . stopReason ) . toBe ( 'error' )
expect ( adapter . requests ) . toHaveLength ( 2 )
await run . dispose ( )
} )
2026-07-06 23:29:08 +08:00
it ( 'the instruction rides ONLY structured requests: appended for the child, absent for a plain agent' , async ( ) = > {
const { ctx , parent , adapter } = await setup ( [
textResponse ( 'parent answer' ) ,
toolCallResponse ( 'c1' , STRUCTURED_OUTPUT_TOOL , { answer : 1 } ) ,
] )
2026-07-28 13:55:59 +08:00
parent . followup ( createUserMessage ( { content : [ { type : 'text' , text : 'hello' } ] , source : { kind : 'user' } } ) )
2026-07-06 23:29:08 +08:00
await parent . whenIdle ( )
expect ( adapter . requests [ 0 ] ! . system ? ? '' ) . not . toContain ( STRUCTURED_OUTPUT_INSTRUCTION )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-06 23:29:08 +08:00
await run . result
// The loop always assembles a base prompt (the harness identity section),
// so the instruction APPENDS — never replaces.
const childSystem = adapter . requests . at ( - 1 ) ! . system !
expect ( childSystem . endsWith ( STRUCTURED_OUTPUT_INSTRUCTION ) ) . toBe ( true )
expect ( childSystem . length ) . toBeGreaterThan ( STRUCTURED_OUTPUT_INSTRUCTION . length )
await run . dispose ( )
} )
2026-07-09 02:10:06 +08:00
describe ( 'scoped registration (each child owns its capture tool)' , ( ) = > {
it ( 'a plain agent never sees the tool: nothing is registered globally at all' , async ( ) = > {
2026-07-07 21:08:12 +08:00
const { ctx , parent , adapter } = await setup ( [ textResponse ( 'parent answer' ) ] )
2026-07-28 13:55:59 +08:00
parent . followup ( createUserMessage ( { content : [ { type : 'text' , text : 'hello' } ] , source : { kind : 'user' } } ) )
2026-07-07 21:08:12 +08:00
await parent . whenIdle ( )
2026-07-09 02:10:06 +08:00
// Scoped registration: the global view has no capture tool, ever.
expect ( ctx . tools . get ( STRUCTURED_OUTPUT_TOOL ) ) . toBeUndefined ( )
2026-07-07 21:08:12 +08:00
expect ( toolNames ( adapter . requests [ 0 ] ! ) ) . not . toContain ( STRUCTURED_OUTPUT_TOOL )
} )
2026-07-06 23:29:08 +08:00
it ( 'a structured child sees structured_output with ITS schema; a plain agent never sees the tool' , async ( ) = > {
const { ctx , parent , adapter } = await setup ( [
// Parent turn (a plain agent): must NOT see the tool.
textResponse ( 'parent answer' ) ,
// Child turn: must see it, with the run's schema.
toolCallResponse ( 'c1' , STRUCTURED_OUTPUT_TOOL , { answer : 42 } ) ,
] )
2026-07-28 13:55:59 +08:00
parent . followup ( createUserMessage ( { content : [ { type : 'text' , text : 'hello' } ] , source : { kind : 'user' } } ) )
2026-07-06 23:29:08 +08:00
await parent . whenIdle ( )
expect ( toolNames ( adapter . requests [ 0 ] ! ) ) . not . toContain ( STRUCTURED_OUTPUT_TOOL )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-06 23:29:08 +08:00
await run . result
const childRequest = adapter . requests [ 1 ] !
expect ( toolNames ( childRequest ) ) . toContain ( STRUCTURED_OUTPUT_TOOL )
const entry = childRequest . tools ! . find ( tool = > tool . name === STRUCTURED_OUTPUT_TOOL ) !
expect ( entry . parameters ) . toEqual ( SCHEMA )
await run . dispose ( )
} )
it ( 'two concurrent structured children each see their OWN schema' , async ( ) = > {
2026-07-21 01:11:55 +08:00
const otherSchema : ObjectJsonSchema = {
2026-07-06 23:29:08 +08:00
type : 'object' ,
properties : { verdict : { type : 'string' , enum : [ 'real' , 'bogus' ] } } ,
required : [ 'verdict' ] ,
}
const { ctx , parent , adapter } = await setup ( [
( options : GenerateOptions ) = > {
// Answer with whatever schema this child was given — proves each
// request carried the right one regardless of scheduling order.
const entry = options . tools ! . find ( tool = > tool . name === STRUCTURED_OUTPUT_TOOL ) !
const args = 'verdict' in ( entry . parameters . properties as Record < string , unknown > )
? { verdict : 'real' }
: { answer : 1 }
return toolCallResponse ( 'c1' , STRUCTURED_OUTPUT_TOOL , args )
} ,
( options : GenerateOptions ) = > {
const entry = options . tools ! . find ( tool = > tool . name === STRUCTURED_OUTPUT_TOOL ) !
const args = 'verdict' in ( entry . parameters . properties as Record < string , unknown > )
? { verdict : 'real' }
: { answer : 1 }
return toolCallResponse ( 'c2' , STRUCTURED_OUTPUT_TOOL , args )
} ,
] )
2026-07-12 22:41:59 +08:00
const runA = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
const runB = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent , { outputSchema : otherSchema } ) )
2026-07-06 23:29:08 +08:00
const [ a , b ] = await Promise . all ( [ runA . result , runB . result ] )
expect ( a . structured ) . toEqual ( { answer : 1 } )
expect ( b . structured ) . toEqual ( { verdict : 'real' } )
const schemas = adapter . requests . map ( request = >
request . tools ! . find ( tool = > tool . name === STRUCTURED_OUTPUT_TOOL ) ! . parameters )
expect ( schemas ) . toContainEqual ( SCHEMA )
expect ( schemas ) . toContainEqual ( otherSchema )
await runA . dispose ( )
await runB . dispose ( )
} )
2026-07-13 13:09:41 +08:00
it ( 'places the capture tool and instruction in their canonical orders' , async ( ) = > {
2026-07-09 12:27:08 +08:00
const { ctx , parent , adapter } = await setup ( [
toolCallResponse ( 'c1' , STRUCTURED_OUTPUT_TOOL , { answer : 7 } ) ,
] )
2026-07-13 13:09:41 +08:00
// A global tool sorts lexicographically after structured_output, while a
// global section above the 190 band follows the capture instruction.
2026-07-21 03:08:35 +08:00
ctx . tools . register ( defineContentToolFixture ( {
2026-07-09 12:27:08 +08:00
name : 'zz_probe' ,
description : 'probe' ,
2026-07-21 03:08:35 +08:00
parameters : { } ,
2026-07-09 12:27:08 +08:00
execute : ( ) = > Promise . resolve ( [ { type : 'text' , text : 'x' } ] ) ,
2026-07-21 03:08:35 +08:00
} ) )
2026-07-09 12:27:08 +08:00
ctx . systemPrompt . section ( { name : 'after-band' , order : 200 , text : 'AFTER-BAND' } )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-09 12:27:08 +08:00
await run . result
const request = adapter . requests [ 0 ] !
const names = toolNames ( request )
expect ( names . indexOf ( STRUCTURED_OUTPUT_TOOL ) ) . toBeGreaterThanOrEqual ( 0 )
expect ( names . indexOf ( STRUCTURED_OUTPUT_TOOL ) ) . toBeLessThan ( names . indexOf ( 'zz_probe' ) )
const system = request . system ? ? ''
const instructionAt = system . indexOf ( STRUCTURED_OUTPUT_INSTRUCTION )
expect ( instructionAt ) . toBeGreaterThanOrEqual ( 0 )
expect ( system . indexOf ( 'AFTER-BAND' ) ) . toBeGreaterThan ( instructionAt )
await run . dispose ( )
} )
2026-07-06 23:29:08 +08:00
it ( 'a non-structured agent request keeps tools ABSENT when it had none (no tools: [] materialized)' , async ( ) = > {
2026-07-09 02:10:06 +08:00
const { parent , adapter } = await setup ( [ textResponse ( 'plain' ) ] )
2026-07-28 13:55:59 +08:00
parent . followup ( createUserMessage ( { content : [ { type : 'text' , text : 'q' } ] , source : { kind : 'user' } } ) )
2026-07-06 23:29:08 +08:00
await parent . whenIdle ( )
const request = adapter . requests [ 0 ] !
2026-07-09 02:10:06 +08:00
expect ( request . tools ) . toBeUndefined ( )
2026-07-06 23:29:08 +08:00
await new Promise ( resolve = > setTimeout ( resolve , 0 ) )
} )
2026-07-09 02:10:06 +08:00
it ( 'registrations ride the child fiber: disposing the run removes them; a provider reload mid-run cannot' , async ( ) = > {
const { ctx , parent , disposeProvider } = await setup ( [
2026-07-07 21:08:12 +08:00
toolCallResponse ( 'c1' , STRUCTURED_OUTPUT_TOOL , { answer : 4 } ) ,
] )
2026-07-06 23:29:08 +08:00
expect ( ctx . tools . get ( STRUCTURED_OUTPUT_TOOL ) ) . toBeUndefined ( )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-09 02:10:06 +08:00
// A backend hot-reload mid-run must not unregister the capture tool out
// from under the live child: the registration rides the CHILD's fiber.
2026-07-13 11:58:55 +08:00
disposeProvider ( )
2026-07-06 23:29:08 +08:00
const result = await run . result
2026-07-07 21:08:12 +08:00
expect ( result . structured ) . toEqual ( { answer : 4 } )
2026-07-09 02:10:06 +08:00
const child = ctx . agents . get ( run . id ) !
expect ( ctx . tools . get ( STRUCTURED_OUTPUT_TOOL , child ) ) . toBeDefined ( )
2026-07-06 23:29:08 +08:00
await run . dispose ( )
2026-07-09 02:10:06 +08:00
// Child disposed ⇒ its scoped registrations are gone.
expect ( ctx . tools . get ( STRUCTURED_OUTPUT_TOOL , child ) ) . toBeUndefined ( )
2026-07-06 23:29:08 +08:00
} )
} )
2026-07-09 02:10:06 +08:00
it ( 'a structured_output call from an agent WITHOUT a structured run is UNKNOWN_TOOL (the tool does not exist for it)' , async ( ) = > {
2026-07-06 23:29:08 +08:00
const { ctx , parent } = await setup ( [ ] )
const result = await ctx . tools . execute ( {
2026-07-19 23:38:54 +08:00
signal : testToolSignal ,
2026-07-06 23:29:08 +08:00
callId : 'x' as never ,
name : STRUCTURED_OUTPUT_TOOL ,
arguments : { answer : 1 } ,
agent : parent ,
} )
expect ( result . isError ) . toBe ( true )
2026-07-21 03:08:35 +08:00
expect ( result . error ? . info ? . code ) . toBe ( 'UNKNOWN_TOOL' )
2026-07-06 23:29:08 +08:00
} )
2026-07-09 02:10:06 +08:00
it ( 'a structured_output call with NO calling agent at all is UNKNOWN_TOOL' , async ( ) = > {
2026-07-06 23:29:08 +08:00
const { ctx } = await setup ( [ ] )
const result = await ctx . tools . execute ( {
2026-07-19 23:38:54 +08:00
signal : testToolSignal ,
2026-07-06 23:29:08 +08:00
callId : 'x' as never ,
name : STRUCTURED_OUTPUT_TOOL ,
arguments : { answer : 1 } ,
} )
expect ( result . isError ) . toBe ( true )
2026-07-21 03:08:35 +08:00
expect ( result . error ? . info ? . code ) . toBe ( 'UNKNOWN_TOOL' )
2026-07-06 23:29:08 +08:00
} )
2026-07-09 03:41:37 +08:00
2026-07-11 22:55:26 +08:00
it ( 'a failed execution stage is discarded and never promoted by a later call' , async ( ) = > {
2026-07-09 03:41:37 +08:00
const { ctx , parent } = await setup ( [
toolCallResponse ( 'c1' , STRUCTURED_OUTPUT_TOOL , { answer : 1 } ) ,
] )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-11 22:55:26 +08:00
// A prepended post-execute listener blocks the first capture without
// delegating. The final-result notification discards that execution's
// stage when it observes the error.
2026-07-09 03:41:37 +08:00
let blocks = 1
ctx . on ( 'tools/post-execute' , ( exec , _result , next ) = > {
if ( exec . name === STRUCTURED_OUTPUT_TOOL && blocks > 0 ) {
blocks -= 1
return Promise . resolve ( { kind : 'block' as const , feedback : [ { type : 'text' as const , text : 'rejected' } ] } )
}
return next ( )
} , { prepend : true } )
const result = await run . result
2026-07-11 22:55:26 +08:00
const child = ctx . agents . get ( run . id ) !
2026-07-09 03:41:37 +08:00
// The blocked capture must NOT surface as structured success…
expect ( result . stopReason ) . toBe ( 'error' )
expect ( result . structured ) . toBeUndefined ( )
// …and a LATER invalid call (its own body staged nothing) must not
2026-07-11 22:55:26 +08:00
// resurrect c1's discarded value: drive the pipeline directly.
2026-07-09 03:41:37 +08:00
const invalid = await ctx . tools . execute ( {
2026-07-19 23:38:54 +08:00
signal : testToolSignal ,
2026-07-09 03:41:37 +08:00
callId : 'c2' as never ,
name : STRUCTURED_OUTPUT_TOOL ,
arguments : { answer : 'not-a-number' } ,
agent : child ,
} )
expect ( invalid . isError ) . toBe ( true )
// A fresh valid call still captures ITS OWN value.
const valid = await ctx . tools . execute ( {
2026-07-19 23:38:54 +08:00
signal : testToolSignal ,
2026-07-09 03:41:37 +08:00
callId : 'c3' as never ,
name : STRUCTURED_OUTPUT_TOOL ,
arguments : { answer : 9 } ,
agent : child ,
} )
expect ( valid . isError ) . toBeFalsy ( )
await run . dispose ( )
} )
2026-07-09 04:48:52 +08:00
2026-07-11 22:55:26 +08:00
it ( 'reusing a failed execution\'s call id never promotes its discarded stage' , async ( ) = > {
2026-07-09 04:48:52 +08:00
const { ctx , parent } = await setup ( [
toolCallResponse ( 'c1' , STRUCTURED_OUTPUT_TOOL , { answer : 1 } ) ,
] )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-11 22:55:26 +08:00
// Block the first capture after its body stages a value. Its final error
// discards that execution's stage.
2026-07-09 04:48:52 +08:00
let blocks = 1
ctx . on ( 'tools/post-execute' , ( exec , _result , next ) = > {
if ( exec . name === STRUCTURED_OUTPUT_TOOL && blocks > 0 ) {
blocks -= 1
return Promise . resolve ( { kind : 'block' as const , feedback : [ { type : 'text' as const , text : 'rejected' } ] } )
}
return next ( )
} , { prepend : true } )
await run . result
2026-07-11 22:55:26 +08:00
const child = ctx . agents . get ( run . id ) !
2026-07-09 04:48:52 +08:00
// A SECOND capture call with the SAME call id whose body never stages
2026-07-11 22:55:26 +08:00
// (invalid args throw before the stage): the discarded value must not ride
2026-07-09 04:48:52 +08:00
// its acceptance.
const reused = await ctx . tools . execute ( {
2026-07-19 23:38:54 +08:00
signal : testToolSignal ,
2026-07-09 04:48:52 +08:00
callId : 'c1' as never ,
name : STRUCTURED_OUTPUT_TOOL ,
arguments : { answer : 'not-a-number' } ,
agent : child ,
} )
expect ( reused . isError ) . toBe ( true )
// Nothing was ever committed: a fresh valid call is still required.
const valid = await ctx . tools . execute ( {
2026-07-19 23:38:54 +08:00
signal : testToolSignal ,
2026-07-09 04:48:52 +08:00
callId : 'c1' as never ,
name : STRUCTURED_OUTPUT_TOOL ,
arguments : { answer : 5 } ,
agent : child ,
} )
expect ( valid . isError ) . toBeFalsy ( )
await run . dispose ( )
} )
2026-07-09 05:03:44 +08:00
2026-07-11 22:55:26 +08:00
it ( 'a pre-execute deny with call-id reuse cannot promote another execution\'s stage' , async ( ) = > {
2026-07-09 05:03:44 +08:00
const { ctx , parent } = await setup ( [
toolCallResponse ( 'c1' , STRUCTURED_OUTPUT_TOOL , { answer : 1 } ) ,
] )
2026-07-12 22:41:59 +08:00
const run = await ctx . subagents . start ( 'spawn' , structuredRequest ( parent ) )
2026-07-11 22:55:26 +08:00
// Discard the first capture's stage via a final post-execute block.
2026-07-09 05:03:44 +08:00
let blocks = 1
ctx . on ( 'tools/post-execute' , ( exec , _result , next ) = > {
if ( exec . name === STRUCTURED_OUTPUT_TOOL && blocks > 0 ) {
blocks -= 1
return Promise . resolve ( { kind : 'block' as const , feedback : [ { type : 'text' as const , text : 'rejected' } ] } )
}
return next ( )
} , { prepend : true } )
await run . result
2026-07-11 22:55:26 +08:00
const child = ctx . agents . get ( run . id ) !
// A prepended pre-execute deny skips the body, while the denied call still
// reaches the final notification with the same adapter-minted call id.
2026-07-09 05:03:44 +08:00
const offDeny = ctx . on ( 'tools/pre-execute' , ( exec ) = > {
if ( exec . name === STRUCTURED_OUTPUT_TOOL ) {
return Promise . resolve ( { kind : 'deny' as const , reason : 'outer veto' } )
}
return undefined as never
} , { prepend : true } )
const denied = await ctx . tools . execute ( {
2026-07-19 23:38:54 +08:00
signal : testToolSignal ,
2026-07-09 05:03:44 +08:00
callId : 'c1' as never ,
name : STRUCTURED_OUTPUT_TOOL ,
arguments : { answer : 2 } ,
agent : child ,
} )
expect ( denied . isError ) . toBe ( true )
offDeny ( )
2026-07-11 22:55:26 +08:00
// The discarded value was never promoted: a fresh valid call is required
2026-07-09 05:03:44 +08:00
// (and succeeds, proving the runtime is not wedged).
const valid = await ctx . tools . execute ( {
2026-07-19 23:38:54 +08:00
signal : testToolSignal ,
2026-07-09 05:03:44 +08:00
callId : 'c1' as never ,
name : STRUCTURED_OUTPUT_TOOL ,
arguments : { answer : 5 } ,
agent : child ,
} )
expect ( valid . isError ) . toBeFalsy ( )
await run . dispose ( )
} )
2026-07-06 23:29:08 +08:00
} )