Skip to content

Commit b28931a

Browse files
committed
support multiple runs
1 parent 7e7adfd commit b28931a

File tree

5 files changed

+826
-12
lines changed

5 files changed

+826
-12
lines changed

CLAUDE.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ cd typescript-sdk/packages/<package-name>
5959
pnpm test
6060

6161
# For running a single test file
62-
pnpm test path/to/test.spec.ts
62+
cd typescript-sdk/packages/<package-name>
63+
pnpm test -- path/to/test.spec.ts
6364
```
6465

6566
## High-Level Architecture
@@ -96,6 +97,13 @@ Each framework integration follows a similar pattern:
9697
- Uses STATE_DELTA with JSON Patch (RFC 6902) for efficient incremental updates
9798
- MESSAGES_SNAPSHOT provides conversation history
9899

100+
### Multiple Sequential Runs
101+
- AG-UI supports multiple sequential runs in a single event stream
102+
- Each run must complete (RUN_FINISHED) before a new run can start (RUN_STARTED)
103+
- Messages accumulate across runs (e.g., messages from run1 + messages from run2)
104+
- State continues to evolve across runs unless explicitly reset with STATE_SNAPSHOT
105+
- Run-specific tracking (active messages, tool calls, steps) resets between runs
106+
99107
### Development Workflow
100108
- Turbo is used for monorepo build orchestration
101109
- Each package has independent versioning
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import { AbstractAgent, RunAgentResult } from "../agent";
2+
import { BaseEvent, EventType, Message, RunAgentInput, TextMessageStartEvent, TextMessageContentEvent, TextMessageEndEvent, RunStartedEvent, RunFinishedEvent } from "@ag-ui/core";
3+
import { Observable, of } from "rxjs";
4+
5+
describe("AbstractAgent multiple runs", () => {
6+
class TestAgent extends AbstractAgent {
7+
private events: BaseEvent[] = [];
8+
9+
setEvents(events: BaseEvent[]) {
10+
this.events = events;
11+
}
12+
13+
protected run(input: RunAgentInput): Observable<BaseEvent> {
14+
return of(...this.events);
15+
}
16+
}
17+
18+
it("should accumulate messages across multiple sequential runs", async () => {
19+
const agent = new TestAgent({
20+
threadId: "test-thread",
21+
initialMessages: [],
22+
});
23+
24+
// First run events
25+
const firstRunEvents: BaseEvent[] = [
26+
{
27+
type: EventType.RUN_STARTED,
28+
threadId: "test-thread",
29+
runId: "run-1",
30+
} as RunStartedEvent,
31+
{
32+
type: EventType.TEXT_MESSAGE_START,
33+
messageId: "msg-1",
34+
role: "assistant",
35+
} as TextMessageStartEvent,
36+
{
37+
type: EventType.TEXT_MESSAGE_CONTENT,
38+
messageId: "msg-1",
39+
delta: "Hello from run 1",
40+
} as TextMessageContentEvent,
41+
{
42+
type: EventType.TEXT_MESSAGE_END,
43+
messageId: "msg-1",
44+
} as TextMessageEndEvent,
45+
{
46+
type: EventType.RUN_FINISHED,
47+
} as RunFinishedEvent,
48+
];
49+
50+
// Execute first run
51+
agent.setEvents(firstRunEvents);
52+
const result1 = await agent.runAgent({ runId: "run-1" });
53+
54+
// Verify first run results
55+
expect(result1.newMessages.length).toBe(1);
56+
expect(result1.newMessages[0].content).toBe("Hello from run 1");
57+
expect(agent.messages.length).toBe(1);
58+
expect(agent.messages[0].content).toBe("Hello from run 1");
59+
60+
// Second run events
61+
const secondRunEvents: BaseEvent[] = [
62+
{
63+
type: EventType.RUN_STARTED,
64+
threadId: "test-thread",
65+
runId: "run-2",
66+
} as RunStartedEvent,
67+
{
68+
type: EventType.TEXT_MESSAGE_START,
69+
messageId: "msg-2",
70+
role: "assistant",
71+
} as TextMessageStartEvent,
72+
{
73+
type: EventType.TEXT_MESSAGE_CONTENT,
74+
messageId: "msg-2",
75+
delta: "Hello from run 2",
76+
} as TextMessageContentEvent,
77+
{
78+
type: EventType.TEXT_MESSAGE_END,
79+
messageId: "msg-2",
80+
} as TextMessageEndEvent,
81+
{
82+
type: EventType.RUN_FINISHED,
83+
} as RunFinishedEvent,
84+
];
85+
86+
// Execute second run
87+
agent.setEvents(secondRunEvents);
88+
const result2 = await agent.runAgent({ runId: "run-2" });
89+
90+
// Verify second run results
91+
expect(result2.newMessages.length).toBe(1);
92+
expect(result2.newMessages[0].content).toBe("Hello from run 2");
93+
94+
// Verify messages are accumulated
95+
expect(agent.messages.length).toBe(2);
96+
expect(agent.messages[0].content).toBe("Hello from run 1");
97+
expect(agent.messages[1].content).toBe("Hello from run 2");
98+
});
99+
100+
it("should handle three sequential runs with message accumulation", async () => {
101+
const agent = new TestAgent({
102+
threadId: "test-thread",
103+
initialMessages: [],
104+
});
105+
106+
const messages = ["First message", "Second message", "Third message"];
107+
108+
for (let i = 0; i < 3; i++) {
109+
const runEvents: BaseEvent[] = [
110+
{
111+
type: EventType.RUN_STARTED,
112+
threadId: "test-thread",
113+
runId: `run-${i + 1}`,
114+
} as RunStartedEvent,
115+
{
116+
type: EventType.TEXT_MESSAGE_START,
117+
messageId: `msg-${i + 1}`,
118+
role: "assistant",
119+
} as TextMessageStartEvent,
120+
{
121+
type: EventType.TEXT_MESSAGE_CONTENT,
122+
messageId: `msg-${i + 1}`,
123+
delta: messages[i],
124+
} as TextMessageContentEvent,
125+
{
126+
type: EventType.TEXT_MESSAGE_END,
127+
messageId: `msg-${i + 1}`,
128+
} as TextMessageEndEvent,
129+
{
130+
type: EventType.RUN_FINISHED,
131+
} as RunFinishedEvent,
132+
];
133+
134+
agent.setEvents(runEvents);
135+
const result = await agent.runAgent({ runId: `run-${i + 1}` });
136+
137+
// Verify new messages for this run
138+
expect(result.newMessages.length).toBe(1);
139+
expect(result.newMessages[0].content).toBe(messages[i]);
140+
141+
// Verify total accumulated messages
142+
expect(agent.messages.length).toBe(i + 1);
143+
for (let j = 0; j <= i; j++) {
144+
expect(agent.messages[j].content).toBe(messages[j]);
145+
}
146+
}
147+
148+
// Final verification
149+
expect(agent.messages.length).toBe(3);
150+
expect(agent.messages.map(m => m.content)).toEqual(messages);
151+
});
152+
153+
it("should handle multiple runs in a single event stream", async () => {
154+
const agent = new TestAgent({
155+
threadId: "test-thread",
156+
initialMessages: [],
157+
});
158+
159+
// Create a single event stream with two runs
160+
const allEvents: BaseEvent[] = [
161+
// First run
162+
{
163+
type: EventType.RUN_STARTED,
164+
threadId: "test-thread",
165+
runId: "run-1",
166+
} as RunStartedEvent,
167+
{
168+
type: EventType.TEXT_MESSAGE_START,
169+
messageId: "msg-1",
170+
role: "assistant",
171+
} as TextMessageStartEvent,
172+
{
173+
type: EventType.TEXT_MESSAGE_CONTENT,
174+
messageId: "msg-1",
175+
delta: "Message from run 1",
176+
} as TextMessageContentEvent,
177+
{
178+
type: EventType.TEXT_MESSAGE_END,
179+
messageId: "msg-1",
180+
} as TextMessageEndEvent,
181+
{
182+
type: EventType.RUN_FINISHED,
183+
} as RunFinishedEvent,
184+
// Second run
185+
{
186+
type: EventType.RUN_STARTED,
187+
threadId: "test-thread",
188+
runId: "run-2",
189+
} as RunStartedEvent,
190+
{
191+
type: EventType.TEXT_MESSAGE_START,
192+
messageId: "msg-2",
193+
role: "assistant",
194+
} as TextMessageStartEvent,
195+
{
196+
type: EventType.TEXT_MESSAGE_CONTENT,
197+
messageId: "msg-2",
198+
delta: "Message from run 2",
199+
} as TextMessageContentEvent,
200+
{
201+
type: EventType.TEXT_MESSAGE_END,
202+
messageId: "msg-2",
203+
} as TextMessageEndEvent,
204+
{
205+
type: EventType.RUN_FINISHED,
206+
} as RunFinishedEvent,
207+
];
208+
209+
// Execute with the combined event stream
210+
agent.setEvents(allEvents);
211+
const result = await agent.runAgent({ runId: "combined-run" });
212+
213+
// Verify results
214+
expect(result.newMessages.length).toBe(2);
215+
expect(result.newMessages[0].content).toBe("Message from run 1");
216+
expect(result.newMessages[1].content).toBe("Message from run 2");
217+
218+
// Verify all messages are accumulated
219+
expect(agent.messages.length).toBe(2);
220+
expect(agent.messages[0].content).toBe("Message from run 1");
221+
expect(agent.messages[1].content).toBe("Message from run 2");
222+
});
223+
224+
it("should start with initial messages and accumulate new ones", async () => {
225+
const initialMessages: Message[] = [
226+
{
227+
id: "initial-1",
228+
role: "user",
229+
content: "Initial message",
230+
},
231+
];
232+
233+
const agent = new TestAgent({
234+
threadId: "test-thread",
235+
initialMessages,
236+
});
237+
238+
// Run events
239+
const runEvents: BaseEvent[] = [
240+
{
241+
type: EventType.RUN_STARTED,
242+
threadId: "test-thread",
243+
runId: "run-1",
244+
} as RunStartedEvent,
245+
{
246+
type: EventType.TEXT_MESSAGE_START,
247+
messageId: "msg-1",
248+
role: "assistant",
249+
} as TextMessageStartEvent,
250+
{
251+
type: EventType.TEXT_MESSAGE_CONTENT,
252+
messageId: "msg-1",
253+
delta: "Response message",
254+
} as TextMessageContentEvent,
255+
{
256+
type: EventType.TEXT_MESSAGE_END,
257+
messageId: "msg-1",
258+
} as TextMessageEndEvent,
259+
{
260+
type: EventType.RUN_FINISHED,
261+
} as RunFinishedEvent,
262+
];
263+
264+
agent.setEvents(runEvents);
265+
const result = await agent.runAgent({ runId: "run-1" });
266+
267+
// Verify new messages don't include initial messages
268+
expect(result.newMessages.length).toBe(1);
269+
expect(result.newMessages[0].content).toBe("Response message");
270+
271+
// Verify total messages include both initial and new
272+
expect(agent.messages.length).toBe(2);
273+
expect(agent.messages[0].content).toBe("Initial message");
274+
expect(agent.messages[1].content).toBe("Response message");
275+
});
276+
});

typescript-sdk/packages/client/src/verify/__tests__/verify.lifecycle.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe("verifyEvents lifecycle", () => {
5858
next: (event) => events.push(event),
5959
error: (err) => {
6060
expect(err).toBeInstanceOf(AGUIError);
61-
expect(err.message).toContain("Cannot send multiple 'RUN_STARTED' events");
61+
expect(err.message).toContain("Cannot send 'RUN_STARTED' while a run is still active");
6262
subscription.unsubscribe();
6363
},
6464
});

0 commit comments

Comments
 (0)