@@ -10,8 +10,11 @@ import {
1010 MessagesSnapshotEvent ,
1111 RunFinishedEvent ,
1212 RunStartedEvent ,
13+ TextMessageStartEvent ,
14+ TextMessageContentEvent ,
15+ TextMessageEndEvent ,
1316} from "@ag-ui/core" ;
14- import { Observable , of } from "rxjs" ;
17+ import { Observable , of , Subject } from "rxjs" ;
1518
1619// Mock uuid module
1720jest . mock ( "uuid" , ( ) => ( {
@@ -59,6 +62,21 @@ class TestAgent extends AbstractAgent {
5962 }
6063}
6164
65+ class StreamingTestAgent extends AbstractAgent {
66+ private eventSubject ?: Subject < BaseEvent > ;
67+
68+ setEventSubject ( subject : Subject < BaseEvent > ) {
69+ this . eventSubject = subject ;
70+ }
71+
72+ run ( input : RunAgentInput ) : Observable < BaseEvent > {
73+ if ( ! this . eventSubject ) {
74+ throw new Error ( "eventSubject not set" ) ;
75+ }
76+ return this . eventSubject . asObservable ( ) ;
77+ }
78+ }
79+
6280describe ( "Agent Result" , ( ) => {
6381 let agent : TestAgent ;
6482
@@ -582,4 +600,150 @@ describe("Agent Result", () => {
582600 expect ( result . result ) . toMatchObject ( complexResult ) ;
583601 } ) ;
584602 } ) ;
603+
604+ describe ( "run detachment" , ( ) => {
605+ let streamingAgent : StreamingTestAgent ;
606+
607+ beforeEach ( ( ) => {
608+ streamingAgent = new StreamingTestAgent ( { threadId : "thread-detach" } ) ;
609+ } ) ;
610+
611+ it ( "finalizes immediately when detached" , async ( ) => {
612+ const subject = new Subject < BaseEvent > ( ) ;
613+ streamingAgent . setEventSubject ( subject ) ;
614+ const onRunFinalized = jest . fn ( ) ;
615+
616+ const runPromise = streamingAgent . runAgent ( { } , { onRunFinalized } ) ;
617+ await waitForAsyncNotifications ( ) ;
618+
619+ subject . next ( {
620+ type : EventType . RUN_STARTED ,
621+ threadId : "thread-detach" ,
622+ runId : "run-detach" ,
623+ } as RunStartedEvent ) ;
624+
625+ await streamingAgent . detachActiveRun ( ) ;
626+ await runPromise ;
627+ subject . complete ( ) ;
628+
629+ expect ( onRunFinalized ) . toHaveBeenCalledTimes ( 1 ) ;
630+ } ) ;
631+
632+ it ( "ignores events emitted after detaching" , async ( ) => {
633+ const subject = new Subject < BaseEvent > ( ) ;
634+ streamingAgent . setEventSubject ( subject ) ;
635+ const onMessagesChanged = jest . fn ( ) ;
636+
637+ const runPromise = streamingAgent . runAgent ( { } , { onMessagesChanged } ) ;
638+ await waitForAsyncNotifications ( ) ;
639+ const initialMessageCount = streamingAgent . messages . length ;
640+
641+ subject . next ( {
642+ type : EventType . RUN_STARTED ,
643+ threadId : "thread-detach" ,
644+ runId : "run-detach" ,
645+ } as RunStartedEvent ) ;
646+
647+ const detachPromise = streamingAgent . detachActiveRun ( ) ;
648+
649+ subject . next ( {
650+ type : EventType . TEXT_MESSAGE_START ,
651+ messageId : "msg-after-detach" ,
652+ role : "assistant" ,
653+ } as TextMessageStartEvent ) ;
654+ subject . next ( {
655+ type : EventType . TEXT_MESSAGE_CONTENT ,
656+ messageId : "msg-after-detach" ,
657+ delta : "Should be ignored" ,
658+ } as TextMessageContentEvent ) ;
659+ subject . next ( {
660+ type : EventType . TEXT_MESSAGE_END ,
661+ messageId : "msg-after-detach" ,
662+ } as TextMessageEndEvent ) ;
663+
664+ subject . complete ( ) ;
665+ await Promise . all ( [ detachPromise , runPromise ] ) ;
666+
667+ expect ( streamingAgent . messages . length ) . toBe ( initialMessageCount ) ;
668+ expect ( onMessagesChanged ) . not . toHaveBeenCalled ( ) ;
669+ } ) ;
670+
671+ it ( "can start a new run on another thread after detaching" , async ( ) => {
672+ const firstSubject = new Subject < BaseEvent > ( ) ;
673+ streamingAgent . setEventSubject ( firstSubject ) ;
674+
675+ const firstRunPromise = streamingAgent . runAgent ( ) ;
676+ await waitForAsyncNotifications ( ) ;
677+
678+ firstSubject . next ( {
679+ type : EventType . RUN_STARTED ,
680+ threadId : "thread-detach" ,
681+ runId : "run-1" ,
682+ } as RunStartedEvent ) ;
683+
684+ await streamingAgent . detachActiveRun ( ) ;
685+ firstSubject . complete ( ) ;
686+ await firstRunPromise ;
687+
688+ streamingAgent . threadId = "thread-detach-2" ;
689+ const secondSubject = new Subject < BaseEvent > ( ) ;
690+ streamingAgent . setEventSubject ( secondSubject ) ;
691+
692+ const secondRunPromise = streamingAgent . runAgent ( ) ;
693+ await waitForAsyncNotifications ( ) ;
694+
695+ secondSubject . next ( {
696+ type : EventType . RUN_STARTED ,
697+ threadId : "thread-detach-2" ,
698+ runId : "run-2" ,
699+ } as RunStartedEvent ) ;
700+ secondSubject . next ( {
701+ type : EventType . TEXT_MESSAGE_START ,
702+ messageId : "msg-new" ,
703+ role : "assistant" ,
704+ } as TextMessageStartEvent ) ;
705+ secondSubject . next ( {
706+ type : EventType . TEXT_MESSAGE_CONTENT ,
707+ messageId : "msg-new" ,
708+ delta : "hello" ,
709+ } as TextMessageContentEvent ) ;
710+ secondSubject . next ( {
711+ type : EventType . TEXT_MESSAGE_END ,
712+ messageId : "msg-new" ,
713+ } as TextMessageEndEvent ) ;
714+ secondSubject . next ( {
715+ type : EventType . RUN_FINISHED ,
716+ threadId : "thread-detach-2" ,
717+ runId : "run-2" ,
718+ } as RunFinishedEvent ) ;
719+ secondSubject . complete ( ) ;
720+
721+ await secondRunPromise ;
722+ await waitForAsyncNotifications ( ) ;
723+
724+ expect ( streamingAgent . messages . some ( ( message ) => message . id === "msg-new" ) ) . toBe ( true ) ;
725+ } ) ;
726+
727+ it ( "resolve order: detachActiveRun waits for finalize" , async ( ) => {
728+ const subject = new Subject < BaseEvent > ( ) ;
729+ streamingAgent . setEventSubject ( subject ) ;
730+ const order : string [ ] = [ ] ;
731+
732+ const runPromise = streamingAgent . runAgent ( { } , { onRunFinalized : ( ) => order . push ( "finalized" ) } ) ;
733+ await waitForAsyncNotifications ( ) ;
734+
735+ subject . next ( {
736+ type : EventType . RUN_STARTED ,
737+ threadId : "thread-detach" ,
738+ runId : "run-order" ,
739+ } as RunStartedEvent ) ;
740+
741+ const detachPromise = streamingAgent . detachActiveRun ( ) . then ( ( ) => order . push ( "awaited" ) ) ;
742+ subject . complete ( ) ;
743+
744+ await Promise . all ( [ runPromise , detachPromise ] ) ;
745+
746+ expect ( order ) . toEqual ( [ "finalized" , "awaited" ] ) ;
747+ } ) ;
748+ } ) ;
585749} ) ;
0 commit comments