@@ -32,6 +32,7 @@ import {
3232 OrchestrationV2ThreadLaunchError ,
3333 type OrchestrationProjectShell ,
3434 type OrchestrationV2ShellSnapshot ,
35+ type OrchestrationV2ThreadProjection ,
3536 type ProjectEntriesFailure ,
3637 type ProjectFileFailure ,
3738 type ProjectFileOperation ,
@@ -95,6 +96,10 @@ import {
9596 projectDomainEventForWire ,
9697 projectThreadProjectionForWire ,
9798} from "./orchestration-v2/WireProjection.ts" ;
99+ import {
100+ mergeSubagentTreeProjection ,
101+ routeSubagentTreeEvent ,
102+ } from "./orchestration-v2/SubagentTreeProjection.ts" ;
98103import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts" ;
99104import * as OrchestrationEventStore from "./persistence/Services/OrchestrationEventStore.ts" ;
100105import { userFacingDispatchErrorMessage } from "./orchestration-v2/UserFacingErrors.ts" ;
@@ -624,27 +629,61 @@ const makeWsRpcLayer = (
624629 ) ,
625630 ) ;
626631
627- const eventStreamFrom = ( afterSequence : number ) =>
628- threadManagement
629- . streamStoredEventsFrom ( {
630- threadId : input . threadId ,
631- afterSequence,
632- } )
633- . pipe (
634- Stream . map ( ( stored ) => ( {
635- kind : "event" as const ,
636- sequence : stored . sequence ,
637- event : projectDomainEventForWire ( stored . event ) ,
638- } ) ) ,
639- Stream . mapError (
640- ( cause ) =>
641- new OrchestrationV2GetThreadProjectionError ( {
642- threadId : input . threadId ,
643- message : `Failed while streaming orchestration V2 thread ${ input . threadId } ` ,
644- cause,
645- } ) ,
632+ const loadSubagentTree = Effect . fn ( "ws.orchestrationV2.loadSubagentTree" ) ( function * (
633+ root : OrchestrationV2ThreadProjection ,
634+ ) {
635+ const threadIds = new Set < ThreadId > ( [ root . thread . id ] ) ;
636+ const descendants : OrchestrationV2ThreadProjection [ ] = [ ] ;
637+ let frontier = root . subagents . flatMap ( ( subagent ) =>
638+ subagent . childThreadId === null ? [ ] : [ subagent . childThreadId ] ,
639+ ) ;
640+
641+ while ( frontier . length > 0 ) {
642+ const nextIds = [ ...new Set ( frontier ) ] . filter ( ( threadId ) => ! threadIds . has ( threadId ) ) ;
643+ if ( nextIds . length === 0 ) break ;
644+ for ( const threadId of nextIds ) threadIds . add ( threadId ) ;
645+ const projections = yield * Effect . forEach (
646+ nextIds ,
647+ ( threadId ) => threadManagement . getThreadProjection ( threadId ) ,
648+ { concurrency : 8 } ,
649+ ) ;
650+ descendants . push ( ...projections ) ;
651+ frontier = projections . flatMap ( ( projection ) =>
652+ projection . subagents . flatMap ( ( subagent ) =>
653+ subagent . childThreadId === null ? [ ] : [ subagent . childThreadId ] ,
646654 ) ,
647655 ) ;
656+ }
657+
658+ return {
659+ threadIds,
660+ projection : mergeSubagentTreeProjection ( root , descendants ) ,
661+ } ;
662+ } ) ;
663+
664+ const eventStreamFrom = (
665+ afterSequence : number ,
666+ initialThreadIds : ReadonlySet < ThreadId > ,
667+ ) =>
668+ applicationEvents . streamApplicationEvents ( { afterSequence } ) . pipe (
669+ Stream . mapAccum (
670+ ( ) => ( { rootThreadId : input . threadId , threadIds : initialThreadIds } ) ,
671+ routeSubagentTreeEvent ,
672+ ) ,
673+ Stream . map ( ( stored ) => ( {
674+ kind : "event" as const ,
675+ sequence : stored . sequence ,
676+ event : projectDomainEventForWire ( stored . event ) ,
677+ } ) ) ,
678+ Stream . mapError (
679+ ( cause ) =>
680+ new OrchestrationV2GetThreadProjectionError ( {
681+ threadId : input . threadId ,
682+ message : `Failed while streaming orchestration V2 thread ${ input . threadId } ` ,
683+ cause,
684+ } ) ,
685+ ) ,
686+ ) ;
648687
649688 const loadReplayThrough = ( afterSequence : number , throughSequence : number ) =>
650689 applicationEvents
@@ -679,6 +718,16 @@ const makeWsRpcLayer = (
679718
680719 const snapshotThenLive = Effect . fn ( "ws.orchestrationV2.threadSnapshotThenLive" ) (
681720 function * ( ) {
721+ const snapshotSequence = yield * applicationEvents . latestApplicationSequence . pipe (
722+ Effect . mapError (
723+ ( cause ) =>
724+ new OrchestrationV2GetThreadProjectionError ( {
725+ threadId : input . threadId ,
726+ message : `Failed to prepare orchestration V2 thread ${ input . threadId } snapshot` ,
727+ cause,
728+ } ) ,
729+ ) ,
730+ ) ;
682731 const snapshot = yield * threadManagement . getThreadSnapshot ( input . threadId ) . pipe (
683732 Effect . mapError (
684733 ( cause ) =>
@@ -689,8 +738,17 @@ const makeWsRpcLayer = (
689738 } ) ,
690739 ) ,
691740 ) ;
692- const { snapshotSequence } = snapshot ;
693- const projection = projectThreadProjectionForWire ( snapshot . projection ) ;
741+ const tree = yield * loadSubagentTree ( snapshot . projection ) . pipe (
742+ Effect . mapError (
743+ ( cause ) =>
744+ new OrchestrationV2GetThreadProjectionError ( {
745+ threadId : input . threadId ,
746+ message : `Failed to load nested agents for orchestration V2 thread ${ input . threadId } ` ,
747+ cause,
748+ } ) ,
749+ ) ,
750+ ) ;
751+ const projection = projectThreadProjectionForWire ( tree . projection ) ;
694752 return Stream . concat (
695753 Stream . concat (
696754 Stream . make ( {
@@ -700,7 +758,7 @@ const makeWsRpcLayer = (
700758 } ) ,
701759 completionMarker ,
702760 ) ,
703- eventStreamFrom ( snapshotSequence ) ,
761+ eventStreamFrom ( snapshotSequence , tree . threadIds ) ,
704762 ) ;
705763 } ,
706764 ) ;
@@ -713,7 +771,7 @@ const makeWsRpcLayer = (
713771 // published during the replay window is lost; overlapping events are
714772 // deduped by sequence on the client.
715773 if ( input . afterSequence !== undefined ) {
716- const highWater = yield * applicationEvents . latestAgentSequence ( input . threadId ) . pipe (
774+ const highWater = yield * applicationEvents . latestApplicationSequence . pipe (
717775 Effect . mapError (
718776 ( cause ) =>
719777 new OrchestrationV2GetThreadProjectionError ( {
@@ -723,6 +781,41 @@ const makeWsRpcLayer = (
723781 } ) ,
724782 ) ,
725783 ) ;
784+ const currentProjection = yield * threadManagement
785+ . getThreadProjection ( input . threadId )
786+ . pipe (
787+ Effect . mapError (
788+ ( cause ) =>
789+ new OrchestrationV2GetThreadProjectionError ( {
790+ threadId : input . threadId ,
791+ message : `Failed to inspect nested agents for orchestration V2 thread ${ input . threadId } ` ,
792+ cause,
793+ } ) ,
794+ ) ,
795+ ) ;
796+ const currentTree = yield * loadSubagentTree ( currentProjection ) . pipe (
797+ Effect . mapError (
798+ ( cause ) =>
799+ new OrchestrationV2GetThreadProjectionError ( {
800+ threadId : input . threadId ,
801+ message : `Failed to inspect nested agents for orchestration V2 thread ${ input . threadId } ` ,
802+ cause,
803+ } ) ,
804+ ) ,
805+ ) ;
806+ if ( currentTree . threadIds . size > 1 ) {
807+ return Stream . concat (
808+ Stream . concat (
809+ Stream . make ( {
810+ kind : "snapshot" as const ,
811+ snapshotSequence : highWater ,
812+ projection : projectThreadProjectionForWire ( currentTree . projection ) ,
813+ } ) ,
814+ completionMarker ,
815+ ) ,
816+ eventStreamFrom ( highWater , currentTree . threadIds ) ,
817+ ) ;
818+ }
726819 const replay = yield * loadReplayThrough ( input . afterSequence , highWater ) ;
727820 const plan = decideThreadResume ( {
728821 afterSequence : input . afterSequence ,
@@ -735,7 +828,7 @@ const makeWsRpcLayer = (
735828 }
736829 return Stream . concat (
737830 Stream . concat ( Stream . fromIterable ( replay ) , completionMarker ) ,
738- eventStreamFrom ( highWater ) ,
831+ eventStreamFrom ( highWater , currentTree . threadIds ) ,
739832 ) ;
740833 }
741834
0 commit comments