@@ -117,6 +117,7 @@ describe('AgentSession', () => {
117117 expect ( events ) . toHaveLength ( 0 ) ;
118118 expect ( protocol . events ) . toHaveLength ( 1 ) ;
119119 expect ( protocol . events [ 0 ] . type ) . toBe ( 'session_update' ) ;
120+ expect ( protocol . events [ 0 ] . streamId ) . toEqual ( expect . any ( String ) ) ;
120121 } ) ;
121122
122123 it ( 'should skip events that occur before agent_start' , async ( ) => {
@@ -171,6 +172,181 @@ describe('AgentSession', () => {
171172 expect ( streamedEvents ) . toEqual ( allEvents . slice ( 2 ) ) ;
172173 } ) ;
173174
175+ it ( 'should complete immediately when resuming from agent_end' , async ( ) => {
176+ const protocol = new MockAgentProtocol ( ) ;
177+ const session = new AgentSession ( protocol ) ;
178+
179+ protocol . pushResponse ( [ { type : 'message' } ] ) ;
180+ const { streamId } = await session . send ( {
181+ message : [ { type : 'text' , text : 'request' } ] ,
182+ } ) ;
183+ await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) ) ;
184+
185+ const endEvent = session . events . findLast (
186+ ( event ) : event is AgentEvent < 'agent_end' > =>
187+ event . type === 'agent_end' && event . streamId === streamId ,
188+ ) ;
189+ expect ( endEvent ) . toBeDefined ( ) ;
190+
191+ const iterator = session
192+ . stream ( { eventId : endEvent ! . id } )
193+ [ Symbol . asyncIterator ] ( ) ;
194+ await expect ( iterator . next ( ) ) . resolves . toEqual ( {
195+ value : undefined ,
196+ done : true ,
197+ } ) ;
198+ } ) ;
199+
200+ it ( 'should throw for an unknown eventId' , async ( ) => {
201+ const protocol = new MockAgentProtocol ( ) ;
202+ const session = new AgentSession ( protocol ) ;
203+
204+ const iterator = session
205+ . stream ( { eventId : 'missing-event' } )
206+ [ Symbol . asyncIterator ] ( ) ;
207+ await expect ( iterator . next ( ) ) . rejects . toThrow (
208+ 'Unknown eventId: missing-event' ,
209+ ) ;
210+ } ) ;
211+
212+ it ( 'should throw when resuming from an event before agent_start on a stream with no agent activity' , async ( ) => {
213+ const protocol = new MockAgentProtocol ( ) ;
214+ const session = new AgentSession ( protocol ) ;
215+
216+ const { streamId } = await session . send ( { update : { title : 'draft' } } ) ;
217+ expect ( streamId ) . toBeNull ( ) ;
218+
219+ const updateEvent = session . events . find (
220+ ( event ) : event is AgentEvent < 'session_update' > =>
221+ event . type === 'session_update' ,
222+ ) ;
223+ expect ( updateEvent ) . toBeDefined ( ) ;
224+
225+ const iterator = session
226+ . stream ( { eventId : updateEvent ! . id } )
227+ [ Symbol . asyncIterator ] ( ) ;
228+ await expect ( iterator . next ( ) ) . rejects . toThrow (
229+ `Cannot resume from eventId ${ updateEvent ! . id } before agent_start for stream ${ updateEvent ! . streamId } ` ,
230+ ) ;
231+ } ) ;
232+
233+ it ( 'should replay from agent_start when resuming from a pre-agent_start event after activity is in history' , async ( ) => {
234+ const protocol = new MockAgentProtocol ( ) ;
235+ const session = new AgentSession ( protocol ) ;
236+
237+ protocol . pushResponse ( [
238+ {
239+ type : 'message' ,
240+ role : 'agent' ,
241+ content : [ { type : 'text' , text : 'hello' } ] ,
242+ } ,
243+ ] ) ;
244+ await session . send ( {
245+ message : [ { type : 'text' , text : 'request' } ] ,
246+ } ) ;
247+ await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) ) ;
248+
249+ const userMessage = session . events . find (
250+ ( event ) : event is AgentEvent < 'message' > =>
251+ event . type === 'message' && event . role === 'user' ,
252+ ) ;
253+ expect ( userMessage ) . toBeDefined ( ) ;
254+
255+ const streamedEvents : AgentEvent [ ] = [ ] ;
256+ for await ( const event of session . stream ( { eventId : userMessage ! . id } ) ) {
257+ streamedEvents . push ( event ) ;
258+ }
259+
260+ expect ( streamedEvents . map ( ( event ) => event . type ) ) . toEqual ( [
261+ 'agent_start' ,
262+ 'message' ,
263+ 'agent_end' ,
264+ ] ) ;
265+ expect ( streamedEvents [ 0 ] ?. streamId ) . toBe ( userMessage ! . streamId ) ;
266+ } ) ;
267+
268+ it ( 'should throw when resuming from a pre-agent_start event before activity is in history' , async ( ) => {
269+ const protocol = new MockAgentProtocol ( [
270+ {
271+ id : 'e-1' ,
272+ timestamp : '2026-01-01T00:00:00.000Z' ,
273+ streamId : 'stream-1' ,
274+ type : 'message' ,
275+ role : 'user' ,
276+ content : [ { type : 'text' , text : 'request' } ] ,
277+ } ,
278+ ] ) ;
279+ const session = new AgentSession ( protocol ) ;
280+
281+ const iterator = session
282+ . stream ( { eventId : 'e-1' } )
283+ [ Symbol . asyncIterator ] ( ) ;
284+ await expect ( iterator . next ( ) ) . rejects . toThrow (
285+ 'Cannot resume from eventId e-1 before agent_start for stream stream-1' ,
286+ ) ;
287+ } ) ;
288+
289+ it ( 'should resume from an in-stream event within the same stream only' , async ( ) => {
290+ const protocol = new MockAgentProtocol ( ) ;
291+ const session = new AgentSession ( protocol ) ;
292+
293+ protocol . pushResponse ( [
294+ {
295+ type : 'message' ,
296+ role : 'agent' ,
297+ content : [ { type : 'text' , text : 'first answer 1' } ] ,
298+ } ,
299+ {
300+ type : 'message' ,
301+ role : 'agent' ,
302+ content : [ { type : 'text' , text : 'first answer 2' } ] ,
303+ } ,
304+ ] ) ;
305+ const { streamId : streamId1 } = await session . send ( {
306+ message : [ { type : 'text' , text : 'first request' } ] ,
307+ } ) ;
308+ await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) ) ;
309+
310+ protocol . pushResponse ( [
311+ {
312+ type : 'message' ,
313+ role : 'agent' ,
314+ content : [ { type : 'text' , text : 'second answer' } ] ,
315+ } ,
316+ ] ) ;
317+ await session . send ( {
318+ message : [ { type : 'text' , text : 'second request' } ] ,
319+ } ) ;
320+ await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) ) ;
321+
322+ const resumeEvent = session . events . find (
323+ ( event ) : event is AgentEvent < 'message' > =>
324+ event . type === 'message' &&
325+ event . streamId === streamId1 &&
326+ event . role === 'agent' &&
327+ event . content [ 0 ] ?. type === 'text' &&
328+ event . content [ 0 ] . text === 'first answer 1' ,
329+ ) ;
330+ expect ( resumeEvent ) . toBeDefined ( ) ;
331+
332+ const streamedEvents : AgentEvent [ ] = [ ] ;
333+ for await ( const event of session . stream ( { eventId : resumeEvent ! . id } ) ) {
334+ streamedEvents . push ( event ) ;
335+ }
336+
337+ expect (
338+ streamedEvents . every ( ( event ) => event . streamId === streamId1 ) ,
339+ ) . toBe ( true ) ;
340+ expect ( streamedEvents . map ( ( event ) => event . type ) ) . toEqual ( [
341+ 'message' ,
342+ 'agent_end' ,
343+ ] ) ;
344+ const resumedMessage = streamedEvents [ 0 ] as AgentEvent < 'message' > ;
345+ expect ( resumedMessage . content ) . toEqual ( [
346+ { type : 'text' , text : 'first answer 2' } ,
347+ ] ) ;
348+ } ) ;
349+
174350 it ( 'should replay events for streamId starting with agent_start' , async ( ) => {
175351 const protocol = new MockAgentProtocol ( ) ;
176352 const session = new AgentSession ( protocol ) ;
@@ -223,6 +399,33 @@ describe('AgentSession', () => {
223399 expect ( streamedEvents . at ( - 1 ) ?. type ) . toBe ( 'agent_end' ) ;
224400 } ) ;
225401
402+ it ( 'should not drop agent_end that arrives while replay events are being yielded' , async ( ) => {
403+ const protocol = new MockAgentProtocol ( ) ;
404+ const session = new AgentSession ( protocol ) ;
405+
406+ protocol . pushResponse ( [ { type : 'message' } ] , { keepOpen : true } ) ;
407+ const { streamId } = await session . send ( { update : { title : 't1' } } ) ;
408+ await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) ) ;
409+
410+ const iterator = session
411+ . stream ( { streamId : streamId ! } )
412+ [ Symbol . asyncIterator ] ( ) ;
413+
414+ const first = await iterator . next ( ) ;
415+ expect ( first . value ?. type ) . toBe ( 'agent_start' ) ;
416+
417+ protocol . pushToStream ( streamId ! , [ ] , { close : true } ) ;
418+
419+ const second = await iterator . next ( ) ;
420+ expect ( second . value ?. type ) . toBe ( 'message' ) ;
421+
422+ const third = await iterator . next ( ) ;
423+ expect ( third . value ?. type ) . toBe ( 'agent_end' ) ;
424+
425+ const fourth = await iterator . next ( ) ;
426+ expect ( fourth . done ) . toBe ( true ) ;
427+ } ) ;
428+
226429 it ( 'should follow an active stream if no options provided' , async ( ) => {
227430 const protocol = new MockAgentProtocol ( ) ;
228431 const session = new AgentSession ( protocol ) ;
0 commit comments