@@ -327,6 +327,136 @@ describe("useHumanInTheLoop E2E - HITL Tool Rendering", () => {
327327 } ) ;
328328 } ) ;
329329
330+ describe ( "Multiple Hook Instances" , ( ) => {
331+ it ( "should isolate state across two useHumanInTheLoop registrations" , async ( ) => {
332+ const agent = new MockStepwiseAgent ( ) ;
333+
334+ const DualHookComponent : React . FC = ( ) => {
335+ const primaryTool : ReactHumanInTheLoop < { action : string } > = {
336+ name : "primaryTool" ,
337+ description : "Primary approval tool" ,
338+ parameters : z . object ( { action : z . string ( ) } ) ,
339+ render : ( { status, args, respond, result } ) => (
340+ < div data-testid = "primary-tool" >
341+ < div data-testid = "primary-status" > { status } </ div >
342+ < div data-testid = "primary-action" > { args . action ?? "" } </ div >
343+ { respond && (
344+ < button
345+ data-testid = "primary-respond"
346+ onClick = { ( ) => respond ( JSON . stringify ( { approved : true } ) ) }
347+ >
348+ Respond Primary
349+ </ button >
350+ ) }
351+ { result && < div data-testid = "primary-result" > { result } </ div > }
352+ </ div >
353+ ) ,
354+ } ;
355+
356+ const secondaryTool : ReactHumanInTheLoop < { detail : string } > = {
357+ name : "secondaryTool" ,
358+ description : "Secondary approval tool" ,
359+ parameters : z . object ( { detail : z . string ( ) } ) ,
360+ render : ( { status, args, respond, result } ) => (
361+ < div data-testid = "secondary-tool" >
362+ < div data-testid = "secondary-status" > { status } </ div >
363+ < div data-testid = "secondary-detail" > { args . detail ?? "" } </ div >
364+ { respond && (
365+ < button
366+ data-testid = "secondary-respond"
367+ onClick = { ( ) => respond ( JSON . stringify ( { confirmed : true } ) ) }
368+ >
369+ Respond Secondary
370+ </ button >
371+ ) }
372+ { result && < div data-testid = "secondary-result" > { result } </ div > }
373+ </ div >
374+ ) ,
375+ } ;
376+
377+ useHumanInTheLoop ( primaryTool ) ;
378+ useHumanInTheLoop ( secondaryTool ) ;
379+ return null ;
380+ } ;
381+
382+ renderWithCopilotKit ( {
383+ agent,
384+ children : (
385+ < >
386+ < DualHookComponent />
387+ < div style = { { height : 400 } } >
388+ < CopilotChat />
389+ </ div >
390+ </ >
391+ ) ,
392+ } ) ;
393+
394+ const input = await screen . findByRole ( "textbox" ) ;
395+ fireEvent . change ( input , { target : { value : "Dual hook instance" } } ) ;
396+ fireEvent . keyDown ( input , { key : "Enter" , code : "Enter" } ) ;
397+
398+ await waitFor ( ( ) => {
399+ expect ( screen . getByText ( "Dual hook instance" ) ) . toBeDefined ( ) ;
400+ } ) ;
401+
402+ const messageId = testId ( "msg" ) ;
403+ const primaryToolCallId = testId ( "tc-primary" ) ;
404+ const secondaryToolCallId = testId ( "tc-secondary" ) ;
405+
406+ agent . emit ( runStartedEvent ( ) ) ;
407+ agent . emit (
408+ toolCallChunkEvent ( {
409+ toolCallId : primaryToolCallId ,
410+ toolCallName : "primaryTool" ,
411+ parentMessageId : messageId ,
412+ delta : JSON . stringify ( { action : "archive" } ) ,
413+ } )
414+ ) ;
415+ agent . emit (
416+ toolCallChunkEvent ( {
417+ toolCallId : secondaryToolCallId ,
418+ toolCallName : "secondaryTool" ,
419+ parentMessageId : messageId ,
420+ delta : JSON . stringify ( { detail : "requires confirmation" } ) ,
421+ } )
422+ ) ;
423+
424+ await waitFor ( ( ) => {
425+ expect ( screen . getByTestId ( "primary-status" ) . textContent ) . toBe ( ToolCallStatus . InProgress ) ;
426+ expect ( screen . getByTestId ( "primary-action" ) . textContent ) . toBe ( "archive" ) ;
427+ expect ( screen . getByTestId ( "secondary-status" ) . textContent ) . toBe ( ToolCallStatus . InProgress ) ;
428+ expect ( screen . getByTestId ( "secondary-detail" ) . textContent ) . toBe ( "requires confirmation" ) ;
429+ } ) ;
430+
431+ agent . emit ( runFinishedEvent ( ) ) ;
432+ agent . complete ( ) ;
433+
434+ const primaryRespondButton = await screen . findByTestId ( "primary-respond" ) ;
435+
436+ expect ( screen . getByTestId ( "primary-status" ) . textContent ) . toBe ( ToolCallStatus . Executing ) ;
437+ expect ( screen . getByTestId ( "secondary-status" ) . textContent ) . toBe ( ToolCallStatus . InProgress ) ;
438+ expect ( screen . queryByTestId ( "secondary-respond" ) ) . toBeNull ( ) ;
439+
440+ fireEvent . click ( primaryRespondButton ) ;
441+
442+ await waitFor ( ( ) => {
443+ expect ( screen . getByTestId ( "primary-status" ) . textContent ) . toBe ( ToolCallStatus . Complete ) ;
444+ expect ( screen . getByTestId ( "primary-result" ) . textContent ) . toContain ( "approved" ) ;
445+ expect ( screen . getByTestId ( "secondary-status" ) . textContent ) . toBe ( ToolCallStatus . Executing ) ;
446+ expect ( screen . queryByTestId ( "secondary-result" ) ) . toBeNull ( ) ;
447+ } ) ;
448+
449+ const secondaryRespondButton = await screen . findByTestId ( "secondary-respond" ) ;
450+
451+ fireEvent . click ( secondaryRespondButton ) ;
452+
453+ await waitFor ( ( ) => {
454+ expect ( screen . getByTestId ( "secondary-status" ) . textContent ) . toBe ( ToolCallStatus . Complete ) ;
455+ expect ( screen . getByTestId ( "secondary-result" ) . textContent ) . toContain ( "confirmed" ) ;
456+ } ) ;
457+ } ) ;
458+ } ) ;
459+
330460 describe ( "HITL Tool with Dynamic Registration" , ( ) => {
331461 it ( "should support dynamic registration and unregistration of HITL tools" , async ( ) => {
332462 const agent = new MockStepwiseAgent ( ) ;
0 commit comments