@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.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/dist/index.d.mts +199 -0
- package/dist/index.d.ts +199 -0
- package/dist/index.js +629 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +606 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +53 -0
- package/src/__tests__/basic-agent.test.ts +566 -0
- package/src/__tests__/config-tools-execution.test.ts +503 -0
- package/src/__tests__/property-overrides.test.ts +559 -0
- package/src/__tests__/state-tools.test.ts +391 -0
- package/src/__tests__/test-helpers.ts +130 -0
- package/src/__tests__/utils.test.ts +438 -0
- package/src/index.ts +1003 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.ts +13 -0
|
@@ -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
|
+
}
|