@@ -5,6 +5,8 @@ import { toolResultContains } from "./tool-result"
55
66const SUBTASK_PARENT_MARKER = "SUBTASK_PARENT_CANCELLATION_SMOKE"
77const SUBTASK_CHILD_MARKER = "SUBTASK_CHILD_CALCULATOR_SMOKE"
8+ const SUBTASK_INTERRUPT_PARENT_MARKER = "SUBTASK_PARENT_INTERRUPT_RESUME"
9+ const SUBTASK_INTERRUPT_CHILD_MARKER = "SUBTASK_CHILD_INTERRUPT_RESUME"
810const SUBTASK_FAST_PARENT_MARKER = "SUBTASK_PARENT_IMMEDIATE_COMPLETION"
911const SUBTASK_FAST_CHILD_MARKER = "SUBTASK_CHILD_IMMEDIATE_COMPLETION"
1012const SUBTASK_XPROFILE_PARENT_MARKER = "SUBTASK_PARENT_CROSS_PROFILE"
@@ -17,6 +19,11 @@ export const SUBTASK_CHILD_FOLLOWUP_ANSWER = "9"
1719const SUBTASK_FAST_CHILD_PROMPT = `${ SUBTASK_FAST_CHILD_MARKER } : Complete immediately with the exact result "Fast child completed".`
1820export const SUBTASK_FAST_PARENT_PROMPT = `${ SUBTASK_FAST_PARENT_MARKER } : Use the new_task tool exactly once. Create an ask-mode subtask with this exact message: "${ SUBTASK_FAST_CHILD_PROMPT } " Do not answer directly.`
1921
22+ const SUBTASK_INTERRUPT_CHILD_PROMPT = `${ SUBTASK_INTERRUPT_CHILD_MARKER } : Ask the user exactly this follow-up question: What is the square root of 81? After the user answers, complete with only the answer.`
23+ export const SUBTASK_INTERRUPT_PARENT_PROMPT = `${ SUBTASK_INTERRUPT_PARENT_MARKER } : Use the new_task tool exactly once. Create an ask-mode subtask with this exact message: "${ SUBTASK_INTERRUPT_CHILD_PROMPT } " Do not answer directly. When the subtask returns, complete with the exact result "Interrupted parent resumed".`
24+ export const SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER = "9"
25+ export const SUBTASK_INTERRUPT_PARENT_RESULT = "Interrupted parent resumed"
26+
2027const SUBTASK_XPROFILE_SAME_CHILD_PROMPT = `${ SUBTASK_XPROFILE_SAME_CHILD_MARKER } : Complete immediately with the exact result "Same-profile child completed".`
2128const SUBTASK_XPROFILE_DIFFERENT_CHILD_PROMPT = `${ SUBTASK_XPROFILE_DIFFERENT_CHILD_MARKER } : Complete immediately with the exact result "Different-profile child completed".`
2229export const SUBTASK_XPROFILE_PARENT_PROMPT = `${ SUBTASK_XPROFILE_PARENT_MARKER } : First use new_task to create a code-mode subtask with this exact message: "${ SUBTASK_XPROFILE_SAME_CHILD_PROMPT } " After it returns, create an ask-mode subtask with the next instructions you receive.`
@@ -32,15 +39,17 @@ const requestContains = (req: ChatCompletionRequest, expected: string[]) => {
3239const completionAfterAnswer = ( followupId : string , completionId : string ) => ( {
3340 match : {
3441 predicate : ( req : ChatCompletionRequest ) =>
42+ ! requestContains ( req , [ SUBTASK_INTERRUPT_CHILD_MARKER ] ) &&
43+ ! requestContains ( req , [ SUBTASK_INTERRUPT_PARENT_MARKER ] ) &&
3544 // Preferred: structured tool-result message carries the followup answer.
36- toolResultContains ( req , followupId , [ SUBTASK_CHILD_FOLLOWUP_ANSWER ] ) ||
37- // Fallback 1: answer present alongside the tool-call ID but not in a role:tool message.
38- requestContains ( req , [ followupId , SUBTASK_CHILD_FOLLOWUP_ANSWER ] ) ||
39- // Fallback 2: answer arrives as a bare user message after task resume (no tool-call ID context).
40- requestContains ( req , [
41- SUBTASK_CHILD_MARKER ,
42- `<user_message>\\n${ SUBTASK_CHILD_FOLLOWUP_ANSWER } \\n</user_message>` ,
43- ] ) ,
45+ ( toolResultContains ( req , followupId , [ SUBTASK_CHILD_FOLLOWUP_ANSWER ] ) ||
46+ // Fallback 1: answer present alongside the tool-call ID but not in a role:tool message.
47+ requestContains ( req , [ followupId , SUBTASK_CHILD_FOLLOWUP_ANSWER ] ) ||
48+ // Fallback 2: answer arrives as a bare user message after task resume (no tool-call ID context).
49+ requestContains ( req , [
50+ SUBTASK_CHILD_MARKER ,
51+ `<user_message>\\n${ SUBTASK_CHILD_FOLLOWUP_ANSWER } \\n</user_message>` ,
52+ ] ) ) ,
4453 } ,
4554 response : {
4655 toolCalls : [
@@ -90,7 +99,8 @@ export function addSubtaskFixtures(mock: InstanceType<typeof LLMock>) {
9099
91100 mock . addFixture ( {
92101 match : {
93- toolCallId : "call_subtasks_fast_parent_new_task_001" ,
102+ predicate : ( req : ChatCompletionRequest ) =>
103+ requestContains ( req , [ SUBTASK_FAST_PARENT_MARKER , "call_subtasks_fast_parent_new_task_001" ] ) ,
94104 } ,
95105 response : {
96106 toolCalls : [
@@ -143,7 +153,8 @@ export function addSubtaskFixtures(mock: InstanceType<typeof LLMock>) {
143153
144154 mock . addFixture ( {
145155 match : {
146- toolCallId : "call_subtasks_parent_new_task_001" ,
156+ predicate : ( req : ChatCompletionRequest ) =>
157+ requestContains ( req , [ SUBTASK_PARENT_MARKER , "call_subtasks_parent_new_task_001" ] ) ,
147158 } ,
148159 response : {
149160 toolCalls : [
@@ -252,4 +263,95 @@ export function addSubtaskFixtures(mock: InstanceType<typeof LLMock>) {
252263 ] ,
253264 } ,
254265 } )
266+
267+ // Interrupted-child-resumes-and-reports-back scenario (#560)
268+ mock . addFixture ( {
269+ match : {
270+ userMessage : new RegExp ( SUBTASK_INTERRUPT_PARENT_MARKER ) ,
271+ sequenceIndex : 0 ,
272+ } ,
273+ response : {
274+ toolCalls : [
275+ {
276+ name : "new_task" ,
277+ arguments : JSON . stringify ( {
278+ mode : "ask" ,
279+ message : SUBTASK_INTERRUPT_CHILD_PROMPT ,
280+ } ) ,
281+ id : "call_interrupt_parent_new_task_001" ,
282+ } ,
283+ ] ,
284+ } ,
285+ } )
286+
287+ // The parent prompt embeds SUBTASK_INTERRUPT_CHILD_MARKER verbatim, so parent-resume turns
288+ // also match a bare substring check. Exclude the parent marker so they fall through.
289+ // The answer exclusion must use the <user_message> wrapping: the bare answer is a single
290+ // digit that can appear anywhere in the serialized request (timestamps in environment
291+ // details, token counts), which would make this fixture unmatchable.
292+ mock . addFixture ( {
293+ match : {
294+ predicate : ( req : ChatCompletionRequest ) =>
295+ requestContains ( req , [ SUBTASK_INTERRUPT_CHILD_MARKER ] ) &&
296+ ! requestContains ( req , [ SUBTASK_INTERRUPT_PARENT_MARKER ] ) &&
297+ ! requestContains ( req , [ "call_interrupt_child_followup_001" ] ) &&
298+ ! requestContains ( req , [
299+ `<user_message>\\n${ SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER } \\n</user_message>` ,
300+ ] ) ,
301+ } ,
302+ response : {
303+ toolCalls : [
304+ {
305+ name : "ask_followup_question" ,
306+ arguments : JSON . stringify ( {
307+ question : "What is the square root of 81?" ,
308+ follow_up : [ { text : SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER } ] ,
309+ } ) ,
310+ id : "call_interrupt_child_followup_001" ,
311+ } ,
312+ ] ,
313+ } ,
314+ } )
315+
316+ mock . addFixture ( {
317+ match : {
318+ predicate : ( req : ChatCompletionRequest ) =>
319+ // Preferred: structured tool-result message carries the followup answer.
320+ toolResultContains ( req , "call_interrupt_child_followup_001" , [
321+ SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER ,
322+ ] ) ||
323+ // Fallback 1: answer present alongside the tool-call ID.
324+ requestContains ( req , [ "call_interrupt_child_followup_001" , SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER ] ) ||
325+ // Fallback 2: answer arrives as a bare user message after task resume.
326+ requestContains ( req , [
327+ SUBTASK_INTERRUPT_CHILD_MARKER ,
328+ `<user_message>\\n${ SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER } \\n</user_message>` ,
329+ ] ) ,
330+ } ,
331+ response : {
332+ toolCalls : [
333+ {
334+ name : "attempt_completion" ,
335+ arguments : JSON . stringify ( { result : SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER } ) ,
336+ id : "call_interrupt_child_completion_002" ,
337+ } ,
338+ ] ,
339+ } ,
340+ } )
341+
342+ mock . addFixture ( {
343+ match : {
344+ predicate : ( req : ChatCompletionRequest ) =>
345+ requestContains ( req , [ SUBTASK_INTERRUPT_PARENT_MARKER , "call_interrupt_parent_new_task_001" ] ) ,
346+ } ,
347+ response : {
348+ toolCalls : [
349+ {
350+ name : "attempt_completion" ,
351+ arguments : JSON . stringify ( { result : SUBTASK_INTERRUPT_PARENT_RESULT } ) ,
352+ id : "call_interrupt_parent_completion_003" ,
353+ } ,
354+ ] ,
355+ } ,
356+ } )
255357}
0 commit comments