From 1ee198cc2cfc42a2d4b91783831badf0bc8df79d Mon Sep 17 00:00:00 2001 From: Adam Weidman Date: Mon, 23 Mar 2026 10:55:57 -0400 Subject: [PATCH] !feat(core): clarify agent session resume boundaries --- packages/core/src/agent/agent-session.test.ts | 30 +++++++++++++++---- packages/core/src/agent/agent-session.ts | 20 ++++++------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/packages/core/src/agent/agent-session.test.ts b/packages/core/src/agent/agent-session.test.ts index fd32ce4fb7..f69a9758bb 100644 --- a/packages/core/src/agent/agent-session.test.ts +++ b/packages/core/src/agent/agent-session.test.ts @@ -209,7 +209,7 @@ describe('AgentSession', () => { ); }); - it('should complete immediately when resuming from an event on a stream with no agent activity', async () => { + it('should throw when resuming from an event before agent_start on a stream with no agent activity', async () => { const protocol = new MockAgentProtocol(); const session = new AgentSession(protocol); @@ -225,10 +225,30 @@ describe('AgentSession', () => { const iterator = session.stream({ eventId: updateEvent!.id })[ Symbol.asyncIterator ](); - await expect(iterator.next()).resolves.toEqual({ - value: undefined, - done: true, - }); + await expect(iterator.next()).rejects.toThrow( + `Cannot resume from eventId ${updateEvent!.id} before agent_start; use stream({ streamId }) instead`, + ); + }); + + it('should throw when resuming from a pre-agent_start event even if agent activity may start later', async () => { + const protocol = new MockAgentProtocol([ + { + id: 'e-1', + timestamp: '2026-01-01T00:00:00.000Z', + streamId: 'stream-1', + type: 'message', + role: 'user', + content: [{ type: 'text', text: 'request' }], + }, + ]); + const session = new AgentSession(protocol); + + const iterator = session.stream({ eventId: 'e-1' })[ + Symbol.asyncIterator + ](); + await expect(iterator.next()).rejects.toThrow( + 'Cannot resume from eventId e-1 before agent_start; use stream({ streamId }) instead', + ); }); it('should resume from an in-stream event within the same stream only', async () => { diff --git a/packages/core/src/agent/agent-session.ts b/packages/core/src/agent/agent-session.ts index a58efec4ea..c48addcc4a 100644 --- a/packages/core/src/agent/agent-session.ts +++ b/packages/core/src/agent/agent-session.ts @@ -148,16 +148,16 @@ export class AgentSession implements AgentProtocol { done = true; } else if (streamHasStarted) { agentActivityStarted = true; - } else if ( - !currentEvents - .slice(index + 1) - .some( - (event) => - event.type === 'agent_start' && - event.streamId === trackedStreamId, - ) - ) { - done = true; + } else { + // Consumers can only resume by eventId once the stream has entered the + // agent_start -> agent_end lifecycle. For pre-start events, use + // stream({ streamId }) instead because this wrapper cannot + // distinguish "agent activity will start later" from "this send was + // acknowledged without agent activity" without risking an infinite + // wait. + throw new Error( + `Cannot resume from eventId ${options.eventId} before agent_start; use stream({ streamId }) instead`, + ); } } else if (options.streamId) { const index = currentEvents.findIndex(