@@ -7,6 +7,8 @@ const SUBTASK_PARENT_MARKER = "SUBTASK_PARENT_CANCELLATION_SMOKE"
77const SUBTASK_CHILD_MARKER = "SUBTASK_CHILD_CALCULATOR_SMOKE"
88const SUBTASK_INTERRUPT_PARENT_MARKER = "SUBTASK_PARENT_INTERRUPT_RESUME"
99const SUBTASK_INTERRUPT_CHILD_MARKER = "SUBTASK_CHILD_INTERRUPT_RESUME"
10+ export const SUBTASK_API_HANG_PARENT_MARKER = "SUBTASK_PARENT_API_HANG_INTERRUPT_RESUME"
11+ export const SUBTASK_API_HANG_CHILD_MARKER = "SUBTASK_CHILD_API_HANG_INTERRUPT_RESUME"
1012const SUBTASK_FAST_PARENT_MARKER = "SUBTASK_PARENT_IMMEDIATE_COMPLETION"
1113const SUBTASK_FAST_CHILD_MARKER = "SUBTASK_CHILD_IMMEDIATE_COMPLETION"
1214const SUBTASK_XPROFILE_PARENT_MARKER = "SUBTASK_PARENT_CROSS_PROFILE"
@@ -24,13 +26,29 @@ export const SUBTASK_INTERRUPT_PARENT_PROMPT = `${SUBTASK_INTERRUPT_PARENT_MARKE
2426export const SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER = "9"
2527export const SUBTASK_INTERRUPT_PARENT_RESULT = "Interrupted parent resumed"
2628
29+ const SUBTASK_API_HANG_CHILD_PROMPT = `${ SUBTASK_API_HANG_CHILD_MARKER } : Complete with the exact result "Hung child completed".`
30+ export const SUBTASK_API_HANG_PARENT_PROMPT = `${ SUBTASK_API_HANG_PARENT_MARKER } : Use the new_task tool exactly once. Create an ask-mode subtask with this exact message: "${ SUBTASK_API_HANG_CHILD_PROMPT } " Do not answer directly. When the subtask returns, complete with the exact result "API hang parent resumed".`
31+ export const SUBTASK_API_HANG_RESUME_MESSAGE = "Continue after provider hang."
32+ export const SUBTASK_API_HANG_CHILD_RESULT = "Hung child completed"
33+ export const SUBTASK_API_HANG_PARENT_RESULT = "API hang parent resumed"
34+
35+ // Abandon-subtask scenario (#559) — separate markers to avoid sequenceIndex collisions with the
36+ // interrupted-child-resumes tests above, which exhaust the sequence count for INTERRUPT markers.
37+ const SUBTASK_ABANDON_PARENT_MARKER = "SUBTASK_PARENT_ABANDON_SEVER"
38+ const SUBTASK_ABANDON_CHILD_MARKER = "SUBTASK_CHILD_ABANDON_SEVER"
39+ const SUBTASK_ABANDON_CHILD_PROMPT = `${ SUBTASK_ABANDON_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.`
40+ export const SUBTASK_ABANDON_PARENT_PROMPT = `${ SUBTASK_ABANDON_PARENT_MARKER } : Use the new_task tool exactly once. Create an ask-mode subtask with this exact message: "${ SUBTASK_ABANDON_CHILD_PROMPT } " Do not answer directly.`
41+ export const SUBTASK_ABANDON_CHILD_FOLLOWUP_ANSWER = "9"
42+
2743const SUBTASK_XPROFILE_SAME_CHILD_PROMPT = `${ SUBTASK_XPROFILE_SAME_CHILD_MARKER } : Complete immediately with the exact result "Same-profile child completed".`
2844const SUBTASK_XPROFILE_DIFFERENT_CHILD_PROMPT = `${ SUBTASK_XPROFILE_DIFFERENT_CHILD_MARKER } : Complete immediately with the exact result "Different-profile child completed".`
2945export 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.`
3046export const SUBTASK_XPROFILE_SAME_CHILD_RESULT = "Same-profile child completed"
3147export const SUBTASK_XPROFILE_DIFFERENT_CHILD_RESULT = "Different-profile child completed"
3248export const SUBTASK_XPROFILE_PARENT_RESULT = "Sequential cross-profile parent resumed"
3349
50+ const apiHangChildMatch = new RegExp ( SUBTASK_API_HANG_CHILD_MARKER )
51+
3452const requestContains = ( req : ChatCompletionRequest , expected : string [ ] ) => {
3553 const rawRequest = JSON . stringify ( req )
3654 return expected . every ( ( text ) => rawRequest . includes ( text ) )
@@ -167,6 +185,79 @@ export function addSubtaskFixtures(mock: InstanceType<typeof LLMock>) {
167185 } ,
168186 } )
169187
188+ mock . addFixture ( {
189+ match : {
190+ userMessage : new RegExp ( SUBTASK_API_HANG_PARENT_MARKER ) ,
191+ sequenceIndex : 0 ,
192+ } ,
193+ response : {
194+ toolCalls : [
195+ {
196+ name : "new_task" ,
197+ arguments : JSON . stringify ( {
198+ mode : "ask" ,
199+ message : SUBTASK_API_HANG_CHILD_PROMPT ,
200+ } ) ,
201+ id : "call_api_hang_parent_new_task_001" ,
202+ } ,
203+ ] ,
204+ } ,
205+ } )
206+
207+ mock . addFixture ( {
208+ match : {
209+ userMessage : apiHangChildMatch ,
210+ sequenceIndex : 0 ,
211+ } ,
212+ // Keep the first child response pending long enough for the e2e test to cancel an in-flight API request.
213+ latency : 15_000 ,
214+ response : {
215+ toolCalls : [
216+ {
217+ name : "attempt_completion" ,
218+ arguments : JSON . stringify ( { result : SUBTASK_API_HANG_CHILD_RESULT } ) ,
219+ id : "call_api_hang_child_completion_002" ,
220+ } ,
221+ ] ,
222+ } ,
223+ } )
224+
225+ mock . addFixture ( {
226+ match : {
227+ userMessage : apiHangChildMatch ,
228+ sequenceIndex : 1 ,
229+ } ,
230+ response : {
231+ toolCalls : [
232+ {
233+ name : "attempt_completion" ,
234+ arguments : JSON . stringify ( { result : SUBTASK_API_HANG_CHILD_RESULT } ) ,
235+ id : "call_api_hang_child_completion_003" ,
236+ } ,
237+ ] ,
238+ } ,
239+ } )
240+
241+ mock . addFixture ( {
242+ match : {
243+ predicate : ( req : ChatCompletionRequest ) =>
244+ requestContains ( req , [
245+ SUBTASK_API_HANG_PARENT_MARKER ,
246+ "call_api_hang_parent_new_task_001" ,
247+ SUBTASK_API_HANG_CHILD_RESULT ,
248+ ] ) ,
249+ } ,
250+ response : {
251+ toolCalls : [
252+ {
253+ name : "attempt_completion" ,
254+ arguments : JSON . stringify ( { result : SUBTASK_API_HANG_PARENT_RESULT } ) ,
255+ id : "call_api_hang_parent_completion_004" ,
256+ } ,
257+ ] ,
258+ } ,
259+ } )
260+
170261 // Issue #457 sequence: a same-profile child returns first, then the resumed
171262 // parent delegates to a child whose mode uses a different API profile.
172263 mock . addFixture ( {
@@ -354,4 +445,67 @@ export function addSubtaskFixtures(mock: InstanceType<typeof LLMock>) {
354445 ] ,
355446 } ,
356447 } )
448+
449+ // Abandon-subtask scenario (#559)
450+ mock . addFixture ( {
451+ match : {
452+ userMessage : new RegExp ( SUBTASK_ABANDON_PARENT_MARKER ) ,
453+ sequenceIndex : 0 ,
454+ } ,
455+ response : {
456+ toolCalls : [
457+ {
458+ name : "new_task" ,
459+ arguments : JSON . stringify ( {
460+ mode : "ask" ,
461+ message : SUBTASK_ABANDON_CHILD_PROMPT ,
462+ } ) ,
463+ id : "call_abandon_parent_new_task_001" ,
464+ } ,
465+ ] ,
466+ } ,
467+ } )
468+
469+ mock . addFixture ( {
470+ match : {
471+ predicate : ( req : ChatCompletionRequest ) =>
472+ requestContains ( req , [ SUBTASK_ABANDON_CHILD_MARKER ] ) &&
473+ ! requestContains ( req , [ SUBTASK_ABANDON_PARENT_MARKER ] ) &&
474+ ! requestContains ( req , [ "call_abandon_child_followup_001" ] ) &&
475+ ! requestContains ( req , [ `<user_message>\\n${ SUBTASK_ABANDON_CHILD_FOLLOWUP_ANSWER } \\n</user_message>` ] ) ,
476+ } ,
477+ response : {
478+ toolCalls : [
479+ {
480+ name : "ask_followup_question" ,
481+ arguments : JSON . stringify ( {
482+ question : "What is the square root of 81?" ,
483+ follow_up : [ { text : SUBTASK_ABANDON_CHILD_FOLLOWUP_ANSWER } ] ,
484+ } ) ,
485+ id : "call_abandon_child_followup_001" ,
486+ } ,
487+ ] ,
488+ } ,
489+ } )
490+
491+ mock . addFixture ( {
492+ match : {
493+ predicate : ( req : ChatCompletionRequest ) =>
494+ toolResultContains ( req , "call_abandon_child_followup_001" , [ SUBTASK_ABANDON_CHILD_FOLLOWUP_ANSWER ] ) ||
495+ requestContains ( req , [ "call_abandon_child_followup_001" , SUBTASK_ABANDON_CHILD_FOLLOWUP_ANSWER ] ) ||
496+ requestContains ( req , [
497+ SUBTASK_ABANDON_CHILD_MARKER ,
498+ `<user_message>\\n${ SUBTASK_ABANDON_CHILD_FOLLOWUP_ANSWER } \\n</user_message>` ,
499+ ] ) ,
500+ } ,
501+ response : {
502+ toolCalls : [
503+ {
504+ name : "attempt_completion" ,
505+ arguments : JSON . stringify ( { result : SUBTASK_ABANDON_CHILD_FOLLOWUP_ANSWER } ) ,
506+ id : "call_abandon_child_completion_002" ,
507+ } ,
508+ ] ,
509+ } ,
510+ } )
357511}
0 commit comments