!feat(core): clarify agent session resume boundaries

This commit is contained in:
Adam Weidman
2026-03-23 10:55:57 -04:00
parent 18544cc994
commit 1ee198cc2c
2 changed files with 35 additions and 15 deletions
+25 -5
View File
@@ -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 () => {
+10 -10
View File
@@ -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(