@copilotkit/runtime 1.55.1 → 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 +13 -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 +3 -2
- package/dist/package.mjs +3 -2
- 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 +4 -3
- 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__/mcp-servers-integration.test.ts +373 -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
- package/src/v2/runtime/__tests__/mcp-apps-middleware-integration.test.ts +275 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { convertInputToTanStackAI } from "../converters/tanstack";
|
|
3
|
+
import { createDefaultInput } from "./agent-test-helpers";
|
|
4
|
+
import type { Message, InputContent } from "@ag-ui/client";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Helper: build a user message with the given content parts, run through
|
|
8
|
+
* convertInputToTanStackAI, and return the first converted message for assertion.
|
|
9
|
+
*/
|
|
10
|
+
function convertUserContent(content: string | InputContent[]) {
|
|
11
|
+
const input = createDefaultInput({
|
|
12
|
+
messages: [{ role: "user", content } as Message],
|
|
13
|
+
});
|
|
14
|
+
const { messages } = convertInputToTanStackAI(input);
|
|
15
|
+
return messages[0];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Shorthand factories for AG-UI input parts */
|
|
19
|
+
function dataSource(value: string, mimeType: string) {
|
|
20
|
+
return { type: "data" as const, value, mimeType };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function urlSource(value: string, mimeType?: string) {
|
|
24
|
+
return { type: "url" as const, value, ...(mimeType ? { mimeType } : {}) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("convertInputToTanStackAI — multimodal", () => {
|
|
28
|
+
it("passes through plain string user content", () => {
|
|
29
|
+
const result = convertUserContent("Hello");
|
|
30
|
+
expect(result.content).toBe("Hello");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("converts text-only InputContent[] to TanStack TextPart array", () => {
|
|
34
|
+
const result = convertUserContent([{ type: "text", text: "Hello world" }]);
|
|
35
|
+
expect(result.content).toEqual([{ type: "text", content: "Hello world" }]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("converts ImageInputPart with data source", () => {
|
|
39
|
+
const result = convertUserContent([
|
|
40
|
+
{ type: "text", text: "What is this?" },
|
|
41
|
+
{ type: "image", source: dataSource("iVBORw0KGgo=", "image/png") },
|
|
42
|
+
]);
|
|
43
|
+
expect(result.content).toEqual([
|
|
44
|
+
{ type: "text", content: "What is this?" },
|
|
45
|
+
{
|
|
46
|
+
type: "image",
|
|
47
|
+
source: { type: "data", value: "iVBORw0KGgo=", mimeType: "image/png" },
|
|
48
|
+
},
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("converts ImageInputPart with url source", () => {
|
|
53
|
+
const result = convertUserContent([
|
|
54
|
+
{
|
|
55
|
+
type: "image",
|
|
56
|
+
source: urlSource("https://example.com/photo.jpg", "image/jpeg"),
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
expect(result.content).toEqual([
|
|
60
|
+
{
|
|
61
|
+
type: "image",
|
|
62
|
+
source: {
|
|
63
|
+
type: "url",
|
|
64
|
+
value: "https://example.com/photo.jpg",
|
|
65
|
+
mimeType: "image/jpeg",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("converts AudioInputPart", () => {
|
|
72
|
+
const result = convertUserContent([
|
|
73
|
+
{ type: "audio", source: dataSource("base64audiodata", "audio/mp3") },
|
|
74
|
+
]);
|
|
75
|
+
expect(result.content).toEqual([
|
|
76
|
+
{
|
|
77
|
+
type: "audio",
|
|
78
|
+
source: {
|
|
79
|
+
type: "data",
|
|
80
|
+
value: "base64audiodata",
|
|
81
|
+
mimeType: "audio/mp3",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("converts VideoInputPart with url source", () => {
|
|
88
|
+
const result = convertUserContent([
|
|
89
|
+
{
|
|
90
|
+
type: "video",
|
|
91
|
+
source: urlSource("https://example.com/video.mp4", "video/mp4"),
|
|
92
|
+
},
|
|
93
|
+
]);
|
|
94
|
+
expect(result.content).toEqual([
|
|
95
|
+
{
|
|
96
|
+
type: "video",
|
|
97
|
+
source: {
|
|
98
|
+
type: "url",
|
|
99
|
+
value: "https://example.com/video.mp4",
|
|
100
|
+
mimeType: "video/mp4",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("converts DocumentInputPart", () => {
|
|
107
|
+
const result = convertUserContent([
|
|
108
|
+
{
|
|
109
|
+
type: "document",
|
|
110
|
+
source: dataSource("base64pdfdata", "application/pdf"),
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
expect(result.content).toEqual([
|
|
114
|
+
{
|
|
115
|
+
type: "document",
|
|
116
|
+
source: {
|
|
117
|
+
type: "data",
|
|
118
|
+
value: "base64pdfdata",
|
|
119
|
+
mimeType: "application/pdf",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("handles mixed text and multimodal parts", () => {
|
|
126
|
+
const result = convertUserContent([
|
|
127
|
+
{ type: "text", text: "Analyze these:" },
|
|
128
|
+
{ type: "image", source: dataSource("imgdata", "image/png") },
|
|
129
|
+
{
|
|
130
|
+
type: "document",
|
|
131
|
+
source: dataSource("docdata", "application/pdf"),
|
|
132
|
+
},
|
|
133
|
+
]);
|
|
134
|
+
expect(result.content).toEqual([
|
|
135
|
+
{ type: "text", content: "Analyze these:" },
|
|
136
|
+
{
|
|
137
|
+
type: "image",
|
|
138
|
+
source: { type: "data", value: "imgdata", mimeType: "image/png" },
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
type: "document",
|
|
142
|
+
source: {
|
|
143
|
+
type: "data",
|
|
144
|
+
value: "docdata",
|
|
145
|
+
mimeType: "application/pdf",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("returns empty string for empty content array", () => {
|
|
152
|
+
const result = convertUserContent([]);
|
|
153
|
+
expect(result.content).toBe("");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("preserves empty string text parts", () => {
|
|
157
|
+
const result = convertUserContent([{ type: "text", text: "" }]);
|
|
158
|
+
expect(result.content).toEqual([{ type: "text", content: "" }]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("skips parts with missing source without crashing", () => {
|
|
162
|
+
const result = convertUserContent([
|
|
163
|
+
{ type: "text", text: "check this" },
|
|
164
|
+
{ type: "image" } as any,
|
|
165
|
+
]);
|
|
166
|
+
expect(result.content).toEqual([{ type: "text", content: "check this" }]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("silently skips unknown part types", () => {
|
|
170
|
+
const result = convertUserContent([
|
|
171
|
+
{ type: "text", text: "hello" },
|
|
172
|
+
{ type: "spreadsheet", source: dataSource("data", "text/csv") } as any,
|
|
173
|
+
]);
|
|
174
|
+
expect(result.content).toEqual([{ type: "text", content: "hello" }]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("returns null for null or undefined content", () => {
|
|
178
|
+
const nullInput = createDefaultInput({
|
|
179
|
+
messages: [{ role: "user", content: null } as unknown as Message],
|
|
180
|
+
});
|
|
181
|
+
const { messages: nullMessages } = convertInputToTanStackAI(nullInput);
|
|
182
|
+
expect(nullMessages[0].content).toBeNull();
|
|
183
|
+
|
|
184
|
+
const undefinedInput = createDefaultInput({
|
|
185
|
+
messages: [{ role: "user", content: undefined } as unknown as Message],
|
|
186
|
+
});
|
|
187
|
+
const { messages: undefinedMessages } =
|
|
188
|
+
convertInputToTanStackAI(undefinedInput);
|
|
189
|
+
expect(undefinedMessages[0].content).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("legacy BinaryInputContent backward compat", () => {
|
|
193
|
+
it("converts binary with image mimeType and data", () => {
|
|
194
|
+
const legacyPart = {
|
|
195
|
+
type: "binary",
|
|
196
|
+
mimeType: "image/jpeg",
|
|
197
|
+
data: "legacybase64",
|
|
198
|
+
};
|
|
199
|
+
const input = createDefaultInput({
|
|
200
|
+
messages: [
|
|
201
|
+
{
|
|
202
|
+
role: "user",
|
|
203
|
+
content: [legacyPart] as unknown as InputContent[],
|
|
204
|
+
} as Message,
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
const { messages } = convertInputToTanStackAI(input);
|
|
208
|
+
expect(messages[0].content).toEqual([
|
|
209
|
+
{
|
|
210
|
+
type: "image",
|
|
211
|
+
source: {
|
|
212
|
+
type: "data",
|
|
213
|
+
value: "legacybase64",
|
|
214
|
+
mimeType: "image/jpeg",
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("skips binary with neither data nor url", () => {
|
|
221
|
+
const legacyPart = {
|
|
222
|
+
type: "binary",
|
|
223
|
+
mimeType: "image/png",
|
|
224
|
+
};
|
|
225
|
+
const input = createDefaultInput({
|
|
226
|
+
messages: [
|
|
227
|
+
{
|
|
228
|
+
role: "user",
|
|
229
|
+
content: [legacyPart] as unknown as InputContent[],
|
|
230
|
+
} as Message,
|
|
231
|
+
],
|
|
232
|
+
});
|
|
233
|
+
const { messages } = convertInputToTanStackAI(input);
|
|
234
|
+
expect(messages[0].content).toBe("");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("converts binary with non-image mimeType and url", () => {
|
|
238
|
+
const legacyPart = {
|
|
239
|
+
type: "binary",
|
|
240
|
+
mimeType: "application/pdf",
|
|
241
|
+
url: "https://example.com/doc.pdf",
|
|
242
|
+
};
|
|
243
|
+
const input = createDefaultInput({
|
|
244
|
+
messages: [
|
|
245
|
+
{
|
|
246
|
+
role: "user",
|
|
247
|
+
content: [legacyPart] as unknown as InputContent[],
|
|
248
|
+
} as Message,
|
|
249
|
+
],
|
|
250
|
+
});
|
|
251
|
+
const { messages } = convertInputToTanStackAI(input);
|
|
252
|
+
expect(messages[0].content).toEqual([
|
|
253
|
+
{
|
|
254
|
+
type: "document",
|
|
255
|
+
source: {
|
|
256
|
+
type: "url",
|
|
257
|
+
value: "https://example.com/doc.pdf",
|
|
258
|
+
mimeType: "application/pdf",
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
]);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("only converts user message content, not assistant messages", () => {
|
|
266
|
+
const input = createDefaultInput({
|
|
267
|
+
messages: [
|
|
268
|
+
{
|
|
269
|
+
role: "user",
|
|
270
|
+
content: [
|
|
271
|
+
{ type: "text", text: "Look at this" },
|
|
272
|
+
{ type: "image", source: dataSource("imgdata", "image/png") },
|
|
273
|
+
],
|
|
274
|
+
} as Message,
|
|
275
|
+
{ role: "assistant", content: "I see an image" } as Message,
|
|
276
|
+
],
|
|
277
|
+
});
|
|
278
|
+
const { messages } = convertInputToTanStackAI(input);
|
|
279
|
+
// User message should have array content
|
|
280
|
+
expect(Array.isArray(messages[0].content)).toBe(true);
|
|
281
|
+
// Assistant message should keep string content
|
|
282
|
+
expect(messages[1].content).toBe("I see an image");
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -178,16 +178,20 @@ export async function collectEvents(
|
|
|
178
178
|
): Promise<BaseEvent[]> {
|
|
179
179
|
return new Promise((resolve, reject) => {
|
|
180
180
|
const events: BaseEvent[] = [];
|
|
181
|
-
const
|
|
182
|
-
next: (event) => events.push(event),
|
|
183
|
-
error: (err: unknown) => reject(err),
|
|
184
|
-
complete: () => resolve(events),
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// Set a timeout to prevent hanging tests
|
|
188
|
-
setTimeout(() => {
|
|
181
|
+
const timeoutId = setTimeout(() => {
|
|
189
182
|
subscription.unsubscribe();
|
|
190
183
|
reject(new Error("Observable did not complete within timeout"));
|
|
191
184
|
}, 5000);
|
|
185
|
+
const subscription = observable.subscribe({
|
|
186
|
+
next: (event) => events.push(event),
|
|
187
|
+
error: (err: unknown) => {
|
|
188
|
+
clearTimeout(timeoutId);
|
|
189
|
+
reject(err);
|
|
190
|
+
},
|
|
191
|
+
complete: () => {
|
|
192
|
+
clearTimeout(timeoutId);
|
|
193
|
+
resolve(events);
|
|
194
|
+
},
|
|
195
|
+
});
|
|
192
196
|
});
|
|
193
197
|
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseEvent,
|
|
3
|
+
EventType,
|
|
4
|
+
ReasoningEndEvent,
|
|
5
|
+
ReasoningMessageContentEvent,
|
|
6
|
+
ReasoningMessageEndEvent,
|
|
7
|
+
ReasoningMessageStartEvent,
|
|
8
|
+
ReasoningStartEvent,
|
|
9
|
+
TextMessageChunkEvent,
|
|
10
|
+
ToolCallArgsEvent,
|
|
11
|
+
ToolCallEndEvent,
|
|
12
|
+
ToolCallStartEvent,
|
|
13
|
+
ToolCallResultEvent,
|
|
14
|
+
StateSnapshotEvent,
|
|
15
|
+
StateDeltaEvent,
|
|
16
|
+
} from "@ag-ui/client";
|
|
17
|
+
import { randomUUID } from "@copilotkit/shared";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Converts an AI SDK `fullStream` into AG-UI `BaseEvent` objects.
|
|
21
|
+
*
|
|
22
|
+
* This is a pure converter — it does NOT emit lifecycle events
|
|
23
|
+
* (RUN_STARTED / RUN_FINISHED / RUN_ERROR). The caller (Agent class)
|
|
24
|
+
* is responsible for those.
|
|
25
|
+
*
|
|
26
|
+
* Terminal stream events (finish, error, abort) cause the generator to
|
|
27
|
+
* return so the caller can handle lifecycle appropriately.
|
|
28
|
+
*/
|
|
29
|
+
export async function* convertAISDKStream(
|
|
30
|
+
fullStream: AsyncIterable<unknown>,
|
|
31
|
+
abortSignal: AbortSignal,
|
|
32
|
+
): AsyncGenerator<BaseEvent> {
|
|
33
|
+
let messageId = randomUUID();
|
|
34
|
+
let reasoningMessageId = randomUUID();
|
|
35
|
+
let isInReasoning = false;
|
|
36
|
+
|
|
37
|
+
const toolCallStates = new Map<
|
|
38
|
+
string,
|
|
39
|
+
{
|
|
40
|
+
started: boolean;
|
|
41
|
+
hasArgsDelta: boolean;
|
|
42
|
+
ended: boolean;
|
|
43
|
+
toolName?: string;
|
|
44
|
+
}
|
|
45
|
+
>();
|
|
46
|
+
|
|
47
|
+
const ensureToolCallState = (toolCallId: string) => {
|
|
48
|
+
let state = toolCallStates.get(toolCallId);
|
|
49
|
+
if (!state) {
|
|
50
|
+
state = { started: false, hasArgsDelta: false, ended: false };
|
|
51
|
+
toolCallStates.set(toolCallId, state);
|
|
52
|
+
}
|
|
53
|
+
return state;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Auto-close an open reasoning lifecycle.
|
|
58
|
+
* Some AI SDK providers (notably @ai-sdk/anthropic) never emit "reasoning-end",
|
|
59
|
+
* which leaves downstream state machines stuck. This helper emits the
|
|
60
|
+
* missing REASONING_MESSAGE_END + REASONING_END events so the stream
|
|
61
|
+
* can transition to text, tool-call, or finish phases.
|
|
62
|
+
*/
|
|
63
|
+
function* closeReasoningIfOpen(): Generator<BaseEvent> {
|
|
64
|
+
if (!isInReasoning) return;
|
|
65
|
+
isInReasoning = false;
|
|
66
|
+
const reasoningMsgEnd: ReasoningMessageEndEvent = {
|
|
67
|
+
type: EventType.REASONING_MESSAGE_END,
|
|
68
|
+
messageId: reasoningMessageId,
|
|
69
|
+
};
|
|
70
|
+
yield reasoningMsgEnd;
|
|
71
|
+
const reasoningEnd: ReasoningEndEvent = {
|
|
72
|
+
type: EventType.REASONING_END,
|
|
73
|
+
messageId: reasoningMessageId,
|
|
74
|
+
};
|
|
75
|
+
yield reasoningEnd;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
for await (const part of fullStream) {
|
|
80
|
+
const p = part as Record<string, unknown>;
|
|
81
|
+
|
|
82
|
+
// Close any open reasoning lifecycle on every event except
|
|
83
|
+
// reasoning-delta, which arrives mid-block and must not interrupt it.
|
|
84
|
+
if (p.type !== "reasoning-delta") {
|
|
85
|
+
yield* closeReasoningIfOpen();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
switch (p.type) {
|
|
89
|
+
case "abort": {
|
|
90
|
+
// Terminal — let the caller handle lifecycle
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case "reasoning-start": {
|
|
95
|
+
// Use SDK-provided id, or generate a fresh UUID if id is falsy/"0"
|
|
96
|
+
// to prevent consecutive reasoning blocks from sharing a messageId
|
|
97
|
+
const providedId = "id" in p ? p.id : undefined;
|
|
98
|
+
reasoningMessageId =
|
|
99
|
+
providedId && providedId !== "0"
|
|
100
|
+
? (providedId as string)
|
|
101
|
+
: randomUUID();
|
|
102
|
+
const reasoningStartEvent: ReasoningStartEvent = {
|
|
103
|
+
type: EventType.REASONING_START,
|
|
104
|
+
messageId: reasoningMessageId,
|
|
105
|
+
};
|
|
106
|
+
yield reasoningStartEvent;
|
|
107
|
+
const reasoningMessageStart: ReasoningMessageStartEvent = {
|
|
108
|
+
type: EventType.REASONING_MESSAGE_START,
|
|
109
|
+
messageId: reasoningMessageId,
|
|
110
|
+
role: "reasoning",
|
|
111
|
+
};
|
|
112
|
+
yield reasoningMessageStart;
|
|
113
|
+
isInReasoning = true;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
case "reasoning-delta": {
|
|
118
|
+
const delta = (p.text as string) ?? "";
|
|
119
|
+
if (!delta) break; // skip — @ag-ui/core schema requires delta to be non-empty
|
|
120
|
+
const reasoningDeltaEvent: ReasoningMessageContentEvent = {
|
|
121
|
+
type: EventType.REASONING_MESSAGE_CONTENT,
|
|
122
|
+
messageId: reasoningMessageId,
|
|
123
|
+
delta,
|
|
124
|
+
};
|
|
125
|
+
yield reasoningDeltaEvent;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
case "reasoning-end": {
|
|
130
|
+
// closeReasoningIfOpen() already called before the switch — no-op here
|
|
131
|
+
// if the SDK never emits this event (e.g. @ai-sdk/anthropic).
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case "tool-input-start": {
|
|
136
|
+
const toolCallId = p.id as string;
|
|
137
|
+
const state = ensureToolCallState(toolCallId);
|
|
138
|
+
state.toolName = p.toolName as string;
|
|
139
|
+
if (!state.started) {
|
|
140
|
+
state.started = true;
|
|
141
|
+
const startEvent: ToolCallStartEvent = {
|
|
142
|
+
type: EventType.TOOL_CALL_START,
|
|
143
|
+
parentMessageId: messageId,
|
|
144
|
+
toolCallId,
|
|
145
|
+
toolCallName: p.toolName as string,
|
|
146
|
+
};
|
|
147
|
+
yield startEvent;
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case "tool-input-delta": {
|
|
153
|
+
const toolCallId = p.id as string;
|
|
154
|
+
const state = ensureToolCallState(toolCallId);
|
|
155
|
+
state.hasArgsDelta = true;
|
|
156
|
+
const argsEvent: ToolCallArgsEvent = {
|
|
157
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
158
|
+
toolCallId,
|
|
159
|
+
delta: p.delta as string,
|
|
160
|
+
};
|
|
161
|
+
yield argsEvent;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case "tool-input-end": {
|
|
166
|
+
// No direct event – the subsequent "tool-call" part marks completion.
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case "text-start": {
|
|
171
|
+
// New text message starting - use the SDK-provided id
|
|
172
|
+
// Use randomUUID() if part.id is falsy or "0" to prevent message merging issues
|
|
173
|
+
const providedId = "id" in p ? p.id : undefined;
|
|
174
|
+
messageId =
|
|
175
|
+
providedId && providedId !== "0"
|
|
176
|
+
? (providedId as string)
|
|
177
|
+
: randomUUID();
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
case "text-delta": {
|
|
182
|
+
// AI SDK text-delta events use 'text' (not 'delta')
|
|
183
|
+
const textDelta = "text" in p ? (p.text as string) : "";
|
|
184
|
+
const textEvent: TextMessageChunkEvent = {
|
|
185
|
+
type: EventType.TEXT_MESSAGE_CHUNK,
|
|
186
|
+
role: "assistant",
|
|
187
|
+
messageId,
|
|
188
|
+
delta: textDelta,
|
|
189
|
+
};
|
|
190
|
+
yield textEvent;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case "tool-call": {
|
|
195
|
+
const toolCallId = p.toolCallId as string;
|
|
196
|
+
const state = ensureToolCallState(toolCallId);
|
|
197
|
+
state.toolName = (p.toolName as string) ?? state.toolName;
|
|
198
|
+
|
|
199
|
+
if (!state.started) {
|
|
200
|
+
state.started = true;
|
|
201
|
+
const startEvent: ToolCallStartEvent = {
|
|
202
|
+
type: EventType.TOOL_CALL_START,
|
|
203
|
+
parentMessageId: messageId,
|
|
204
|
+
toolCallId,
|
|
205
|
+
toolCallName: p.toolName as string,
|
|
206
|
+
};
|
|
207
|
+
yield startEvent;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!state.hasArgsDelta && "input" in p && p.input !== undefined) {
|
|
211
|
+
let serializedInput = "";
|
|
212
|
+
if (typeof p.input === "string") {
|
|
213
|
+
serializedInput = p.input;
|
|
214
|
+
} else {
|
|
215
|
+
try {
|
|
216
|
+
serializedInput = JSON.stringify(p.input);
|
|
217
|
+
} catch {
|
|
218
|
+
serializedInput = String(p.input);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (serializedInput.length > 0) {
|
|
223
|
+
const argsEvent: ToolCallArgsEvent = {
|
|
224
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
225
|
+
toolCallId,
|
|
226
|
+
delta: serializedInput,
|
|
227
|
+
};
|
|
228
|
+
yield argsEvent;
|
|
229
|
+
state.hasArgsDelta = true;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!state.ended) {
|
|
234
|
+
state.ended = true;
|
|
235
|
+
const endEvent: ToolCallEndEvent = {
|
|
236
|
+
type: EventType.TOOL_CALL_END,
|
|
237
|
+
toolCallId,
|
|
238
|
+
};
|
|
239
|
+
yield endEvent;
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
case "tool-result": {
|
|
245
|
+
// AI SDK tool-result uses "output"; older versions used "result" — check both
|
|
246
|
+
const toolResult =
|
|
247
|
+
"output" in p ? p.output : "result" in p ? p.result : null;
|
|
248
|
+
const toolName = "toolName" in p ? (p.toolName as string) : "";
|
|
249
|
+
toolCallStates.delete(p.toolCallId as string);
|
|
250
|
+
|
|
251
|
+
// Check if this is a state update tool
|
|
252
|
+
if (
|
|
253
|
+
toolName === "AGUISendStateSnapshot" &&
|
|
254
|
+
toolResult &&
|
|
255
|
+
typeof toolResult === "object"
|
|
256
|
+
) {
|
|
257
|
+
const snapshot = (toolResult as Record<string, unknown>).snapshot;
|
|
258
|
+
if (snapshot !== undefined) {
|
|
259
|
+
const stateSnapshotEvent: StateSnapshotEvent = {
|
|
260
|
+
type: EventType.STATE_SNAPSHOT,
|
|
261
|
+
snapshot,
|
|
262
|
+
};
|
|
263
|
+
yield stateSnapshotEvent;
|
|
264
|
+
}
|
|
265
|
+
} else if (
|
|
266
|
+
toolName === "AGUISendStateDelta" &&
|
|
267
|
+
toolResult &&
|
|
268
|
+
typeof toolResult === "object"
|
|
269
|
+
) {
|
|
270
|
+
const delta = (toolResult as Record<string, unknown>).delta;
|
|
271
|
+
if (delta !== undefined) {
|
|
272
|
+
const stateDeltaEvent: StateDeltaEvent = {
|
|
273
|
+
type: EventType.STATE_DELTA,
|
|
274
|
+
delta,
|
|
275
|
+
};
|
|
276
|
+
yield stateDeltaEvent;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Always emit the tool result event for the LLM
|
|
281
|
+
let serializedResult: string;
|
|
282
|
+
try {
|
|
283
|
+
serializedResult = JSON.stringify(toolResult);
|
|
284
|
+
} catch {
|
|
285
|
+
serializedResult = `[Unserializable tool result from ${toolName || "unknown tool"}]`;
|
|
286
|
+
}
|
|
287
|
+
const resultEvent: ToolCallResultEvent = {
|
|
288
|
+
type: EventType.TOOL_CALL_RESULT,
|
|
289
|
+
role: "tool",
|
|
290
|
+
messageId: randomUUID(),
|
|
291
|
+
toolCallId: p.toolCallId as string,
|
|
292
|
+
content: serializedResult,
|
|
293
|
+
};
|
|
294
|
+
yield resultEvent;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
case "finish": {
|
|
299
|
+
// Terminal — let the caller handle lifecycle
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
case "error": {
|
|
304
|
+
if (abortSignal.aborted) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
// Re-throw so the caller can emit RUN_ERROR
|
|
308
|
+
const err = p.error ?? p.message ?? p.cause;
|
|
309
|
+
if (err instanceof Error) throw err;
|
|
310
|
+
throw new Error(
|
|
311
|
+
typeof err === "string"
|
|
312
|
+
? err
|
|
313
|
+
: `AI SDK stream error: ${JSON.stringify(p)}`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
default:
|
|
318
|
+
// Unknown event types are silently ignored
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} finally {
|
|
323
|
+
// Always close reasoning on exit (normal or exceptional)
|
|
324
|
+
yield* closeReasoningIfOpen();
|
|
325
|
+
}
|
|
326
|
+
}
|