@copilotkit/runtime 1.55.2-next.0 → 1.55.2-next.1
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/dist/agent/converters/aisdk.cjs +215 -0
- package/dist/agent/converters/aisdk.cjs.map +1 -0
- package/dist/agent/converters/aisdk.d.cts +18 -0
- package/dist/agent/converters/aisdk.d.cts.map +1 -0
- package/dist/agent/converters/aisdk.d.mts +18 -0
- package/dist/agent/converters/aisdk.d.mts.map +1 -0
- package/dist/agent/converters/aisdk.mjs +214 -0
- package/dist/agent/converters/aisdk.mjs.map +1 -0
- package/dist/agent/converters/index.d.mts +3 -0
- package/dist/agent/converters/tanstack.cjs +180 -0
- package/dist/agent/converters/tanstack.cjs.map +1 -0
- package/dist/agent/converters/tanstack.d.cts +68 -0
- package/dist/agent/converters/tanstack.d.cts.map +1 -0
- package/dist/agent/converters/tanstack.d.mts +68 -0
- package/dist/agent/converters/tanstack.d.mts.map +1 -0
- package/dist/agent/converters/tanstack.mjs +178 -0
- package/dist/agent/converters/tanstack.mjs.map +1 -0
- package/dist/agent/index.cjs +111 -17
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +61 -4
- package/dist/agent/index.d.cts.map +1 -1
- package/dist/agent/index.d.mts +62 -4
- package/dist/agent/index.d.mts.map +1 -1
- package/dist/agent/index.mjs +111 -17
- package/dist/agent/index.mjs.map +1 -1
- package/dist/lib/integrations/nextjs/pages-router.cjs.map +1 -1
- package/dist/lib/integrations/nextjs/pages-router.d.cts.map +1 -1
- package/dist/lib/integrations/nextjs/pages-router.d.mts.map +1 -1
- package/dist/lib/integrations/nextjs/pages-router.mjs.map +1 -1
- package/dist/lib/runtime/copilot-runtime.cjs +4 -2
- package/dist/lib/runtime/copilot-runtime.cjs.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.cts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.mts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.mjs +4 -2
- package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
- package/dist/lib/runtime/mcp-tools-utils.cjs +1 -1
- package/dist/lib/runtime/mcp-tools-utils.cjs.map +1 -1
- package/dist/lib/runtime/mcp-tools-utils.mjs +1 -1
- package/dist/lib/runtime/mcp-tools-utils.mjs.map +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/service-adapters/anthropic/utils.cjs +1 -1
- package/dist/service-adapters/anthropic/utils.cjs.map +1 -1
- package/dist/service-adapters/anthropic/utils.mjs +1 -1
- package/dist/service-adapters/anthropic/utils.mjs.map +1 -1
- package/dist/service-adapters/openai/utils.cjs +1 -1
- package/dist/service-adapters/openai/utils.cjs.map +1 -1
- package/dist/service-adapters/openai/utils.mjs +1 -1
- package/dist/service-adapters/openai/utils.mjs.map +1 -1
- package/dist/v2/index.cjs +5 -0
- package/dist/v2/index.d.cts +4 -2
- package/dist/v2/index.d.mts +4 -2
- package/dist/v2/index.mjs +3 -1
- package/package.json +2 -2
- package/src/agent/__tests__/agent-test-helpers.ts +446 -0
- package/src/agent/__tests__/agent.test.ts +593 -0
- package/src/agent/__tests__/converter-aisdk.test.ts +692 -0
- package/src/agent/__tests__/converter-custom.test.ts +319 -0
- package/src/agent/__tests__/converter-tanstack-input.test.ts +211 -0
- package/src/agent/__tests__/converter-tanstack.test.ts +314 -0
- package/src/agent/__tests__/multimodal-tanstack.test.ts +284 -0
- package/src/agent/__tests__/test-helpers.ts +12 -8
- package/src/agent/converters/aisdk.ts +326 -0
- package/src/agent/converters/index.ts +7 -0
- package/src/agent/converters/tanstack.ts +286 -0
- package/src/agent/index.ts +245 -26
- package/src/lib/integrations/nextjs/pages-router.ts +1 -0
- package/src/lib/runtime/copilot-runtime.ts +21 -12
- package/src/lib/runtime/mcp-tools-utils.ts +1 -1
- package/src/service-adapters/anthropic/utils.ts +1 -1
- package/src/service-adapters/openai/utils.ts +1 -1
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { EventType } from "@ag-ui/client";
|
|
3
|
+
import {
|
|
4
|
+
createAgent,
|
|
5
|
+
createDefaultInput,
|
|
6
|
+
collectEvents,
|
|
7
|
+
collectEventsIncludingErrors,
|
|
8
|
+
expectLifecycleWrapped,
|
|
9
|
+
expectEventSequence,
|
|
10
|
+
eventField,
|
|
11
|
+
textStart,
|
|
12
|
+
textDelta,
|
|
13
|
+
toolCallStreamingStart,
|
|
14
|
+
toolCallDelta,
|
|
15
|
+
toolCall,
|
|
16
|
+
toolResult,
|
|
17
|
+
reasoningStart,
|
|
18
|
+
reasoningDelta,
|
|
19
|
+
reasoningEnd,
|
|
20
|
+
finish,
|
|
21
|
+
} from "./agent-test-helpers";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Basic Event Emission
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
describe("AI SDK Converter", () => {
|
|
28
|
+
describe("Basic Event Emission", () => {
|
|
29
|
+
it("text delta emits TEXT_MESSAGE_CHUNK with correct role, messageId, and delta", async () => {
|
|
30
|
+
const agent = createAgent("aisdk", [textDelta("Hello"), finish()]);
|
|
31
|
+
const input = createDefaultInput();
|
|
32
|
+
const events = await collectEvents(agent.run(input));
|
|
33
|
+
|
|
34
|
+
expectLifecycleWrapped(events);
|
|
35
|
+
|
|
36
|
+
const textChunks = events.filter(
|
|
37
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
38
|
+
);
|
|
39
|
+
expect(textChunks).toHaveLength(1);
|
|
40
|
+
|
|
41
|
+
expect(eventField<string>(textChunks[0], "role")).toBe("assistant");
|
|
42
|
+
expect(eventField<string>(textChunks[0], "delta")).toBe("Hello");
|
|
43
|
+
expect(eventField<string>(textChunks[0], "messageId")).toBeDefined();
|
|
44
|
+
expect(typeof eventField<string>(textChunks[0], "messageId")).toBe(
|
|
45
|
+
"string",
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("text-start with provider id uses that id as messageId", async () => {
|
|
50
|
+
const agent = createAgent("aisdk", [
|
|
51
|
+
textStart("custom-msg-id"),
|
|
52
|
+
textDelta("Hi"),
|
|
53
|
+
finish(),
|
|
54
|
+
]);
|
|
55
|
+
const input = createDefaultInput();
|
|
56
|
+
const events = await collectEvents(agent.run(input));
|
|
57
|
+
|
|
58
|
+
const chunk = events.find(
|
|
59
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
60
|
+
)!;
|
|
61
|
+
expect(eventField<string>(chunk, "messageId")).toBe("custom-msg-id");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('text-start with "0" generates a unique messageId (not "0")', async () => {
|
|
65
|
+
const agent = createAgent("aisdk", [
|
|
66
|
+
textStart("0"),
|
|
67
|
+
textDelta("Hi"),
|
|
68
|
+
finish(),
|
|
69
|
+
]);
|
|
70
|
+
const input = createDefaultInput();
|
|
71
|
+
const events = await collectEvents(agent.run(input));
|
|
72
|
+
|
|
73
|
+
const chunk = events.find(
|
|
74
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
75
|
+
)!;
|
|
76
|
+
expect(eventField<string>(chunk, "messageId")).not.toBe("0");
|
|
77
|
+
expect(eventField<string>(chunk, "messageId")).toBeDefined();
|
|
78
|
+
expect(eventField<string>(chunk, "messageId").length).toBeGreaterThan(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("multiple text deltas share the same messageId", async () => {
|
|
82
|
+
const agent = createAgent("aisdk", [
|
|
83
|
+
textDelta("Hello "),
|
|
84
|
+
textDelta("world"),
|
|
85
|
+
finish(),
|
|
86
|
+
]);
|
|
87
|
+
const input = createDefaultInput();
|
|
88
|
+
const events = await collectEvents(agent.run(input));
|
|
89
|
+
|
|
90
|
+
const textChunks = events.filter(
|
|
91
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
92
|
+
);
|
|
93
|
+
expect(textChunks).toHaveLength(2);
|
|
94
|
+
expect(eventField<string>(textChunks[0], "messageId")).toBe(
|
|
95
|
+
eventField<string>(textChunks[1], "messageId"),
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("empty stream (only finish) emits only RUN_STARTED + RUN_FINISHED", async () => {
|
|
100
|
+
const agent = createAgent("aisdk", [finish()]);
|
|
101
|
+
const input = createDefaultInput();
|
|
102
|
+
const events = await collectEvents(agent.run(input));
|
|
103
|
+
|
|
104
|
+
expectEventSequence(events, [
|
|
105
|
+
EventType.RUN_STARTED,
|
|
106
|
+
EventType.RUN_FINISHED,
|
|
107
|
+
]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Tool Call Events
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
describe("Tool Call Events", () => {
|
|
116
|
+
it("streamed tool call emits correct START/ARGS/END/RESULT events", async () => {
|
|
117
|
+
const agent = createAgent("aisdk", [
|
|
118
|
+
toolCallStreamingStart("tc-1", "myTool"),
|
|
119
|
+
toolCallDelta("tc-1", '{"key":'),
|
|
120
|
+
toolCallDelta("tc-1", '"value"}'),
|
|
121
|
+
toolCall("tc-1", "myTool"),
|
|
122
|
+
toolResult("tc-1", "myTool", { result: "ok" }),
|
|
123
|
+
finish(),
|
|
124
|
+
]);
|
|
125
|
+
const input = createDefaultInput();
|
|
126
|
+
const events = await collectEvents(agent.run(input));
|
|
127
|
+
|
|
128
|
+
expectLifecycleWrapped(events);
|
|
129
|
+
|
|
130
|
+
// Check the sequence of tool events
|
|
131
|
+
const toolEvents = events.filter(
|
|
132
|
+
(e) =>
|
|
133
|
+
e.type === EventType.TOOL_CALL_START ||
|
|
134
|
+
e.type === EventType.TOOL_CALL_ARGS ||
|
|
135
|
+
e.type === EventType.TOOL_CALL_END ||
|
|
136
|
+
e.type === EventType.TOOL_CALL_RESULT,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expectEventSequence(toolEvents, [
|
|
140
|
+
EventType.TOOL_CALL_START,
|
|
141
|
+
EventType.TOOL_CALL_ARGS,
|
|
142
|
+
EventType.TOOL_CALL_ARGS,
|
|
143
|
+
EventType.TOOL_CALL_END,
|
|
144
|
+
EventType.TOOL_CALL_RESULT,
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
// Verify TOOL_CALL_START details
|
|
148
|
+
expect(eventField<string>(toolEvents[0], "toolCallId")).toBe("tc-1");
|
|
149
|
+
expect(eventField<string>(toolEvents[0], "toolCallName")).toBe("myTool");
|
|
150
|
+
|
|
151
|
+
// Verify TOOL_CALL_ARGS deltas
|
|
152
|
+
const argsEvts = toolEvents.filter(
|
|
153
|
+
(e) => e.type === EventType.TOOL_CALL_ARGS,
|
|
154
|
+
);
|
|
155
|
+
expect(eventField<string>(argsEvts[0], "delta")).toBe('{"key":');
|
|
156
|
+
expect(eventField<string>(argsEvts[1], "delta")).toBe('"value"}');
|
|
157
|
+
|
|
158
|
+
// Verify TOOL_CALL_END
|
|
159
|
+
expect(eventField<string>(toolEvents[2 + 1], "toolCallId")).toBe("tc-1");
|
|
160
|
+
|
|
161
|
+
// Verify TOOL_CALL_RESULT
|
|
162
|
+
expect(eventField<string>(toolEvents[4], "toolCallId")).toBe("tc-1");
|
|
163
|
+
expect(JSON.parse(eventField<string>(toolEvents[4], "content"))).toEqual({
|
|
164
|
+
result: "ok",
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("non-streamed tool call (tool-call with input, no prior tool-input-start) emits START + ARGS + END", async () => {
|
|
169
|
+
const agent = createAgent("aisdk", [
|
|
170
|
+
toolCall("tc-2", "directTool", { foo: "bar" }),
|
|
171
|
+
finish(),
|
|
172
|
+
]);
|
|
173
|
+
const input = createDefaultInput();
|
|
174
|
+
const events = await collectEvents(agent.run(input));
|
|
175
|
+
|
|
176
|
+
expectLifecycleWrapped(events);
|
|
177
|
+
|
|
178
|
+
const toolEvents = events.filter(
|
|
179
|
+
(e) =>
|
|
180
|
+
e.type === EventType.TOOL_CALL_START ||
|
|
181
|
+
e.type === EventType.TOOL_CALL_ARGS ||
|
|
182
|
+
e.type === EventType.TOOL_CALL_END,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expectEventSequence(toolEvents, [
|
|
186
|
+
EventType.TOOL_CALL_START,
|
|
187
|
+
EventType.TOOL_CALL_ARGS,
|
|
188
|
+
EventType.TOOL_CALL_END,
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
expect(eventField<string>(toolEvents[0], "toolCallId")).toBe("tc-2");
|
|
192
|
+
expect(eventField<string>(toolEvents[0], "toolCallName")).toBe(
|
|
193
|
+
"directTool",
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
expect(JSON.parse(eventField<string>(toolEvents[1], "delta"))).toEqual({
|
|
197
|
+
foo: "bar",
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("no duplicate START after tool-input-start followed by tool-call", async () => {
|
|
202
|
+
const agent = createAgent("aisdk", [
|
|
203
|
+
toolCallStreamingStart("tc-3", "myTool"),
|
|
204
|
+
toolCallDelta("tc-3", "{}"),
|
|
205
|
+
toolCall("tc-3", "myTool"),
|
|
206
|
+
finish(),
|
|
207
|
+
]);
|
|
208
|
+
const input = createDefaultInput();
|
|
209
|
+
const events = await collectEvents(agent.run(input));
|
|
210
|
+
|
|
211
|
+
const startEvents = events.filter(
|
|
212
|
+
(e) =>
|
|
213
|
+
e.type === EventType.TOOL_CALL_START &&
|
|
214
|
+
eventField<string>(e, "toolCallId") === "tc-3",
|
|
215
|
+
);
|
|
216
|
+
expect(startEvents).toHaveLength(1);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("multiple concurrent tool calls have events correctly paired by toolCallId", async () => {
|
|
220
|
+
const agent = createAgent("aisdk", [
|
|
221
|
+
toolCallStreamingStart("tc-a", "toolA"),
|
|
222
|
+
toolCallStreamingStart("tc-b", "toolB"),
|
|
223
|
+
toolCallDelta("tc-a", '{"a":1}'),
|
|
224
|
+
toolCallDelta("tc-b", '{"b":2}'),
|
|
225
|
+
toolCall("tc-a", "toolA"),
|
|
226
|
+
toolCall("tc-b", "toolB"),
|
|
227
|
+
toolResult("tc-a", "toolA", "resultA"),
|
|
228
|
+
toolResult("tc-b", "toolB", "resultB"),
|
|
229
|
+
finish(),
|
|
230
|
+
]);
|
|
231
|
+
const input = createDefaultInput();
|
|
232
|
+
const events = await collectEvents(agent.run(input));
|
|
233
|
+
|
|
234
|
+
expectLifecycleWrapped(events);
|
|
235
|
+
|
|
236
|
+
// Verify each tool call has its own START
|
|
237
|
+
const startsA = events.filter(
|
|
238
|
+
(e) =>
|
|
239
|
+
e.type === EventType.TOOL_CALL_START &&
|
|
240
|
+
eventField<string>(e, "toolCallId") === "tc-a",
|
|
241
|
+
);
|
|
242
|
+
const startsB = events.filter(
|
|
243
|
+
(e) =>
|
|
244
|
+
e.type === EventType.TOOL_CALL_START &&
|
|
245
|
+
eventField<string>(e, "toolCallId") === "tc-b",
|
|
246
|
+
);
|
|
247
|
+
expect(startsA).toHaveLength(1);
|
|
248
|
+
expect(startsB).toHaveLength(1);
|
|
249
|
+
|
|
250
|
+
// Verify args are correctly paired
|
|
251
|
+
const argsA = events.filter(
|
|
252
|
+
(e) =>
|
|
253
|
+
e.type === EventType.TOOL_CALL_ARGS &&
|
|
254
|
+
eventField<string>(e, "toolCallId") === "tc-a",
|
|
255
|
+
);
|
|
256
|
+
const argsB = events.filter(
|
|
257
|
+
(e) =>
|
|
258
|
+
e.type === EventType.TOOL_CALL_ARGS &&
|
|
259
|
+
eventField<string>(e, "toolCallId") === "tc-b",
|
|
260
|
+
);
|
|
261
|
+
expect(eventField<string>(argsA[0], "delta")).toBe('{"a":1}');
|
|
262
|
+
expect(eventField<string>(argsB[0], "delta")).toBe('{"b":2}');
|
|
263
|
+
|
|
264
|
+
// Verify results are correctly paired
|
|
265
|
+
const resultsA = events.filter(
|
|
266
|
+
(e) =>
|
|
267
|
+
e.type === EventType.TOOL_CALL_RESULT &&
|
|
268
|
+
eventField<string>(e, "toolCallId") === "tc-a",
|
|
269
|
+
);
|
|
270
|
+
const resultsB = events.filter(
|
|
271
|
+
(e) =>
|
|
272
|
+
e.type === EventType.TOOL_CALL_RESULT &&
|
|
273
|
+
eventField<string>(e, "toolCallId") === "tc-b",
|
|
274
|
+
);
|
|
275
|
+
expect(JSON.parse(eventField<string>(resultsA[0], "content"))).toBe(
|
|
276
|
+
"resultA",
|
|
277
|
+
);
|
|
278
|
+
expect(JSON.parse(eventField<string>(resultsB[0], "content"))).toBe(
|
|
279
|
+
"resultB",
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// Reasoning Events
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
describe("Reasoning Events", () => {
|
|
289
|
+
it("full reasoning lifecycle emits correct REASONING_START/MESSAGE_START/CONTENT/MESSAGE_END/END events", async () => {
|
|
290
|
+
const agent = createAgent("aisdk", [
|
|
291
|
+
reasoningStart("r-1"),
|
|
292
|
+
reasoningDelta("thinking..."),
|
|
293
|
+
reasoningEnd(),
|
|
294
|
+
textDelta("Answer"),
|
|
295
|
+
finish(),
|
|
296
|
+
]);
|
|
297
|
+
const input = createDefaultInput();
|
|
298
|
+
const events = await collectEvents(agent.run(input));
|
|
299
|
+
|
|
300
|
+
expectLifecycleWrapped(events);
|
|
301
|
+
|
|
302
|
+
const reasoningEvents = events.filter(
|
|
303
|
+
(e) =>
|
|
304
|
+
e.type === EventType.REASONING_START ||
|
|
305
|
+
e.type === EventType.REASONING_MESSAGE_START ||
|
|
306
|
+
e.type === EventType.REASONING_MESSAGE_CONTENT ||
|
|
307
|
+
e.type === EventType.REASONING_MESSAGE_END ||
|
|
308
|
+
e.type === EventType.REASONING_END,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
expectEventSequence(reasoningEvents, [
|
|
312
|
+
EventType.REASONING_START,
|
|
313
|
+
EventType.REASONING_MESSAGE_START,
|
|
314
|
+
EventType.REASONING_MESSAGE_CONTENT,
|
|
315
|
+
EventType.REASONING_MESSAGE_END,
|
|
316
|
+
EventType.REASONING_END,
|
|
317
|
+
]);
|
|
318
|
+
|
|
319
|
+
// Verify messageId consistency
|
|
320
|
+
expect(eventField<string>(reasoningEvents[0], "messageId")).toBe("r-1");
|
|
321
|
+
expect(eventField<string>(reasoningEvents[1], "messageId")).toBe("r-1");
|
|
322
|
+
expect(eventField<string>(reasoningEvents[1], "role")).toBe("reasoning");
|
|
323
|
+
expect(eventField<string>(reasoningEvents[2], "messageId")).toBe("r-1");
|
|
324
|
+
expect(eventField<string>(reasoningEvents[2], "delta")).toBe(
|
|
325
|
+
"thinking...",
|
|
326
|
+
);
|
|
327
|
+
expect(eventField<string>(reasoningEvents[3], "messageId")).toBe("r-1");
|
|
328
|
+
expect(eventField<string>(reasoningEvents[4], "messageId")).toBe("r-1");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("empty reasoning deltas are skipped", async () => {
|
|
332
|
+
const agent = createAgent("aisdk", [
|
|
333
|
+
reasoningStart(),
|
|
334
|
+
reasoningDelta(""),
|
|
335
|
+
reasoningDelta("actual content"),
|
|
336
|
+
reasoningDelta(""),
|
|
337
|
+
reasoningEnd(),
|
|
338
|
+
finish(),
|
|
339
|
+
]);
|
|
340
|
+
const input = createDefaultInput();
|
|
341
|
+
const events = await collectEvents(agent.run(input));
|
|
342
|
+
|
|
343
|
+
const contentEvents = events.filter(
|
|
344
|
+
(e) => e.type === EventType.REASONING_MESSAGE_CONTENT,
|
|
345
|
+
);
|
|
346
|
+
expect(contentEvents).toHaveLength(1);
|
|
347
|
+
expect(eventField<string>(contentEvents[0], "delta")).toBe(
|
|
348
|
+
"actual content",
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("auto-close reasoning before text-delta", async () => {
|
|
353
|
+
// No explicit reasoning-end — the converter should auto-close
|
|
354
|
+
const agent = createAgent("aisdk", [
|
|
355
|
+
reasoningStart("r-auto"),
|
|
356
|
+
reasoningDelta("thinking"),
|
|
357
|
+
textDelta("Answer"),
|
|
358
|
+
finish(),
|
|
359
|
+
]);
|
|
360
|
+
const input = createDefaultInput();
|
|
361
|
+
const events = await collectEvents(agent.run(input));
|
|
362
|
+
|
|
363
|
+
expectLifecycleWrapped(events);
|
|
364
|
+
|
|
365
|
+
// Reasoning should be closed before the text event
|
|
366
|
+
const types = events.map((e) => e.type);
|
|
367
|
+
const msgEndIdx = types.indexOf(EventType.REASONING_MESSAGE_END);
|
|
368
|
+
const reasoningEndIdx = types.indexOf(EventType.REASONING_END);
|
|
369
|
+
const textIdx = types.indexOf(EventType.TEXT_MESSAGE_CHUNK);
|
|
370
|
+
|
|
371
|
+
expect(msgEndIdx).toBeGreaterThan(-1);
|
|
372
|
+
expect(reasoningEndIdx).toBeGreaterThan(-1);
|
|
373
|
+
expect(textIdx).toBeGreaterThan(reasoningEndIdx);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("auto-close reasoning before tool-input-start", async () => {
|
|
377
|
+
const agent = createAgent("aisdk", [
|
|
378
|
+
reasoningStart(),
|
|
379
|
+
reasoningDelta("thinking about tools"),
|
|
380
|
+
toolCallStreamingStart("tc-r", "someTool"),
|
|
381
|
+
toolCallDelta("tc-r", "{}"),
|
|
382
|
+
toolCall("tc-r", "someTool"),
|
|
383
|
+
finish(),
|
|
384
|
+
]);
|
|
385
|
+
const input = createDefaultInput();
|
|
386
|
+
const events = await collectEvents(agent.run(input));
|
|
387
|
+
|
|
388
|
+
expectLifecycleWrapped(events);
|
|
389
|
+
|
|
390
|
+
const types = events.map((e) => e.type);
|
|
391
|
+
const reasoningEndIdx = types.indexOf(EventType.REASONING_END);
|
|
392
|
+
const toolStartIdx = types.indexOf(EventType.TOOL_CALL_START);
|
|
393
|
+
|
|
394
|
+
expect(reasoningEndIdx).toBeGreaterThan(-1);
|
|
395
|
+
expect(toolStartIdx).toBeGreaterThan(reasoningEndIdx);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("auto-close reasoning before finish", async () => {
|
|
399
|
+
const agent = createAgent("aisdk", [
|
|
400
|
+
reasoningStart(),
|
|
401
|
+
reasoningDelta("deep thought"),
|
|
402
|
+
finish(),
|
|
403
|
+
]);
|
|
404
|
+
const input = createDefaultInput();
|
|
405
|
+
const events = await collectEvents(agent.run(input));
|
|
406
|
+
|
|
407
|
+
expectLifecycleWrapped(events);
|
|
408
|
+
|
|
409
|
+
// Should contain reasoning close events
|
|
410
|
+
const types = events.map((e) => e.type);
|
|
411
|
+
expect(types).toContain(EventType.REASONING_MESSAGE_END);
|
|
412
|
+
expect(types).toContain(EventType.REASONING_END);
|
|
413
|
+
|
|
414
|
+
// They should appear before RUN_FINISHED
|
|
415
|
+
const reasoningEndIdx = types.indexOf(EventType.REASONING_END);
|
|
416
|
+
const runFinishedIdx = types.indexOf(EventType.RUN_FINISHED);
|
|
417
|
+
expect(reasoningEndIdx).toBeLessThan(runFinishedIdx);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
// State Management Tool Results
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
describe("State Management Tool Results", () => {
|
|
426
|
+
it("AGUISendStateSnapshot tool result emits STATE_SNAPSHOT event", async () => {
|
|
427
|
+
const agent = createAgent("aisdk", [
|
|
428
|
+
toolCallStreamingStart("tc-state", "AGUISendStateSnapshot"),
|
|
429
|
+
toolCallDelta("tc-state", '{"snapshot":{"count":1}}'),
|
|
430
|
+
toolCall("tc-state", "AGUISendStateSnapshot"),
|
|
431
|
+
toolResult("tc-state", "AGUISendStateSnapshot", {
|
|
432
|
+
snapshot: { count: 1 },
|
|
433
|
+
}),
|
|
434
|
+
finish(),
|
|
435
|
+
]);
|
|
436
|
+
const input = createDefaultInput();
|
|
437
|
+
const events = await collectEvents(agent.run(input));
|
|
438
|
+
|
|
439
|
+
const stateSnapshots = events.filter(
|
|
440
|
+
(e) => e.type === EventType.STATE_SNAPSHOT,
|
|
441
|
+
);
|
|
442
|
+
expect(stateSnapshots).toHaveLength(1);
|
|
443
|
+
expect(eventField(stateSnapshots[0], "snapshot")).toEqual({ count: 1 });
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("AGUISendStateDelta tool result emits STATE_DELTA event", async () => {
|
|
447
|
+
const delta = [{ op: "replace", path: "/count", value: 2 }];
|
|
448
|
+
const agent = createAgent("aisdk", [
|
|
449
|
+
toolCallStreamingStart("tc-delta", "AGUISendStateDelta"),
|
|
450
|
+
toolCallDelta("tc-delta", JSON.stringify({ delta })),
|
|
451
|
+
toolCall("tc-delta", "AGUISendStateDelta"),
|
|
452
|
+
toolResult("tc-delta", "AGUISendStateDelta", { delta }),
|
|
453
|
+
finish(),
|
|
454
|
+
]);
|
|
455
|
+
const input = createDefaultInput();
|
|
456
|
+
const events = await collectEvents(agent.run(input));
|
|
457
|
+
|
|
458
|
+
const stateDeltas = events.filter(
|
|
459
|
+
(e) => e.type === EventType.STATE_DELTA,
|
|
460
|
+
);
|
|
461
|
+
expect(stateDeltas).toHaveLength(1);
|
|
462
|
+
expect(eventField(stateDeltas[0], "delta")).toEqual(delta);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("state tool result also emits TOOL_CALL_RESULT event", async () => {
|
|
466
|
+
const agent = createAgent("aisdk", [
|
|
467
|
+
toolCallStreamingStart("tc-state2", "AGUISendStateSnapshot"),
|
|
468
|
+
toolCallDelta("tc-state2", "{}"),
|
|
469
|
+
toolCall("tc-state2", "AGUISendStateSnapshot"),
|
|
470
|
+
toolResult("tc-state2", "AGUISendStateSnapshot", {
|
|
471
|
+
snapshot: { x: 1 },
|
|
472
|
+
}),
|
|
473
|
+
finish(),
|
|
474
|
+
]);
|
|
475
|
+
const input = createDefaultInput();
|
|
476
|
+
const events = await collectEvents(agent.run(input));
|
|
477
|
+
|
|
478
|
+
const toolResults = events.filter(
|
|
479
|
+
(e) => e.type === EventType.TOOL_CALL_RESULT,
|
|
480
|
+
);
|
|
481
|
+
expect(toolResults).toHaveLength(1);
|
|
482
|
+
expect(eventField<string>(toolResults[0], "toolCallId")).toBe(
|
|
483
|
+
"tc-state2",
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("does not emit STATE_SNAPSHOT when snapshot field is undefined", async () => {
|
|
488
|
+
const agent = createAgent("aisdk", [
|
|
489
|
+
toolCallStreamingStart("tc-no-snap", "AGUISendStateSnapshot"),
|
|
490
|
+
toolCallDelta("tc-no-snap", "{}"),
|
|
491
|
+
toolCall("tc-no-snap", "AGUISendStateSnapshot"),
|
|
492
|
+
toolResult("tc-no-snap", "AGUISendStateSnapshot", {
|
|
493
|
+
success: true,
|
|
494
|
+
}),
|
|
495
|
+
finish(),
|
|
496
|
+
]);
|
|
497
|
+
const input = createDefaultInput();
|
|
498
|
+
const events = await collectEvents(agent.run(input));
|
|
499
|
+
|
|
500
|
+
const stateSnapshots = events.filter(
|
|
501
|
+
(e) => e.type === EventType.STATE_SNAPSHOT,
|
|
502
|
+
);
|
|
503
|
+
expect(stateSnapshots).toHaveLength(0);
|
|
504
|
+
|
|
505
|
+
// Should still emit the TOOL_CALL_RESULT
|
|
506
|
+
const toolResults = events.filter(
|
|
507
|
+
(e) => e.type === EventType.TOOL_CALL_RESULT,
|
|
508
|
+
);
|
|
509
|
+
expect(toolResults).toHaveLength(1);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("does not emit STATE_DELTA when delta field is undefined", async () => {
|
|
513
|
+
const agent = createAgent("aisdk", [
|
|
514
|
+
toolCallStreamingStart("tc-no-delta", "AGUISendStateDelta"),
|
|
515
|
+
toolCallDelta("tc-no-delta", "{}"),
|
|
516
|
+
toolCall("tc-no-delta", "AGUISendStateDelta"),
|
|
517
|
+
toolResult("tc-no-delta", "AGUISendStateDelta", {
|
|
518
|
+
success: true,
|
|
519
|
+
}),
|
|
520
|
+
finish(),
|
|
521
|
+
]);
|
|
522
|
+
const input = createDefaultInput();
|
|
523
|
+
const events = await collectEvents(agent.run(input));
|
|
524
|
+
|
|
525
|
+
const stateDeltas = events.filter(
|
|
526
|
+
(e) => e.type === EventType.STATE_DELTA,
|
|
527
|
+
);
|
|
528
|
+
expect(stateDeltas).toHaveLength(0);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
// Tool Result Property Compatibility
|
|
534
|
+
// ---------------------------------------------------------------------------
|
|
535
|
+
|
|
536
|
+
describe("Tool Result Property Compatibility", () => {
|
|
537
|
+
it("reads tool result from 'result' property when 'output' is absent", async () => {
|
|
538
|
+
// Simulate older AI SDK that uses "result" instead of "output"
|
|
539
|
+
const agent = createAgent("aisdk", [
|
|
540
|
+
toolCall("tc-compat", "myTool"),
|
|
541
|
+
{
|
|
542
|
+
type: "tool-result",
|
|
543
|
+
toolCallId: "tc-compat",
|
|
544
|
+
toolName: "myTool",
|
|
545
|
+
result: { data: "from-result-prop" },
|
|
546
|
+
},
|
|
547
|
+
finish(),
|
|
548
|
+
]);
|
|
549
|
+
const input = createDefaultInput();
|
|
550
|
+
const events = await collectEvents(agent.run(input));
|
|
551
|
+
|
|
552
|
+
const resultEvents = events.filter(
|
|
553
|
+
(e) => e.type === EventType.TOOL_CALL_RESULT,
|
|
554
|
+
);
|
|
555
|
+
expect(resultEvents).toHaveLength(1);
|
|
556
|
+
expect(
|
|
557
|
+
JSON.parse(eventField<string>(resultEvents[0], "content")),
|
|
558
|
+
).toEqual({ data: "from-result-prop" });
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("prefers 'output' over 'result' when both are present", async () => {
|
|
562
|
+
const agent = createAgent("aisdk", [
|
|
563
|
+
toolCall("tc-both", "myTool"),
|
|
564
|
+
{
|
|
565
|
+
type: "tool-result",
|
|
566
|
+
toolCallId: "tc-both",
|
|
567
|
+
toolName: "myTool",
|
|
568
|
+
output: { source: "output" },
|
|
569
|
+
result: { source: "result" },
|
|
570
|
+
},
|
|
571
|
+
finish(),
|
|
572
|
+
]);
|
|
573
|
+
const input = createDefaultInput();
|
|
574
|
+
const events = await collectEvents(agent.run(input));
|
|
575
|
+
|
|
576
|
+
const resultEvents = events.filter(
|
|
577
|
+
(e) => e.type === EventType.TOOL_CALL_RESULT,
|
|
578
|
+
);
|
|
579
|
+
expect(
|
|
580
|
+
JSON.parse(eventField<string>(resultEvents[0], "content")),
|
|
581
|
+
).toEqual({ source: "output" });
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
// Error Event Handling
|
|
587
|
+
// ---------------------------------------------------------------------------
|
|
588
|
+
|
|
589
|
+
describe("Error Event Handling", () => {
|
|
590
|
+
it("wraps undefined error in a descriptive Error", async () => {
|
|
591
|
+
const agent = createAgent("aisdk", [
|
|
592
|
+
textDelta("before error"),
|
|
593
|
+
{ type: "error" }, // no .error property
|
|
594
|
+
]);
|
|
595
|
+
const input = createDefaultInput();
|
|
596
|
+
|
|
597
|
+
const { events, errored } = await collectEventsIncludingErrors(
|
|
598
|
+
agent.run(input),
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
expect(errored).toBe(true);
|
|
602
|
+
const errorEvents = events.filter((e) => e.type === EventType.RUN_ERROR);
|
|
603
|
+
expect(errorEvents).toHaveLength(1);
|
|
604
|
+
// Should contain a useful message, not just "undefined"
|
|
605
|
+
expect(eventField<string>(errorEvents[0], "message")).not.toBe(
|
|
606
|
+
"undefined",
|
|
607
|
+
);
|
|
608
|
+
expect(
|
|
609
|
+
eventField<string>(errorEvents[0], "message").length,
|
|
610
|
+
).toBeGreaterThan(5);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it("preserves Error instances from error event", async () => {
|
|
614
|
+
const agent = createAgent("aisdk", [
|
|
615
|
+
{ type: "error", error: new Error("rate limit exceeded") },
|
|
616
|
+
]);
|
|
617
|
+
const input = createDefaultInput();
|
|
618
|
+
|
|
619
|
+
const { events, errored } = await collectEventsIncludingErrors(
|
|
620
|
+
agent.run(input),
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
expect(errored).toBe(true);
|
|
624
|
+
const errorEvents = events.filter((e) => e.type === EventType.RUN_ERROR);
|
|
625
|
+
expect(eventField<string>(errorEvents[0], "message")).toBe(
|
|
626
|
+
"rate limit exceeded",
|
|
627
|
+
);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it("handles string error from error event", async () => {
|
|
631
|
+
const agent = createAgent("aisdk", [
|
|
632
|
+
{ type: "error", message: "auth failed" },
|
|
633
|
+
]);
|
|
634
|
+
const input = createDefaultInput();
|
|
635
|
+
|
|
636
|
+
const { events, errored } = await collectEventsIncludingErrors(
|
|
637
|
+
agent.run(input),
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
expect(errored).toBe(true);
|
|
641
|
+
const errorEvents = events.filter((e) => e.type === EventType.RUN_ERROR);
|
|
642
|
+
expect(eventField<string>(errorEvents[0], "message")).toBe("auth failed");
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// ---------------------------------------------------------------------------
|
|
647
|
+
// Edge Cases
|
|
648
|
+
// ---------------------------------------------------------------------------
|
|
649
|
+
|
|
650
|
+
describe("Edge Cases", () => {
|
|
651
|
+
it("unknown event types are silently ignored", async () => {
|
|
652
|
+
const agent = createAgent("aisdk", [
|
|
653
|
+
{ type: "some-unknown-event", data: "hello" },
|
|
654
|
+
textDelta("text after unknown"),
|
|
655
|
+
{ type: "another-mystery-event" },
|
|
656
|
+
finish(),
|
|
657
|
+
]);
|
|
658
|
+
const input = createDefaultInput();
|
|
659
|
+
const events = await collectEvents(agent.run(input));
|
|
660
|
+
|
|
661
|
+
expectLifecycleWrapped(events);
|
|
662
|
+
|
|
663
|
+
// Should still have the text chunk
|
|
664
|
+
const textChunks = events.filter(
|
|
665
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
666
|
+
);
|
|
667
|
+
expect(textChunks).toHaveLength(1);
|
|
668
|
+
|
|
669
|
+
// No events for unknown types — only RUN_STARTED, TEXT_MESSAGE_CHUNK, RUN_FINISHED
|
|
670
|
+
expectEventSequence(events, [
|
|
671
|
+
EventType.RUN_STARTED,
|
|
672
|
+
EventType.TEXT_MESSAGE_CHUNK,
|
|
673
|
+
EventType.RUN_FINISHED,
|
|
674
|
+
]);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it("large text deltas (100k chars) are passed through", async () => {
|
|
678
|
+
const largeText = "x".repeat(100_000);
|
|
679
|
+
const agent = createAgent("aisdk", [textDelta(largeText), finish()]);
|
|
680
|
+
const input = createDefaultInput();
|
|
681
|
+
const events = await collectEvents(agent.run(input));
|
|
682
|
+
|
|
683
|
+
expectLifecycleWrapped(events);
|
|
684
|
+
|
|
685
|
+
const chunk = events.find(
|
|
686
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
687
|
+
)!;
|
|
688
|
+
expect(eventField<string>(chunk, "delta")).toBe(largeText);
|
|
689
|
+
expect(eventField<string>(chunk, "delta").length).toBe(100_000);
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
});
|