@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,566 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { z } from "zod";
3
+ import { BasicAgent, defineTool, type ToolDefinition } from "../index";
4
+ import { EventType, type RunAgentInput } from "@ag-ui/client";
5
+ import { streamText } from "ai";
6
+ import {
7
+ mockStreamTextResponse,
8
+ textStart,
9
+ textDelta,
10
+ finish,
11
+ collectEvents,
12
+ toolCallStreamingStart,
13
+ toolCallDelta,
14
+ toolCall,
15
+ toolResult,
16
+ } from "./test-helpers";
17
+
18
+ // Mock the ai module
19
+ vi.mock("ai", () => ({
20
+ streamText: vi.fn(),
21
+ tool: vi.fn((config) => config),
22
+ }));
23
+
24
+ // Mock the SDK clients
25
+ vi.mock("@ai-sdk/openai", () => ({
26
+ createOpenAI: vi.fn(() => (modelId: string) => ({
27
+ modelId,
28
+ provider: "openai",
29
+ })),
30
+ }));
31
+
32
+ vi.mock("@ai-sdk/anthropic", () => ({
33
+ createAnthropic: vi.fn(() => (modelId: string) => ({
34
+ modelId,
35
+ provider: "anthropic",
36
+ })),
37
+ }));
38
+
39
+ vi.mock("@ai-sdk/google", () => ({
40
+ createGoogleGenerativeAI: vi.fn(() => (modelId: string) => ({
41
+ modelId,
42
+ provider: "google",
43
+ })),
44
+ }));
45
+
46
+ describe("BasicAgent", () => {
47
+ const originalEnv = process.env;
48
+
49
+ beforeEach(() => {
50
+ vi.clearAllMocks();
51
+ process.env = { ...originalEnv };
52
+ process.env.OPENAI_API_KEY = "test-key";
53
+ process.env.ANTHROPIC_API_KEY = "test-key";
54
+ process.env.GOOGLE_API_KEY = "test-key";
55
+ });
56
+
57
+ afterEach(() => {
58
+ process.env = originalEnv;
59
+ });
60
+
61
+ describe("Basic Event Emission", () => {
62
+ it("should emit RUN_STARTED and RUN_FINISHED events", async () => {
63
+ const agent = new BasicAgent({
64
+ model: "openai/gpt-4o",
65
+ });
66
+
67
+ vi.mocked(streamText).mockReturnValue(mockStreamTextResponse([textDelta("Hello"), finish()]) as any);
68
+
69
+ const input: RunAgentInput = {
70
+ threadId: "thread1",
71
+ runId: "run1",
72
+ messages: [],
73
+ tools: [],
74
+ context: [],
75
+ state: {},
76
+ };
77
+
78
+ const events = await collectEvents(agent["run"](input));
79
+
80
+ expect(events[0]).toMatchObject({
81
+ type: EventType.RUN_STARTED,
82
+ threadId: "thread1",
83
+ runId: "run1",
84
+ });
85
+
86
+ expect(events[events.length - 1]).toMatchObject({
87
+ type: EventType.RUN_FINISHED,
88
+ threadId: "thread1",
89
+ runId: "run1",
90
+ });
91
+ });
92
+
93
+ it("should emit TEXT_MESSAGE_CHUNK events for text deltas", async () => {
94
+ const agent = new BasicAgent({
95
+ model: "openai/gpt-4o",
96
+ });
97
+
98
+ vi.mocked(streamText).mockReturnValue(
99
+ mockStreamTextResponse([textDelta("Hello"), textDelta(" world"), finish()]) as any,
100
+ );
101
+
102
+ const input: RunAgentInput = {
103
+ threadId: "thread1",
104
+ runId: "run1",
105
+ messages: [],
106
+ tools: [],
107
+ context: [],
108
+ state: {},
109
+ };
110
+
111
+ const events = await collectEvents(agent["run"](input));
112
+
113
+ const textEvents = events.filter((e: any) => e.type === EventType.TEXT_MESSAGE_CHUNK);
114
+ expect(textEvents).toHaveLength(2);
115
+ expect(textEvents[0]).toMatchObject({
116
+ type: EventType.TEXT_MESSAGE_CHUNK,
117
+ role: "assistant",
118
+ delta: "Hello",
119
+ });
120
+ expect(textEvents[1]).toMatchObject({
121
+ type: EventType.TEXT_MESSAGE_CHUNK,
122
+ delta: " world",
123
+ });
124
+ });
125
+
126
+ it("should generate unique messageId when provider returns id '0'", async () => {
127
+ const agent = new BasicAgent({
128
+ model: "openai/gpt-4o",
129
+ });
130
+
131
+ vi.mocked(streamText).mockReturnValue(
132
+ mockStreamTextResponse([
133
+ textStart("0"), // Simulate Google Gemini returning "0"
134
+ textDelta("First message"),
135
+ finish(),
136
+ ]) as any,
137
+ );
138
+
139
+ const input: RunAgentInput = {
140
+ threadId: "thread1",
141
+ runId: "run1",
142
+ messages: [],
143
+ tools: [],
144
+ context: [],
145
+ state: {},
146
+ };
147
+
148
+ const events = await collectEvents(agent["run"](input));
149
+
150
+ const textEvents = events.filter((e: any) => e.type === EventType.TEXT_MESSAGE_CHUNK);
151
+ expect(textEvents).toHaveLength(1);
152
+
153
+ // Verify that messageId is NOT "0" - should be a UUID
154
+ expect(textEvents[0].messageId).not.toBe("0");
155
+ expect(textEvents[0].messageId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
156
+ });
157
+
158
+ it("should use provider-supplied messageId when it's not '0'", async () => {
159
+ const agent = new BasicAgent({
160
+ model: "openai/gpt-4o",
161
+ });
162
+
163
+ const validId = "msg_abc123";
164
+ vi.mocked(streamText).mockReturnValue(
165
+ mockStreamTextResponse([
166
+ textStart(validId), // Valid ID from provider
167
+ textDelta("Test message"),
168
+ finish(),
169
+ ]) as any,
170
+ );
171
+
172
+ const input: RunAgentInput = {
173
+ threadId: "thread1",
174
+ runId: "run1",
175
+ messages: [],
176
+ tools: [],
177
+ context: [],
178
+ state: {},
179
+ };
180
+
181
+ const events = await collectEvents(agent["run"](input));
182
+
183
+ const textEvents = events.filter((e: any) => e.type === EventType.TEXT_MESSAGE_CHUNK);
184
+ expect(textEvents).toHaveLength(1);
185
+
186
+ // Verify that the valid ID from provider is used
187
+ expect(textEvents[0].messageId).toBe(validId);
188
+ });
189
+ });
190
+
191
+ describe("Tool Call Events", () => {
192
+ it("should emit tool call lifecycle events", async () => {
193
+ const agent = new BasicAgent({
194
+ model: "openai/gpt-4o",
195
+ });
196
+
197
+ vi.mocked(streamText).mockReturnValue(
198
+ mockStreamTextResponse([
199
+ toolCallStreamingStart("call1", "testTool"),
200
+ toolCallDelta("call1", '{"arg'),
201
+ toolCallDelta("call1", '":"val"}'),
202
+ toolCall("call1", "testTool", { arg: "val" }),
203
+ toolResult("call1", "testTool", { result: "success" }),
204
+ finish(),
205
+ ]) as any,
206
+ );
207
+
208
+ const input: RunAgentInput = {
209
+ threadId: "thread1",
210
+ runId: "run1",
211
+ messages: [],
212
+ tools: [],
213
+ context: [],
214
+ state: {},
215
+ };
216
+
217
+ const events = await collectEvents(agent["run"](input));
218
+
219
+ // Check for TOOL_CALL_START
220
+ const startEvent = events.find((e: any) => e.type === EventType.TOOL_CALL_START);
221
+ expect(startEvent).toMatchObject({
222
+ type: EventType.TOOL_CALL_START,
223
+ toolCallId: "call1",
224
+ toolCallName: "testTool",
225
+ });
226
+
227
+ // Check for TOOL_CALL_ARGS
228
+ const argsEvents = events.filter((e: any) => e.type === EventType.TOOL_CALL_ARGS);
229
+ expect(argsEvents).toHaveLength(2);
230
+
231
+ // Check for TOOL_CALL_END
232
+ const endEvent = events.find((e: any) => e.type === EventType.TOOL_CALL_END);
233
+ expect(endEvent).toMatchObject({
234
+ type: EventType.TOOL_CALL_END,
235
+ toolCallId: "call1",
236
+ });
237
+
238
+ // Check for TOOL_CALL_RESULT
239
+ const resultEvent = events.find((e: any) => e.type === EventType.TOOL_CALL_RESULT);
240
+ expect(resultEvent).toMatchObject({
241
+ type: EventType.TOOL_CALL_RESULT,
242
+ role: "tool",
243
+ toolCallId: "call1",
244
+ });
245
+ });
246
+ });
247
+
248
+ describe("Prompt Building", () => {
249
+ it("should not add system message when no prompt, context, or state", async () => {
250
+ const agent = new BasicAgent({
251
+ model: "openai/gpt-4o",
252
+ });
253
+
254
+ vi.mocked(streamText).mockReturnValue(mockStreamTextResponse([finish()]) as any);
255
+
256
+ const input: RunAgentInput = {
257
+ threadId: "thread1",
258
+ runId: "run1",
259
+ messages: [{ id: "1", role: "user", content: "Hello" }],
260
+ tools: [],
261
+ context: [],
262
+ state: {},
263
+ };
264
+
265
+ await collectEvents(agent["run"](input));
266
+
267
+ const callArgs = vi.mocked(streamText).mock.calls[0][0];
268
+ expect(callArgs.messages).toHaveLength(1);
269
+ expect(callArgs.messages[0].role).toBe("user");
270
+ });
271
+
272
+ it("should prepend system message with config prompt", async () => {
273
+ const agent = new BasicAgent({
274
+ model: "openai/gpt-4o",
275
+ prompt: "You are a helpful assistant.",
276
+ });
277
+
278
+ vi.mocked(streamText).mockReturnValue(mockStreamTextResponse([finish()]) as any);
279
+
280
+ const input: RunAgentInput = {
281
+ threadId: "thread1",
282
+ runId: "run1",
283
+ messages: [{ id: "1", role: "user", content: "Hello" }],
284
+ tools: [],
285
+ context: [],
286
+ state: {},
287
+ };
288
+
289
+ await collectEvents(agent["run"](input));
290
+
291
+ const callArgs = vi.mocked(streamText).mock.calls[0][0];
292
+ expect(callArgs.messages).toHaveLength(2);
293
+ expect(callArgs.messages[0]).toMatchObject({
294
+ role: "system",
295
+ content: "You are a helpful assistant.",
296
+ });
297
+ });
298
+
299
+ it("should include context in system message", async () => {
300
+ const agent = new BasicAgent({
301
+ model: "openai/gpt-4o",
302
+ });
303
+
304
+ vi.mocked(streamText).mockReturnValue(mockStreamTextResponse([finish()]) as any);
305
+
306
+ const input: RunAgentInput = {
307
+ threadId: "thread1",
308
+ runId: "run1",
309
+ messages: [],
310
+ tools: [],
311
+ context: [
312
+ { description: "User Name", value: "John Doe" },
313
+ { description: "Location", value: "New York" },
314
+ ],
315
+ state: {},
316
+ };
317
+
318
+ await collectEvents(agent["run"](input));
319
+
320
+ const callArgs = vi.mocked(streamText).mock.calls[0][0];
321
+ const systemMessage = callArgs.messages[0];
322
+ expect(systemMessage.role).toBe("system");
323
+ expect(systemMessage.content).toContain("Context from the application");
324
+ expect(systemMessage.content).toContain("User Name");
325
+ expect(systemMessage.content).toContain("John Doe");
326
+ expect(systemMessage.content).toContain("Location");
327
+ expect(systemMessage.content).toContain("New York");
328
+ });
329
+
330
+ it("should include state in system message", async () => {
331
+ const agent = new BasicAgent({
332
+ model: "openai/gpt-4o",
333
+ });
334
+
335
+ vi.mocked(streamText).mockReturnValue(mockStreamTextResponse([finish()]) as any);
336
+
337
+ const input: RunAgentInput = {
338
+ threadId: "thread1",
339
+ runId: "run1",
340
+ messages: [],
341
+ tools: [],
342
+ context: [],
343
+ state: { counter: 0, items: ["a", "b"] },
344
+ };
345
+
346
+ await collectEvents(agent["run"](input));
347
+
348
+ const callArgs = vi.mocked(streamText).mock.calls[0][0];
349
+ const systemMessage = callArgs.messages[0];
350
+ expect(systemMessage.role).toBe("system");
351
+ expect(systemMessage.content).toContain("Application State");
352
+ expect(systemMessage.content).toContain("AGUISendStateSnapshot");
353
+ expect(systemMessage.content).toContain("AGUISendStateDelta");
354
+ expect(systemMessage.content).toContain('"counter": 0');
355
+ expect(systemMessage.content).toContain('"items"');
356
+ });
357
+
358
+ it("should combine prompt, context, and state", async () => {
359
+ const agent = new BasicAgent({
360
+ model: "openai/gpt-4o",
361
+ prompt: "You are helpful.",
362
+ });
363
+
364
+ vi.mocked(streamText).mockReturnValue(mockStreamTextResponse([finish()]) as any);
365
+
366
+ const input: RunAgentInput = {
367
+ threadId: "thread1",
368
+ runId: "run1",
369
+ messages: [],
370
+ tools: [],
371
+ context: [{ description: "Context", value: "Data" }],
372
+ state: { value: 1 },
373
+ };
374
+
375
+ await collectEvents(agent["run"](input));
376
+
377
+ const callArgs = vi.mocked(streamText).mock.calls[0][0];
378
+ const systemMessage = callArgs.messages[0];
379
+ expect(systemMessage.content).toContain("You are helpful.");
380
+ expect(systemMessage.content).toContain("Context from the application");
381
+ expect(systemMessage.content).toContain("Application State");
382
+
383
+ // Check order: prompt, then context, then state
384
+ const promptIndex = systemMessage.content.indexOf("You are helpful.");
385
+ const contextIndex = systemMessage.content.indexOf("Context from the application");
386
+ const stateIndex = systemMessage.content.indexOf("Application State");
387
+
388
+ expect(promptIndex).toBeLessThan(contextIndex);
389
+ expect(contextIndex).toBeLessThan(stateIndex);
390
+ });
391
+ });
392
+
393
+ describe("Tool Configuration", () => {
394
+ it("should include tools from config", async () => {
395
+ const tool1 = defineTool({
396
+ name: "configTool",
397
+ description: "A config tool",
398
+ parameters: z.object({ input: z.string() }),
399
+ execute: async () => ({ result: "ok" }),
400
+ });
401
+
402
+ const agent = new BasicAgent({
403
+ model: "openai/gpt-4o",
404
+ tools: [tool1],
405
+ });
406
+
407
+ vi.mocked(streamText).mockReturnValue(mockStreamTextResponse([finish()]) as any);
408
+
409
+ const input: RunAgentInput = {
410
+ threadId: "thread1",
411
+ runId: "run1",
412
+ messages: [],
413
+ tools: [],
414
+ context: [],
415
+ state: {},
416
+ };
417
+
418
+ await collectEvents(agent["run"](input));
419
+
420
+ const callArgs = vi.mocked(streamText).mock.calls[0][0];
421
+ expect(callArgs.tools).toHaveProperty("configTool");
422
+ });
423
+
424
+ it("should merge config tools with input tools", async () => {
425
+ const configTool = defineTool({
426
+ name: "configTool",
427
+ description: "From config",
428
+ parameters: z.object({}),
429
+ execute: async () => ({ result: "ok" }),
430
+ });
431
+
432
+ const agent = new BasicAgent({
433
+ model: "openai/gpt-4o",
434
+ tools: [configTool],
435
+ });
436
+
437
+ vi.mocked(streamText).mockReturnValue(mockStreamTextResponse([finish()]) as any);
438
+
439
+ const input: RunAgentInput = {
440
+ threadId: "thread1",
441
+ runId: "run1",
442
+ messages: [],
443
+ tools: [
444
+ {
445
+ name: "inputTool",
446
+ description: "From input",
447
+ parameters: { type: "object", properties: {} },
448
+ },
449
+ ],
450
+ context: [],
451
+ state: {},
452
+ };
453
+
454
+ await collectEvents(agent["run"](input));
455
+
456
+ const callArgs = vi.mocked(streamText).mock.calls[0][0];
457
+ expect(callArgs.tools).toHaveProperty("configTool");
458
+ expect(callArgs.tools).toHaveProperty("inputTool");
459
+ });
460
+
461
+ it("should always include state update tools", async () => {
462
+ const agent = new BasicAgent({
463
+ model: "openai/gpt-4o",
464
+ });
465
+
466
+ vi.mocked(streamText).mockReturnValue(mockStreamTextResponse([finish()]) as any);
467
+
468
+ const input: RunAgentInput = {
469
+ threadId: "thread1",
470
+ runId: "run1",
471
+ messages: [],
472
+ tools: [],
473
+ context: [],
474
+ state: {},
475
+ };
476
+
477
+ await collectEvents(agent["run"](input));
478
+
479
+ const callArgs = vi.mocked(streamText).mock.calls[0][0];
480
+ expect(callArgs.tools).toHaveProperty("AGUISendStateSnapshot");
481
+ expect(callArgs.tools).toHaveProperty("AGUISendStateDelta");
482
+ });
483
+ });
484
+
485
+ describe("Property Overrides", () => {
486
+ it("should respect overridable properties", async () => {
487
+ const agent = new BasicAgent({
488
+ model: "openai/gpt-4o",
489
+ temperature: 0.5,
490
+ overridableProperties: ["temperature"],
491
+ });
492
+
493
+ vi.mocked(streamText).mockReturnValue(mockStreamTextResponse([finish()]) as any);
494
+
495
+ const input: RunAgentInput = {
496
+ threadId: "thread1",
497
+ runId: "run1",
498
+ messages: [],
499
+ tools: [],
500
+ context: [],
501
+ state: {},
502
+ forwardedProps: { temperature: 0.9 },
503
+ };
504
+
505
+ await collectEvents(agent["run"](input));
506
+
507
+ const callArgs = vi.mocked(streamText).mock.calls[0][0];
508
+ expect(callArgs.temperature).toBe(0.9);
509
+ });
510
+
511
+ it("should ignore non-overridable properties", async () => {
512
+ const agent = new BasicAgent({
513
+ model: "openai/gpt-4o",
514
+ temperature: 0.5,
515
+ overridableProperties: [], // No properties can be overridden
516
+ });
517
+
518
+ vi.mocked(streamText).mockReturnValue(mockStreamTextResponse([finish()]) as any);
519
+
520
+ const input: RunAgentInput = {
521
+ threadId: "thread1",
522
+ runId: "run1",
523
+ messages: [],
524
+ tools: [],
525
+ context: [],
526
+ state: {},
527
+ forwardedProps: { temperature: 0.9 },
528
+ };
529
+
530
+ await collectEvents(agent["run"](input));
531
+
532
+ const callArgs = vi.mocked(streamText).mock.calls[0][0];
533
+ expect(callArgs.temperature).toBe(0.5); // Original value, not overridden
534
+ });
535
+ });
536
+
537
+ describe("Error Handling", () => {
538
+ it("should emit RUN_ERROR event on failure", async () => {
539
+ const agent = new BasicAgent({
540
+ model: "openai/gpt-4o",
541
+ });
542
+
543
+ vi.mocked(streamText).mockImplementation(() => {
544
+ throw new Error("Test error");
545
+ });
546
+
547
+ const input: RunAgentInput = {
548
+ threadId: "thread1",
549
+ runId: "run1",
550
+ messages: [],
551
+ tools: [],
552
+ context: [],
553
+ state: {},
554
+ };
555
+
556
+ try {
557
+ await collectEvents(agent["run"](input));
558
+ expect.fail("Should have thrown");
559
+ } catch (error: any) {
560
+ // Error is expected - check that we got a RUN_ERROR event
561
+ // Note: The error is thrown after emitting the event
562
+ expect(error.message).toContain("Test error");
563
+ }
564
+ });
565
+ });
566
+ });