@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,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
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseEvent,
|
|
3
|
+
EventType,
|
|
4
|
+
RunAgentInput,
|
|
5
|
+
Message,
|
|
6
|
+
TextMessageChunkEvent,
|
|
7
|
+
ToolCallArgsEvent,
|
|
8
|
+
ToolCallEndEvent,
|
|
9
|
+
ToolCallStartEvent,
|
|
10
|
+
ToolCallResultEvent,
|
|
11
|
+
} from "@ag-ui/client";
|
|
12
|
+
import { randomUUID } from "@copilotkit/shared";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A TanStack AI content part (text, image, audio, video, or document).
|
|
16
|
+
*/
|
|
17
|
+
export type TanStackContentPart =
|
|
18
|
+
| { type: "text"; content: string }
|
|
19
|
+
| {
|
|
20
|
+
type: "image" | "audio" | "video" | "document";
|
|
21
|
+
source:
|
|
22
|
+
| { type: "data"; value: string; mimeType: string }
|
|
23
|
+
| { type: "url"; value: string; mimeType?: string };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Message format expected by TanStack AI's `chat()`.
|
|
28
|
+
*/
|
|
29
|
+
export interface TanStackChatMessage {
|
|
30
|
+
role: "user" | "assistant" | "tool";
|
|
31
|
+
content: string | null | TanStackContentPart[];
|
|
32
|
+
name?: string;
|
|
33
|
+
toolCalls?: Array<{
|
|
34
|
+
id: string;
|
|
35
|
+
type: "function";
|
|
36
|
+
function: { name: string; arguments: string };
|
|
37
|
+
}>;
|
|
38
|
+
toolCallId?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Result of converting RunAgentInput to TanStack AI format.
|
|
43
|
+
*/
|
|
44
|
+
export interface TanStackInputResult {
|
|
45
|
+
/** Chat messages (only user/assistant/tool roles; all others excluded) */
|
|
46
|
+
messages: TanStackChatMessage[];
|
|
47
|
+
/** System prompts extracted from system/developer messages, context, and state */
|
|
48
|
+
systemPrompts: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Converts AG-UI user message content to TanStack AI format.
|
|
53
|
+
* Handles plain strings, multimodal parts (image/audio/video/document),
|
|
54
|
+
* and legacy BinaryInputContent for backward compatibility.
|
|
55
|
+
*/
|
|
56
|
+
function convertUserContent(
|
|
57
|
+
content: unknown,
|
|
58
|
+
): string | null | TanStackContentPart[] {
|
|
59
|
+
if (!content) return null;
|
|
60
|
+
if (typeof content === "string") return content;
|
|
61
|
+
if (!Array.isArray(content)) return null;
|
|
62
|
+
if (content.length === 0) return "";
|
|
63
|
+
|
|
64
|
+
const parts: TanStackContentPart[] = [];
|
|
65
|
+
|
|
66
|
+
for (const part of content) {
|
|
67
|
+
if (!part || typeof part !== "object" || !("type" in part)) continue;
|
|
68
|
+
|
|
69
|
+
switch ((part as { type: string }).type) {
|
|
70
|
+
case "text": {
|
|
71
|
+
const text = (part as { text?: string }).text;
|
|
72
|
+
if (text != null) parts.push({ type: "text", content: text });
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case "image":
|
|
77
|
+
case "audio":
|
|
78
|
+
case "video":
|
|
79
|
+
case "document": {
|
|
80
|
+
const source = (part as { source?: any }).source;
|
|
81
|
+
if (!source) break;
|
|
82
|
+
const partType = (part as { type: string }).type as
|
|
83
|
+
| "image"
|
|
84
|
+
| "audio"
|
|
85
|
+
| "video"
|
|
86
|
+
| "document";
|
|
87
|
+
if (source.type === "data") {
|
|
88
|
+
parts.push({
|
|
89
|
+
type: partType,
|
|
90
|
+
source: {
|
|
91
|
+
type: "data",
|
|
92
|
+
value: source.value,
|
|
93
|
+
mimeType: source.mimeType,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
} else if (source.type === "url") {
|
|
97
|
+
parts.push({
|
|
98
|
+
type: partType,
|
|
99
|
+
source: {
|
|
100
|
+
type: "url",
|
|
101
|
+
value: source.value,
|
|
102
|
+
...(source.mimeType ? { mimeType: source.mimeType } : {}),
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Legacy BinaryInputContent backward compatibility
|
|
110
|
+
case "binary": {
|
|
111
|
+
const legacy = part as {
|
|
112
|
+
mimeType?: string;
|
|
113
|
+
data?: string;
|
|
114
|
+
url?: string;
|
|
115
|
+
};
|
|
116
|
+
const mimeType = legacy.mimeType ?? "application/octet-stream";
|
|
117
|
+
const isImage = mimeType.startsWith("image/");
|
|
118
|
+
|
|
119
|
+
if (legacy.data) {
|
|
120
|
+
const partType = isImage ? "image" : "document";
|
|
121
|
+
parts.push({
|
|
122
|
+
type: partType,
|
|
123
|
+
source: { type: "data", value: legacy.data, mimeType },
|
|
124
|
+
});
|
|
125
|
+
} else if (legacy.url) {
|
|
126
|
+
const partType = isImage ? "image" : "document";
|
|
127
|
+
parts.push({
|
|
128
|
+
type: partType,
|
|
129
|
+
source: { type: "url", value: legacy.url, mimeType },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return parts.length > 0 ? parts : "";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Converts a RunAgentInput into the format expected by TanStack AI's `chat()`.
|
|
142
|
+
*
|
|
143
|
+
* - Keeps only user/assistant/tool messages (activity, reasoning, and other roles are also excluded)
|
|
144
|
+
* - Extracts system/developer messages into `systemPrompts`
|
|
145
|
+
* - Appends context entries and application state to `systemPrompts`
|
|
146
|
+
* - Preserves tool calls on assistant messages and toolCallId on tool messages
|
|
147
|
+
*/
|
|
148
|
+
export function convertInputToTanStackAI(
|
|
149
|
+
input: RunAgentInput,
|
|
150
|
+
): TanStackInputResult {
|
|
151
|
+
// Allowlist: only pass user/assistant/tool messages to TanStack.
|
|
152
|
+
// Other roles (system, developer, activity, reasoning) are either
|
|
153
|
+
// extracted into systemPrompts or not applicable.
|
|
154
|
+
const chatRoles = new Set(["user", "assistant", "tool"]);
|
|
155
|
+
const messages: TanStackChatMessage[] = input.messages
|
|
156
|
+
.filter((m: Message) => chatRoles.has(m.role))
|
|
157
|
+
.map((m: Message): TanStackChatMessage => {
|
|
158
|
+
const msg: TanStackChatMessage = {
|
|
159
|
+
role: m.role as "user" | "assistant" | "tool",
|
|
160
|
+
content:
|
|
161
|
+
m.role === "user"
|
|
162
|
+
? convertUserContent(m.content)
|
|
163
|
+
: typeof m.content === "string"
|
|
164
|
+
? m.content
|
|
165
|
+
: null,
|
|
166
|
+
};
|
|
167
|
+
if (m.role === "assistant" && "toolCalls" in m && m.toolCalls) {
|
|
168
|
+
msg.toolCalls = m.toolCalls.map((tc) => ({
|
|
169
|
+
id: tc.id,
|
|
170
|
+
type: "function" as const,
|
|
171
|
+
function: {
|
|
172
|
+
name: tc.function.name,
|
|
173
|
+
arguments: tc.function.arguments,
|
|
174
|
+
},
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
if (m.role === "tool" && "toolCallId" in m) {
|
|
178
|
+
msg.toolCallId = (m as Record<string, unknown>).toolCallId as string;
|
|
179
|
+
}
|
|
180
|
+
return msg;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const systemPrompts: string[] = [];
|
|
184
|
+
for (const m of input.messages) {
|
|
185
|
+
if ((m.role === "system" || m.role === "developer") && m.content) {
|
|
186
|
+
systemPrompts.push(
|
|
187
|
+
typeof m.content === "string" ? m.content : JSON.stringify(m.content),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (input.context?.length) {
|
|
193
|
+
for (const ctx of input.context) {
|
|
194
|
+
systemPrompts.push(`${ctx.description}:\n${ctx.value}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (
|
|
199
|
+
input.state !== undefined &&
|
|
200
|
+
input.state !== null &&
|
|
201
|
+
typeof input.state === "object" &&
|
|
202
|
+
Object.keys(input.state).length > 0
|
|
203
|
+
) {
|
|
204
|
+
systemPrompts.push(
|
|
205
|
+
`Application State:\n\`\`\`json\n${JSON.stringify(input.state, null, 2)}\n\`\`\``,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { messages, systemPrompts };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Converts a TanStack AI stream into AG-UI `BaseEvent` objects.
|
|
214
|
+
*
|
|
215
|
+
* This is a pure converter — it does NOT emit lifecycle events
|
|
216
|
+
* (RUN_STARTED / RUN_FINISHED / RUN_ERROR). The caller (Agent class)
|
|
217
|
+
* is responsible for those.
|
|
218
|
+
*/
|
|
219
|
+
export async function* convertTanStackStream(
|
|
220
|
+
stream: AsyncIterable<unknown>,
|
|
221
|
+
abortSignal: AbortSignal,
|
|
222
|
+
): AsyncGenerator<BaseEvent> {
|
|
223
|
+
const messageId = randomUUID();
|
|
224
|
+
|
|
225
|
+
for await (const chunk of stream) {
|
|
226
|
+
if (abortSignal.aborted) break;
|
|
227
|
+
|
|
228
|
+
const raw = chunk as Record<string, unknown>;
|
|
229
|
+
const type = raw.type as string;
|
|
230
|
+
|
|
231
|
+
if (type === "TEXT_MESSAGE_CONTENT" && raw.delta) {
|
|
232
|
+
const textEvent: TextMessageChunkEvent = {
|
|
233
|
+
type: EventType.TEXT_MESSAGE_CHUNK,
|
|
234
|
+
role: "assistant",
|
|
235
|
+
messageId,
|
|
236
|
+
delta: raw.delta as string,
|
|
237
|
+
};
|
|
238
|
+
yield textEvent;
|
|
239
|
+
} else if (type === "TOOL_CALL_START") {
|
|
240
|
+
const startEvent: ToolCallStartEvent = {
|
|
241
|
+
type: EventType.TOOL_CALL_START,
|
|
242
|
+
parentMessageId: messageId,
|
|
243
|
+
toolCallId: raw.toolCallId as string,
|
|
244
|
+
toolCallName: raw.toolCallName as string,
|
|
245
|
+
};
|
|
246
|
+
yield startEvent;
|
|
247
|
+
} else if (type === "TOOL_CALL_ARGS") {
|
|
248
|
+
const argsEvent: ToolCallArgsEvent = {
|
|
249
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
250
|
+
toolCallId: raw.toolCallId as string,
|
|
251
|
+
delta: raw.delta as string,
|
|
252
|
+
};
|
|
253
|
+
yield argsEvent;
|
|
254
|
+
} else if (type === "TOOL_CALL_END") {
|
|
255
|
+
const endEvent: ToolCallEndEvent = {
|
|
256
|
+
type: EventType.TOOL_CALL_END,
|
|
257
|
+
toolCallId: raw.toolCallId as string,
|
|
258
|
+
};
|
|
259
|
+
yield endEvent;
|
|
260
|
+
} else if (type === "TOOL_CALL_RESULT") {
|
|
261
|
+
let serializedContent: string;
|
|
262
|
+
if (typeof raw.content === "string") {
|
|
263
|
+
serializedContent = raw.content;
|
|
264
|
+
} else {
|
|
265
|
+
try {
|
|
266
|
+
serializedContent = JSON.stringify(raw.content ?? raw.result ?? null);
|
|
267
|
+
} catch {
|
|
268
|
+
serializedContent = "[Unserializable tool result]";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const resultEvent: ToolCallResultEvent = {
|
|
272
|
+
type: EventType.TOOL_CALL_RESULT,
|
|
273
|
+
role: "tool",
|
|
274
|
+
messageId: randomUUID(),
|
|
275
|
+
toolCallId: raw.toolCallId as string,
|
|
276
|
+
content: serializedContent,
|
|
277
|
+
};
|
|
278
|
+
yield resultEvent;
|
|
279
|
+
}
|
|
280
|
+
// Unhandled chunk types are silently ignored.
|
|
281
|
+
// Known gaps: STATE_SNAPSHOT, STATE_DELTA, and REASONING events are not
|
|
282
|
+
// converted from TanStack streams. Shared state and reasoning will not
|
|
283
|
+
// surface when using the TanStack backend. Use the AI SDK backend if these
|
|
284
|
+
// features are required.
|
|
285
|
+
}
|
|
286
|
+
}
|