@copilotkitnext/agent 0.0.0-max-changeset-20260109174803

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,391 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { BasicAgent } from "../index";
3
+ import { EventType, type RunAgentInput } from "@ag-ui/client";
4
+ import { streamText } from "ai";
5
+ import { mockStreamTextResponse, toolCallStreamingStart, toolCall, toolResult, finish, collectEvents } from "./test-helpers";
6
+
7
+ // Mock the ai module
8
+ vi.mock("ai", () => ({
9
+ streamText: vi.fn(),
10
+ tool: vi.fn((config) => config),
11
+ }));
12
+
13
+ // Mock the SDK clients
14
+ vi.mock("@ai-sdk/openai", () => ({
15
+ createOpenAI: vi.fn(() => (modelId: string) => ({
16
+ modelId,
17
+ provider: "openai",
18
+ })),
19
+ }));
20
+
21
+ describe("State Update Tools", () => {
22
+ const originalEnv = process.env;
23
+
24
+ beforeEach(() => {
25
+ vi.clearAllMocks();
26
+ process.env = { ...originalEnv };
27
+ process.env.OPENAI_API_KEY = "test-key";
28
+ });
29
+
30
+ afterEach(() => {
31
+ process.env = originalEnv;
32
+ });
33
+
34
+ describe("AGUISendStateSnapshot", () => {
35
+ it("should emit STATE_SNAPSHOT event when tool is called", async () => {
36
+ const agent = new BasicAgent({
37
+ model: "openai/gpt-4o",
38
+ });
39
+
40
+ const newState = { counter: 5, items: ["x", "y"] };
41
+
42
+ vi.mocked(streamText).mockReturnValue(
43
+ mockStreamTextResponse([
44
+ toolCallStreamingStart("call1", "AGUISendStateSnapshot"),
45
+ toolCall("call1", "AGUISendStateSnapshot"),
46
+ toolResult("call1", "AGUISendStateSnapshot", { success: true, snapshot: newState }),
47
+ finish(),
48
+ ]) as any,
49
+ );
50
+
51
+ const input: RunAgentInput = {
52
+ threadId: "thread1",
53
+ runId: "run1",
54
+ messages: [],
55
+ tools: [],
56
+ context: [],
57
+ state: { counter: 0 },
58
+ };
59
+
60
+ const events = await collectEvents(agent["run"](input));
61
+
62
+ // Find STATE_SNAPSHOT event
63
+ const snapshotEvent = events.find((e: any) => e.type === EventType.STATE_SNAPSHOT);
64
+ expect(snapshotEvent).toBeDefined();
65
+ expect(snapshotEvent).toMatchObject({
66
+ type: EventType.STATE_SNAPSHOT,
67
+ snapshot: newState,
68
+ });
69
+ });
70
+
71
+ it("should still emit TOOL_CALL_RESULT for the LLM", async () => {
72
+ const agent = new BasicAgent({
73
+ model: "openai/gpt-4o",
74
+ });
75
+
76
+ vi.mocked(streamText).mockReturnValue(
77
+ mockStreamTextResponse([
78
+ toolCallStreamingStart("call1", "AGUISendStateSnapshot"),
79
+ toolCall("call1", "AGUISendStateSnapshot"),
80
+ toolResult("call1", "AGUISendStateSnapshot", { success: true, snapshot: { value: 1 } }),
81
+ finish(),
82
+ ]) as any,
83
+ );
84
+
85
+ const input: RunAgentInput = {
86
+ threadId: "thread1",
87
+ runId: "run1",
88
+ messages: [],
89
+ tools: [],
90
+ context: [],
91
+ state: {},
92
+ };
93
+
94
+ const events = await collectEvents(agent["run"](input));
95
+
96
+ // Should have both STATE_SNAPSHOT and TOOL_CALL_RESULT
97
+ const snapshotEvent = events.find((e: any) => e.type === EventType.STATE_SNAPSHOT);
98
+ const toolResultEvent = events.find((e: any) => e.type === EventType.TOOL_CALL_RESULT);
99
+
100
+ expect(snapshotEvent).toBeDefined();
101
+ expect(toolResultEvent).toBeDefined();
102
+ expect(toolResultEvent).toMatchObject({
103
+ type: EventType.TOOL_CALL_RESULT,
104
+ toolCallId: "call1",
105
+ });
106
+ });
107
+ });
108
+
109
+ describe("AGUISendStateDelta", () => {
110
+ it("should emit STATE_DELTA event when tool is called", async () => {
111
+ const agent = new BasicAgent({
112
+ model: "openai/gpt-4o",
113
+ });
114
+
115
+ const delta = [
116
+ { op: "replace", path: "/counter", value: 10 },
117
+ { op: "add", path: "/newField", value: "test" },
118
+ ];
119
+
120
+ vi.mocked(streamText).mockReturnValue(
121
+ mockStreamTextResponse([
122
+ toolCallStreamingStart("call1", "AGUISendStateDelta"),
123
+ toolCall("call1", "AGUISendStateDelta"),
124
+ toolResult("call1", "AGUISendStateDelta", { success: true, delta }),
125
+ finish(),
126
+ ]) as any,
127
+ );
128
+
129
+ const input: RunAgentInput = {
130
+ threadId: "thread1",
131
+ runId: "run1",
132
+ messages: [],
133
+ tools: [],
134
+ context: [],
135
+ state: { counter: 0 },
136
+ };
137
+
138
+ const events = await collectEvents(agent["run"](input));
139
+
140
+ // Find STATE_DELTA event
141
+ const deltaEvent = events.find((e: any) => e.type === EventType.STATE_DELTA);
142
+ expect(deltaEvent).toBeDefined();
143
+ expect(deltaEvent).toMatchObject({
144
+ type: EventType.STATE_DELTA,
145
+ delta,
146
+ });
147
+ });
148
+
149
+ it("should handle add operations", async () => {
150
+ const agent = new BasicAgent({
151
+ model: "openai/gpt-4o",
152
+ });
153
+
154
+ const delta = [{ op: "add", path: "/items/0", value: "new item" }];
155
+
156
+ vi.mocked(streamText).mockReturnValue(
157
+ mockStreamTextResponse([
158
+ toolCallStreamingStart("call1", "AGUISendStateDelta"),
159
+ toolCall("call1", "AGUISendStateDelta"),
160
+ toolResult("call1", "AGUISendStateDelta", { success: true, delta }),
161
+ finish(),
162
+ ]) as any,
163
+ );
164
+
165
+ const input: RunAgentInput = {
166
+ threadId: "thread1",
167
+ runId: "run1",
168
+ messages: [],
169
+ tools: [],
170
+ context: [],
171
+ state: { items: [] },
172
+ };
173
+
174
+ const events = await collectEvents(agent["run"](input));
175
+
176
+ const deltaEvent = events.find((e: any) => e.type === EventType.STATE_DELTA);
177
+ expect(deltaEvent?.delta).toEqual(delta);
178
+ });
179
+
180
+ it("should handle replace operations", async () => {
181
+ const agent = new BasicAgent({
182
+ model: "openai/gpt-4o",
183
+ });
184
+
185
+ const delta = [{ op: "replace", path: "/status", value: "active" }];
186
+
187
+ vi.mocked(streamText).mockReturnValue(
188
+ mockStreamTextResponse([
189
+ toolCallStreamingStart("call1", "AGUISendStateDelta"),
190
+ toolCall("call1", "AGUISendStateDelta"),
191
+ toolResult("call1", "AGUISendStateDelta", { success: true, delta }),
192
+ finish(),
193
+ ]) as any,
194
+ );
195
+
196
+ const input: RunAgentInput = {
197
+ threadId: "thread1",
198
+ runId: "run1",
199
+ messages: [],
200
+ tools: [],
201
+ context: [],
202
+ state: { status: "inactive" },
203
+ };
204
+
205
+ const events = await collectEvents(agent["run"](input));
206
+
207
+ const deltaEvent = events.find((e: any) => e.type === EventType.STATE_DELTA);
208
+ expect(deltaEvent?.delta).toEqual(delta);
209
+ });
210
+
211
+ it("should handle remove operations", async () => {
212
+ const agent = new BasicAgent({
213
+ model: "openai/gpt-4o",
214
+ });
215
+
216
+ const delta = [{ op: "remove", path: "/oldField" }];
217
+
218
+ vi.mocked(streamText).mockReturnValue(
219
+ mockStreamTextResponse([
220
+ toolCallStreamingStart("call1", "AGUISendStateDelta"),
221
+ toolCall("call1", "AGUISendStateDelta"),
222
+ toolResult("call1", "AGUISendStateDelta", { success: true, delta }),
223
+ finish(),
224
+ ]) as any,
225
+ );
226
+
227
+ const input: RunAgentInput = {
228
+ threadId: "thread1",
229
+ runId: "run1",
230
+ messages: [],
231
+ tools: [],
232
+ context: [],
233
+ state: { oldField: "value", keepField: "keep" },
234
+ };
235
+
236
+ const events = await collectEvents(agent["run"](input));
237
+
238
+ const deltaEvent = events.find((e: any) => e.type === EventType.STATE_DELTA);
239
+ expect(deltaEvent?.delta).toEqual(delta);
240
+ });
241
+
242
+ it("should handle multiple operations in a single delta", async () => {
243
+ const agent = new BasicAgent({
244
+ model: "openai/gpt-4o",
245
+ });
246
+
247
+ const delta = [
248
+ { op: "replace", path: "/counter", value: 5 },
249
+ { op: "add", path: "/items/-", value: "new" },
250
+ { op: "remove", path: "/temp" },
251
+ ];
252
+
253
+ vi.mocked(streamText).mockReturnValue(
254
+ mockStreamTextResponse([
255
+ toolCallStreamingStart("call1", "AGUISendStateDelta"),
256
+ toolCall("call1", "AGUISendStateDelta"),
257
+ toolResult("call1", "AGUISendStateDelta", { success: true, delta }),
258
+ finish(),
259
+ ]) as any,
260
+ );
261
+
262
+ const input: RunAgentInput = {
263
+ threadId: "thread1",
264
+ runId: "run1",
265
+ messages: [],
266
+ tools: [],
267
+ context: [],
268
+ state: { counter: 0, items: [], temp: "remove me" },
269
+ };
270
+
271
+ const events = await collectEvents(agent["run"](input));
272
+
273
+ const deltaEvent = events.find((e: any) => e.type === EventType.STATE_DELTA);
274
+ expect(deltaEvent?.delta).toEqual(delta);
275
+ });
276
+
277
+ it("should still emit TOOL_CALL_RESULT for the LLM", async () => {
278
+ const agent = new BasicAgent({
279
+ model: "openai/gpt-4o",
280
+ });
281
+
282
+ const delta = [{ op: "replace", path: "/value", value: 1 }];
283
+
284
+ vi.mocked(streamText).mockReturnValue(
285
+ mockStreamTextResponse([
286
+ toolCallStreamingStart("call1", "AGUISendStateDelta"),
287
+ toolCall("call1", "AGUISendStateDelta"),
288
+ toolResult("call1", "AGUISendStateDelta", { success: true, delta }),
289
+ finish(),
290
+ ]) as any,
291
+ );
292
+
293
+ const input: RunAgentInput = {
294
+ threadId: "thread1",
295
+ runId: "run1",
296
+ messages: [],
297
+ tools: [],
298
+ context: [],
299
+ state: {},
300
+ };
301
+
302
+ const events = await collectEvents(agent["run"](input));
303
+
304
+ // Should have both STATE_DELTA and TOOL_CALL_RESULT
305
+ const deltaEvent = events.find((e: any) => e.type === EventType.STATE_DELTA);
306
+ const toolResultEvent = events.find((e: any) => e.type === EventType.TOOL_CALL_RESULT);
307
+
308
+ expect(deltaEvent).toBeDefined();
309
+ expect(toolResultEvent).toBeDefined();
310
+ expect(toolResultEvent).toMatchObject({
311
+ type: EventType.TOOL_CALL_RESULT,
312
+ toolCallId: "call1",
313
+ });
314
+ });
315
+ });
316
+
317
+ describe("State Tools Integration", () => {
318
+ it("should handle both snapshot and delta in same run", async () => {
319
+ const agent = new BasicAgent({
320
+ model: "openai/gpt-4o",
321
+ });
322
+
323
+ vi.mocked(streamText).mockReturnValue(
324
+ mockStreamTextResponse([
325
+ toolCallStreamingStart("call1", "AGUISendStateSnapshot"),
326
+ toolCall("call1", "AGUISendStateSnapshot"),
327
+ toolResult("call1", "AGUISendStateSnapshot", { success: true, snapshot: { value: 1 } }),
328
+ toolCallStreamingStart("call2", "AGUISendStateDelta"),
329
+ toolCall("call2", "AGUISendStateDelta"),
330
+ toolResult("call2", "AGUISendStateDelta", { success: true, delta: [{ op: "replace", path: "/value", value: 2 }] }),
331
+ finish(),
332
+ ]) as any,
333
+ );
334
+
335
+ const input: RunAgentInput = {
336
+ threadId: "thread1",
337
+ runId: "run1",
338
+ messages: [],
339
+ tools: [],
340
+ context: [],
341
+ state: {},
342
+ };
343
+
344
+ const events = await collectEvents(agent["run"](input));
345
+
346
+ const snapshotEvents = events.filter((e: any) => e.type === EventType.STATE_SNAPSHOT);
347
+ const deltaEvents = events.filter((e: any) => e.type === EventType.STATE_DELTA);
348
+
349
+ expect(snapshotEvents).toHaveLength(1);
350
+ expect(deltaEvents).toHaveLength(1);
351
+ });
352
+
353
+ it("should not emit state events for non-state tools", async () => {
354
+ const agent = new BasicAgent({
355
+ model: "openai/gpt-4o",
356
+ });
357
+
358
+ vi.mocked(streamText).mockReturnValue(
359
+ mockStreamTextResponse([
360
+ toolCallStreamingStart("call1", "otherTool"),
361
+ toolCall("call1", "otherTool"),
362
+ toolResult("call1", "otherTool", { result: "data" }),
363
+ finish(),
364
+ ]) as any,
365
+ );
366
+
367
+ const input: RunAgentInput = {
368
+ threadId: "thread1",
369
+ runId: "run1",
370
+ messages: [],
371
+ tools: [
372
+ {
373
+ name: "otherTool",
374
+ description: "Other tool",
375
+ parameters: { type: "object", properties: {} },
376
+ },
377
+ ],
378
+ context: [],
379
+ state: {},
380
+ };
381
+
382
+ const events = await collectEvents(agent["run"](input));
383
+
384
+ const stateEvents = events.filter(
385
+ (e: any) => e.type === EventType.STATE_SNAPSHOT || e.type === EventType.STATE_DELTA,
386
+ );
387
+
388
+ expect(stateEvents).toHaveLength(0);
389
+ });
390
+ });
391
+ });
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Test helpers for mocking streamText responses
3
+ */
4
+
5
+ export interface MockStreamEvent {
6
+ type: string;
7
+ [key: string]: any;
8
+ }
9
+
10
+ /**
11
+ * Creates a mock streamText response with controlled events
12
+ */
13
+ export function mockStreamTextResponse(events: MockStreamEvent[]) {
14
+ return {
15
+ fullStream: (async function* () {
16
+ for (const event of events) {
17
+ yield event;
18
+ }
19
+ })(),
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Helper to create a text-start event
25
+ */
26
+ export function textStart(id?: string): MockStreamEvent {
27
+ const event: MockStreamEvent = {
28
+ type: "text-start",
29
+ };
30
+ if (id !== undefined) {
31
+ event.id = id;
32
+ }
33
+ return event;
34
+ }
35
+
36
+ /**
37
+ * Helper to create a text delta event
38
+ */
39
+ export function textDelta(text: string): MockStreamEvent {
40
+ return {
41
+ type: "text-delta",
42
+ text,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Helper to create a tool call streaming start event
48
+ */
49
+ export function toolCallStreamingStart(toolCallId: string, toolName: string): MockStreamEvent {
50
+ return {
51
+ type: "tool-input-start",
52
+ id: toolCallId,
53
+ toolName,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Helper to create a tool call delta event
59
+ */
60
+ export function toolCallDelta(toolCallId: string, argsTextDelta: string): MockStreamEvent {
61
+ return {
62
+ type: "tool-input-delta",
63
+ id: toolCallId,
64
+ delta: argsTextDelta,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Helper to create a tool call event
70
+ */
71
+ export function toolCall(toolCallId: string, toolName: string, input: unknown = {}): MockStreamEvent {
72
+ return {
73
+ type: "tool-call",
74
+ toolCallId,
75
+ toolName,
76
+ input,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Helper to create a tool result event
82
+ */
83
+ export function toolResult(toolCallId: string, toolName: string, output: any): MockStreamEvent {
84
+ return {
85
+ type: "tool-result",
86
+ toolCallId,
87
+ toolName,
88
+ output,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Helper to create a finish event
94
+ */
95
+ export function finish(): MockStreamEvent {
96
+ return {
97
+ type: "finish",
98
+ finishReason: "stop",
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Helper to create an error event
104
+ */
105
+ export function error(errorMessage: string): MockStreamEvent {
106
+ return {
107
+ type: "error",
108
+ error: new Error(errorMessage),
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Collects all events from an Observable into an array
114
+ */
115
+ export async function collectEvents<T>(observable: { subscribe: (observer: any) => any }): Promise<T[]> {
116
+ return new Promise((resolve, reject) => {
117
+ const events: T[] = [];
118
+ const subscription = observable.subscribe({
119
+ next: (event: T) => events.push(event),
120
+ error: (err: any) => reject(err),
121
+ complete: () => resolve(events),
122
+ });
123
+
124
+ // Set a timeout to prevent hanging tests
125
+ setTimeout(() => {
126
+ subscription.unsubscribe();
127
+ reject(new Error("Observable did not complete within timeout"));
128
+ }, 5000);
129
+ });
130
+ }