@@ -129,6 +129,7 @@ beforeEach(() => {
129129 fullStream : ( async function * ( ) { yield { type : 'text' , text : DEFAULT_RESULT . text } ; } ) ( ) ,
130130 text : Promise . resolve ( DEFAULT_RESULT . text ) ,
131131 usage : Promise . resolve ( DEFAULT_RESULT . usage ) ,
132+ agentCalls : Promise . resolve ( DEFAULT_RESULT . agentCalls ) ,
132133 } ) ;
133134} ) ;
134135
@@ -597,7 +598,7 @@ describe('Agency Full Integration', () => {
597598 // stream()
598599 // ---------------------------------------------------------------------------
599600
600- it ( 'stream() delegates to the compiled strategy' , ( ) => {
601+ it ( 'stream() delegates to the compiled strategy' , async ( ) => {
601602 const team = agency ( {
602603 agents : { a : mockAgentConfig ( 'a' ) } ,
603604 strategy : 'sequential' ,
@@ -608,11 +609,35 @@ describe('Agency Full Integration', () => {
608609 textStream : AsyncIterable < string > ;
609610 } ;
610611
612+ await streamResult . text ;
613+
611614 // The mock strategy stream is invoked and returns a valid object.
612615 expect ( streamResult ) . toBeDefined ( ) ;
613616 expect ( hoisted . strategyStream ) . toHaveBeenCalledWith ( 'stream test' , undefined ) ;
614617 } ) ;
615618
619+ it ( 'stream() appends the structured-output schema hint before streaming' , async ( ) => {
620+ hoisted . strategyStream . mockClear ( ) ;
621+
622+ const schema = {
623+ parse : ( v : unknown ) => v ,
624+ shape : { score : { } } ,
625+ } ;
626+
627+ const team = agency ( {
628+ agents : { a : mockAgentConfig ( 'a' ) } ,
629+ strategy : 'sequential' ,
630+ output : schema ,
631+ } ) ;
632+
633+ const streamResult = team . stream ( 'Return JSON' ) as { text : Promise < string > } ;
634+ await streamResult . text ;
635+
636+ const calledPrompt = hoisted . strategyStream . mock . calls [ 0 ] [ 0 ] as string ;
637+ expect ( calledPrompt ) . toContain ( 'Respond with valid JSON' ) ;
638+ expect ( calledPrompt ) . toContain ( 'score' ) ;
639+ } ) ;
640+
616641 it ( 'stream textStream is iterable' , async ( ) => {
617642 const team = agency ( {
618643 agents : { a : mockAgentConfig ( 'a' ) } ,
@@ -630,6 +655,68 @@ describe('Agency Full Integration', () => {
630655 expect ( chunks [ 0 ] ) . toBe ( DEFAULT_RESULT . text ) ;
631656 } ) ;
632657
658+ it ( 'stream() applies beforeReturn HITL modifications to the resolved text' , async ( ) => {
659+ const approvalRequested = vi . fn ( ) ;
660+ const approvalDecided = vi . fn ( ) ;
661+ const agentEnd = vi . fn ( ) ;
662+
663+ const team = agency ( {
664+ agents : { a : mockAgentConfig ( 'a' ) } ,
665+ strategy : 'sequential' ,
666+ hitl : {
667+ approvals : { beforeReturn : true } ,
668+ handler : async ( ) => ( {
669+ approved : true ,
670+ modifications : { output : 'approved stream output' } ,
671+ } ) ,
672+ } ,
673+ on : { approvalRequested, approvalDecided, agentEnd } ,
674+ } ) ;
675+
676+ const streamResult = team . stream ( 'stream approval' ) as { text : Promise < string > } ;
677+
678+ await expect ( streamResult . text ) . resolves . toBe ( 'approved stream output' ) ;
679+ expect ( approvalRequested ) . toHaveBeenCalledOnce ( ) ;
680+ expect ( approvalDecided ) . toHaveBeenCalledOnce ( ) ;
681+ expect ( agentEnd ) . toHaveBeenCalledWith (
682+ expect . objectContaining ( { output : 'approved stream output' } ) ,
683+ ) ;
684+ } ) ;
685+
686+ it ( 'stream() exposes parsed output and aggregates usage once' , async ( ) => {
687+ const jsonResult = '{"score":42}' ;
688+ hoisted . strategyStream . mockReturnValueOnce ( {
689+ textStream : ( async function * ( ) { yield jsonResult ; } ) ( ) ,
690+ fullStream : ( async function * ( ) { yield { type : 'text' , text : jsonResult } ; } ) ( ) ,
691+ text : Promise . resolve ( jsonResult ) ,
692+ usage : Promise . resolve ( DEFAULT_USAGE ) ,
693+ } ) ;
694+
695+ const schema = {
696+ parse : ( v : unknown ) => v ,
697+ shape : { score : { } } ,
698+ } ;
699+
700+ const team = agency ( {
701+ agents : { a : mockAgentConfig ( 'a' ) } ,
702+ strategy : 'sequential' ,
703+ output : schema ,
704+ } ) ;
705+
706+ const streamResult = team . stream ( 'stream structured' ) as unknown as {
707+ text : Promise < string > ;
708+ usage : Promise < Record < string , unknown > > ;
709+ parsed : Promise < unknown > ;
710+ } ;
711+
712+ await expect ( streamResult . text ) . resolves . toBe ( jsonResult ) ;
713+ await expect ( streamResult . parsed ) . resolves . toEqual ( { score : 42 } ) ;
714+ await expect ( streamResult . usage ) . resolves . toEqual ( DEFAULT_USAGE ) ;
715+
716+ const usage = await team . usage ( ) as Record < string , unknown > ;
717+ expect ( usage . totalTokens ) . toBe ( DEFAULT_USAGE . totalTokens ) ;
718+ } ) ;
719+
633720 // ---------------------------------------------------------------------------
634721 // Validation edge cases
635722 // ---------------------------------------------------------------------------
@@ -1180,6 +1267,48 @@ describe('Agency Full Integration', () => {
11801267 expect ( chunks . join ( '' ) ) . toBe ( DEFAULT_RESULT . text ) ;
11811268 } ) ;
11821269
1270+ it ( 'stream() yields text chunks incrementally before completion' , async ( ) => {
1271+ let releaseSecondChunk ! : ( ) => void ;
1272+ const secondChunkGate = new Promise < void > ( ( resolve ) => {
1273+ releaseSecondChunk = resolve ;
1274+ } ) ;
1275+
1276+ hoisted . strategyStream . mockReturnValueOnce ( {
1277+ textStream : ( async function * ( ) {
1278+ yield 'first ' ;
1279+ await secondChunkGate ;
1280+ yield 'second' ;
1281+ } ) ( ) ,
1282+ fullStream : ( async function * ( ) {
1283+ yield { type : 'text' , text : 'first ' } ;
1284+ await secondChunkGate ;
1285+ yield { type : 'text' , text : 'second' } ;
1286+ } ) ( ) ,
1287+ text : ( async ( ) => {
1288+ await secondChunkGate ;
1289+ return 'first second' ;
1290+ } ) ( ) ,
1291+ usage : Promise . resolve ( DEFAULT_USAGE ) ,
1292+ agentCalls : Promise . resolve ( DEFAULT_RESULT . agentCalls ) ,
1293+ } ) ;
1294+
1295+ const team = agency ( {
1296+ agents : { worker : mockAgentConfig ( 'worker' ) } ,
1297+ strategy : 'sequential' ,
1298+ } ) ;
1299+
1300+ const streamResult = team . stream ( 'incremental stream' ) as {
1301+ textStream : AsyncIterable < string > ;
1302+ text : Promise < string > ;
1303+ } ;
1304+ const iterator = streamResult . textStream [ Symbol . asyncIterator ] ( ) ;
1305+
1306+ await expect ( iterator . next ( ) ) . resolves . toEqual ( { value : 'first ' , done : false } ) ;
1307+ releaseSecondChunk ( ) ;
1308+ await expect ( iterator . next ( ) ) . resolves . toEqual ( { value : 'second' , done : false } ) ;
1309+ await expect ( streamResult . text ) . resolves . toBe ( 'first second' ) ;
1310+ } ) ;
1311+
11831312 it ( 'stream() text promise resolves to the full text' , async ( ) => {
11841313 const team = agency ( {
11851314 agents : { worker : mockAgentConfig ( 'worker' ) } ,
@@ -1190,6 +1319,94 @@ describe('Agency Full Integration', () => {
11901319 const text = await streamResult . text ;
11911320 expect ( text ) . toBe ( DEFAULT_RESULT . text ) ;
11921321 } ) ;
1322+
1323+ it ( 'stream() exposes the finalized agentCalls ledger' , async ( ) => {
1324+ const team = agency ( {
1325+ agents : { worker : mockAgentConfig ( 'worker' ) } ,
1326+ strategy : 'sequential' ,
1327+ } ) ;
1328+
1329+ const streamResult = team . stream ( 'stream ledger' ) as unknown as {
1330+ agentCalls : Promise < unknown > ;
1331+ } ;
1332+
1333+ await expect ( streamResult . agentCalls ) . resolves . toEqual ( DEFAULT_RESULT . agentCalls ) ;
1334+ } ) ;
1335+
1336+ it ( 'stream() fullStream includes approval events and the finalized output event' , async ( ) => {
1337+ const team = agency ( {
1338+ agents : { worker : mockAgentConfig ( 'worker' ) } ,
1339+ strategy : 'sequential' ,
1340+ hitl : {
1341+ approvals : { beforeReturn : true } ,
1342+ handler : async ( ) => ( {
1343+ approved : true ,
1344+ modifications : { output : 'approved stream output' } ,
1345+ } ) ,
1346+ } ,
1347+ } ) ;
1348+
1349+ const streamResult = team . stream ( 'stream approval events' ) as {
1350+ fullStream : AsyncIterable < Record < string , unknown > > ;
1351+ text : Promise < string > ;
1352+ } ;
1353+
1354+ const parts : Array < Record < string , unknown > > = [ ] ;
1355+ for await ( const part of streamResult . fullStream ) {
1356+ parts . push ( part ) ;
1357+ }
1358+
1359+ await expect ( streamResult . text ) . resolves . toBe ( 'approved stream output' ) ;
1360+ expect ( parts ) . toEqual (
1361+ expect . arrayContaining ( [
1362+ expect . objectContaining ( { type : 'approval-requested' } ) ,
1363+ expect . objectContaining ( { type : 'approval-decided' , approved : true } ) ,
1364+ expect . objectContaining ( {
1365+ type : 'final-output' ,
1366+ text : 'approved stream output' ,
1367+ usage : DEFAULT_USAGE ,
1368+ agentCalls : DEFAULT_RESULT . agentCalls ,
1369+ } ) ,
1370+ expect . objectContaining ( {
1371+ type : 'agent-end' ,
1372+ agent : '__agency__' ,
1373+ output : 'approved stream output' ,
1374+ } ) ,
1375+ ] ) ,
1376+ ) ;
1377+ } ) ;
1378+
1379+ it ( 'stream() exposes finalTextStream with only the finalized approved text' , async ( ) => {
1380+ const team = agency ( {
1381+ agents : { worker : mockAgentConfig ( 'worker' ) } ,
1382+ strategy : 'sequential' ,
1383+ hitl : {
1384+ approvals : { beforeReturn : true } ,
1385+ handler : async ( ) => ( {
1386+ approved : true ,
1387+ modifications : { output : 'approved stream output' } ,
1388+ } ) ,
1389+ } ,
1390+ } ) ;
1391+
1392+ const streamResult = team . stream ( 'stream finalized text' ) as unknown as {
1393+ textStream : AsyncIterable < string > ;
1394+ finalTextStream : AsyncIterable < string > ;
1395+ } ;
1396+
1397+ const rawChunks : string [ ] = [ ] ;
1398+ for await ( const chunk of streamResult . textStream ) {
1399+ rawChunks . push ( chunk ) ;
1400+ }
1401+
1402+ const finalizedChunks : string [ ] = [ ] ;
1403+ for await ( const chunk of streamResult . finalTextStream ) {
1404+ finalizedChunks . push ( chunk ) ;
1405+ }
1406+
1407+ expect ( rawChunks . join ( '' ) ) . toBe ( DEFAULT_RESULT . text ) ;
1408+ expect ( finalizedChunks ) . toEqual ( [ 'approved stream output' ] ) ;
1409+ } ) ;
11931410 } ) ;
11941411
11951412 // ---------------------------------------------------------------------------
@@ -1214,7 +1431,8 @@ describe('Agency Full Integration', () => {
12141431 await session . send ( 'First turn' ) ;
12151432
12161433 // Second turn via stream() — should include the prior history in the prompt.
1217- session . stream ( 'Second turn via stream' ) ;
1434+ const result = session . stream ( 'Second turn via stream' ) as { text : Promise < string > } ;
1435+ await result . text ;
12181436
12191437 // The strategy stream should have been called with the history-prefixed prompt.
12201438 expect ( hoisted . strategyStream ) . toHaveBeenCalledWith (
@@ -1242,20 +1460,53 @@ describe('Agency Full Integration', () => {
12421460 expect ( chunks . join ( '' ) ) . toBe ( DEFAULT_RESULT . text ) ;
12431461 } ) ;
12441462
1245- it ( 'session.stream() does not lose history on first call with empty history' , ( ) => {
1463+ it ( 'session.stream() stores the finalized assistant text and session usage' , async ( ) => {
1464+ const team = agency ( {
1465+ agents : { a : mockAgentConfig ( 'a' ) } ,
1466+ strategy : 'sequential' ,
1467+ hitl : {
1468+ approvals : { beforeReturn : true } ,
1469+ handler : async ( ) => ( {
1470+ approved : true ,
1471+ modifications : { output : 'approved session reply' } ,
1472+ } ) ,
1473+ } ,
1474+ } ) ;
1475+
1476+ const session = team . session ( 'stream-finalized-history' ) as {
1477+ stream : ( t : string ) => { text : Promise < string > } ;
1478+ messages : ( ) => Array < { role : 'user' | 'assistant' ; content : string } > ;
1479+ usage : ( ) => Promise < Record < string , unknown > > ;
1480+ } ;
1481+
1482+ const result = session . stream ( 'Hello with approvals' ) ;
1483+ await expect ( result . text ) . resolves . toBe ( 'approved session reply' ) ;
1484+ await Promise . resolve ( ) ;
1485+
1486+ expect ( session . messages ( ) ) . toEqual ( [
1487+ { role : 'user' , content : 'Hello with approvals' } ,
1488+ { role : 'assistant' , content : 'approved session reply' } ,
1489+ ] ) ;
1490+
1491+ const usage = await session . usage ( ) ;
1492+ expect ( usage . totalTokens ) . toBe ( DEFAULT_USAGE . totalTokens ) ;
1493+ } ) ;
1494+
1495+ it ( 'session.stream() does not lose history on first call with empty history' , async ( ) => {
12461496 const team = agency ( {
12471497 agents : { a : mockAgentConfig ( 'a' ) } ,
12481498 strategy : 'sequential' ,
12491499 } ) ;
12501500
12511501 const session = team . session ( 'stream-empty-history' ) as {
1252- stream : ( t : string ) => unknown ;
1502+ stream : ( t : string ) => { text : Promise < string > } ;
12531503 } ;
12541504
12551505 hoisted . strategyStream . mockClear ( ) ;
12561506
12571507 // When history is empty, the plain text should be passed through unchanged.
1258- session . stream ( 'Plain prompt' ) ;
1508+ const result = session . stream ( 'Plain prompt' ) ;
1509+ await result . text ;
12591510
12601511 expect ( hoisted . strategyStream ) . toHaveBeenCalledWith ( 'Plain prompt' , undefined ) ;
12611512 } ) ;
0 commit comments