@@ -254,4 +254,78 @@ describe("Human-in-the-Loop (HITL) Integration Tests", () => {
254254 expect ( result . messages . length ) . toBeGreaterThan ( 0 ) ;
255255 } ,
256256 ) ;
257+
258+ it . concurrent (
259+ "should properly propagate HITL interrupts from subagents without TypeError" ,
260+ { timeout : 120000 } ,
261+ async ( ) => {
262+ // This test specifically verifies the fix for the issue where
263+ // GraphInterrupt.interrupts was undefined when propagating from subagents,
264+ // causing "Cannot read properties of undefined (reading 'length')" error
265+
266+ const checkpointer = new MemorySaver ( ) ;
267+ const agent = createDeepAgent ( {
268+ tools : [ sampleTool ] ,
269+ interruptOn : { sample_tool : true } ,
270+ checkpointer,
271+ } ) ;
272+
273+ const config = { configurable : { thread_id : uuidv4 ( ) } } ;
274+
275+ // Invoke with a task that will use the subagent which has HITL
276+ // The subagent should interrupt, and this interrupt should propagate
277+ // properly to the parent graph without causing a TypeError
278+ const result = await agent . invoke (
279+ {
280+ messages : [
281+ new HumanMessage (
282+ "Use the task tool with the general-purpose subagent to call the sample_tool" ,
283+ ) ,
284+ ] ,
285+ } ,
286+ config ,
287+ ) ;
288+
289+ // Verify the agent called the task tool
290+ const aiMessages = result . messages . filter ( ( msg : any ) =>
291+ AIMessage . isInstance ( msg ) ,
292+ ) ;
293+ const toolCalls = aiMessages . flatMap ( ( msg : any ) => msg . tool_calls || [ ] ) ;
294+ expect ( toolCalls . some ( ( tc : any ) => tc . name === "task" ) ) . toBe ( true ) ;
295+
296+ // Verify interrupt was properly propagated from the subagent
297+ expect ( result . __interrupt__ ) . toBeDefined ( ) ;
298+ expect ( result . __interrupt__ ) . toHaveLength ( 1 ) ;
299+
300+ // Verify the interrupt has the correct HITL structure
301+ const interrupt = result . __interrupt__ ?. [ 0 ] ;
302+ expect ( interrupt ) . toBeDefined ( ) ;
303+ expect ( interrupt ! . value ) . toBeDefined ( ) ;
304+
305+ const hitlRequest = interrupt ! . value as HITLRequest ;
306+ expect ( hitlRequest . actionRequests ) . toBeDefined ( ) ;
307+ expect ( hitlRequest . actionRequests . length ) . toBeGreaterThan ( 0 ) ;
308+ expect ( hitlRequest . reviewConfigs ) . toBeDefined ( ) ;
309+ expect ( hitlRequest . reviewConfigs . length ) . toBeGreaterThan ( 0 ) ;
310+
311+ // Verify we can resume successfully
312+ const resumeResult = await agent . invoke (
313+ new Command ( {
314+ resume : {
315+ decisions : [ { type : "approve" } ] ,
316+ } ,
317+ } ) ,
318+ config ,
319+ ) ;
320+
321+ // After resume, there should be no more interrupts
322+ expect ( resumeResult . __interrupt__ ) . toBeUndefined ( ) ;
323+
324+ // The tool should have been executed
325+ const toolMessages = resumeResult . messages . filter (
326+ ( msg : any ) => msg . _getType ( ) === "tool" ,
327+ ) ;
328+ expect ( toolMessages . length ) . toBeGreaterThan ( 0 ) ;
329+ } ,
330+ ) ;
257331} ) ;
0 commit comments