@convex-dev/agent 0.6.2 → 0.6.4
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/dist/UIMessages.d.ts.map +1 -1
- package/dist/UIMessages.js.map +1 -1
- package/dist/client/definePlaygroundAPI.d.ts +69 -64
- package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
- package/dist/client/definePlaygroundAPI.js +8 -5
- package/dist/client/definePlaygroundAPI.js.map +1 -1
- package/dist/client/index.d.ts +12 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +11 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/saveInputMessages.d.ts.map +1 -1
- package/dist/client/saveInputMessages.js.map +1 -1
- package/dist/client/types.d.ts +3 -3
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/apiKeys.js +5 -5
- package/dist/component/apiKeys.js.map +1 -1
- package/dist/component/convex.config.d.ts +1 -1
- package/dist/component/files.d.ts.map +1 -1
- package/dist/component/files.js +13 -11
- package/dist/component/files.js.map +1 -1
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +37 -27
- package/dist/component/messages.js.map +1 -1
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +22 -17
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.js +7 -7
- package/dist/component/threads.js.map +1 -1
- package/dist/component/users.js +2 -2
- package/dist/component/users.js.map +1 -1
- package/dist/component/vector/index.d.ts.map +1 -1
- package/dist/component/vector/index.js +14 -8
- package/dist/component/vector/index.js.map +1 -1
- package/dist/deltas.d.ts +16 -27
- package/dist/deltas.d.ts.map +1 -1
- package/dist/deltas.js +269 -286
- package/dist/deltas.js.map +1 -1
- package/dist/mapping.d.ts +9 -3
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +16 -14
- package/dist/mapping.js.map +1 -1
- package/dist/react/useStreamingUIMessages.d.ts.map +1 -1
- package/dist/react/useStreamingUIMessages.js +42 -26
- package/dist/react/useStreamingUIMessages.js.map +1 -1
- package/dist/react/useUIMessages.d.ts +1 -0
- package/dist/react/useUIMessages.d.ts.map +1 -1
- package/dist/react/useUIMessages.js +7 -3
- package/dist/react/useUIMessages.js.map +1 -1
- package/package.json +7 -6
- package/src/UIMessages.ts +1 -2
- package/src/client/approval.test.ts +25 -6
- package/src/client/createTool.ts +1 -1
- package/src/client/definePlaygroundAPI.ts +33 -17
- package/src/client/index.test.ts +91 -0
- package/src/client/index.ts +25 -1
- package/src/client/saveInputMessages.ts +4 -1
- package/src/client/streaming.integration.test.ts +39 -117
- package/src/client/types.ts +6 -21
- package/src/component/apiKeys.ts +5 -5
- package/src/component/files.test.ts +1 -1
- package/src/component/files.ts +14 -12
- package/src/component/messages.ts +40 -28
- package/src/component/streams.ts +33 -17
- package/src/component/threads.ts +7 -7
- package/src/component/users.ts +2 -2
- package/src/component/vector/index.ts +14 -7
- package/src/deltas.test.ts +373 -392
- package/src/deltas.ts +339 -378
- package/src/mapping.test.ts +296 -18
- package/src/mapping.ts +17 -11
- package/src/react/useStreamingUIMessages.ts +62 -34
- package/src/react/useUIMessages.test.ts +80 -1
- package/src/react/useUIMessages.ts +11 -3
package/src/deltas.ts
CHANGED
|
@@ -3,13 +3,11 @@ import {
|
|
|
3
3
|
type DynamicToolUIPart,
|
|
4
4
|
type ProviderMetadata,
|
|
5
5
|
type ReasoningUIPart,
|
|
6
|
-
type TextStreamPart,
|
|
7
6
|
type TextUIPart,
|
|
8
|
-
type ToolSet,
|
|
9
7
|
type ToolUIPart,
|
|
10
8
|
type UIMessageChunk,
|
|
11
9
|
} from "ai";
|
|
12
|
-
import { assert
|
|
10
|
+
import { assert } from "convex-helpers";
|
|
13
11
|
import { type UIMessage } from "./UIMessages.js";
|
|
14
12
|
import { joinText, sorted } from "./shared.js";
|
|
15
13
|
import {
|
|
@@ -17,7 +15,6 @@ import {
|
|
|
17
15
|
type StreamDelta,
|
|
18
16
|
type StreamMessage,
|
|
19
17
|
} from "./validators.js";
|
|
20
|
-
import { getErrorMessage } from "@ai-sdk/provider-utils";
|
|
21
18
|
|
|
22
19
|
export function blankUIMessage<METADATA = unknown>(
|
|
23
20
|
streamMessage: StreamMessage & { metadata?: METADATA },
|
|
@@ -57,6 +54,9 @@ export async function updateFromUIMessageChunks(
|
|
|
57
54
|
uiMessage: UIMessage,
|
|
58
55
|
parts: UIMessageChunk[],
|
|
59
56
|
) {
|
|
57
|
+
if (parts.length === 0) {
|
|
58
|
+
return uiMessage;
|
|
59
|
+
}
|
|
60
60
|
const partsStream = new ReadableStream<UIMessageChunk>({
|
|
61
61
|
start(controller) {
|
|
62
62
|
for (const part of parts) {
|
|
@@ -72,11 +72,7 @@ export async function updateFromUIMessageChunks(
|
|
|
72
72
|
stream: partsStream,
|
|
73
73
|
onError: (e) => {
|
|
74
74
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
75
|
-
// Tool invocation errors can be safely ignored when streaming continuation
|
|
76
|
-
// after tool approval - the stored messages have the complete tool context
|
|
77
75
|
if (errorMessage.toLowerCase().includes("no tool invocation found")) {
|
|
78
|
-
// Silently suppress - this is expected after tool approval when the
|
|
79
|
-
// continuation stream has tool-result without the original tool-call
|
|
80
76
|
suppressError = true;
|
|
81
77
|
return;
|
|
82
78
|
}
|
|
@@ -95,8 +91,6 @@ export async function updateFromUIMessageChunks(
|
|
|
95
91
|
message = messagePart;
|
|
96
92
|
}
|
|
97
93
|
} catch (e) {
|
|
98
|
-
// If we've already handled this error in onError and marked it as suppressed,
|
|
99
|
-
// don't rethrow - the stored messages provide the fallback
|
|
100
94
|
if (!suppressError) {
|
|
101
95
|
throw e;
|
|
102
96
|
}
|
|
@@ -108,188 +102,89 @@ export async function updateFromUIMessageChunks(
|
|
|
108
102
|
return message;
|
|
109
103
|
}
|
|
110
104
|
|
|
111
|
-
|
|
112
|
-
threadId: string,
|
|
113
|
-
streamMessages: StreamMessage[],
|
|
114
|
-
allDeltas: StreamDelta[],
|
|
115
|
-
): Promise<UIMessage[]> {
|
|
116
|
-
const messages: UIMessage[] = [];
|
|
117
|
-
for (const streamMessage of streamMessages) {
|
|
118
|
-
if (streamMessage.format === "UIMessageChunk") {
|
|
119
|
-
const { parts } = getParts<UIMessageChunk>(
|
|
120
|
-
allDeltas.filter((d) => d.streamId === streamMessage.streamId),
|
|
121
|
-
0,
|
|
122
|
-
);
|
|
123
|
-
const uiMessage = await updateFromUIMessageChunks(
|
|
124
|
-
blankUIMessage(streamMessage, threadId),
|
|
125
|
-
parts,
|
|
126
|
-
);
|
|
127
|
-
messages.push(uiMessage);
|
|
128
|
-
} else {
|
|
129
|
-
const [uiMessages] = deriveUIMessagesFromTextStreamParts(
|
|
130
|
-
threadId,
|
|
131
|
-
[streamMessage],
|
|
132
|
-
[],
|
|
133
|
-
allDeltas,
|
|
134
|
-
);
|
|
135
|
-
messages.push(...uiMessages);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return sorted(messages);
|
|
139
|
-
}
|
|
105
|
+
type ToolPart = ToolUIPart | DynamicToolUIPart;
|
|
140
106
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
threadId: string,
|
|
147
|
-
streamMessages: StreamMessage[],
|
|
148
|
-
existingStreams: Array<{
|
|
149
|
-
streamId: string;
|
|
150
|
-
cursor: number;
|
|
151
|
-
message: UIMessage;
|
|
152
|
-
}>,
|
|
153
|
-
allDeltas: StreamDelta[],
|
|
154
|
-
): [
|
|
155
|
-
UIMessage[],
|
|
156
|
-
Array<{ streamId: string; cursor: number; message: UIMessage }>,
|
|
157
|
-
boolean,
|
|
158
|
-
] {
|
|
159
|
-
const newStreams: Array<{
|
|
160
|
-
streamId: string;
|
|
161
|
-
cursor: number;
|
|
162
|
-
message: UIMessage;
|
|
163
|
-
}> = [];
|
|
164
|
-
// Seed the existing chunks
|
|
165
|
-
let changed = false;
|
|
166
|
-
for (const streamMessage of streamMessages) {
|
|
167
|
-
const deltas = allDeltas.filter(
|
|
168
|
-
(d) => d.streamId === streamMessage.streamId,
|
|
169
|
-
);
|
|
170
|
-
const existing = existingStreams.find(
|
|
171
|
-
(s) => s.streamId === streamMessage.streamId,
|
|
172
|
-
);
|
|
173
|
-
const [newStream, messageChanged] = updateFromTextStreamParts(
|
|
174
|
-
threadId,
|
|
175
|
-
streamMessage,
|
|
176
|
-
existing,
|
|
177
|
-
deltas,
|
|
178
|
-
);
|
|
179
|
-
newStreams.push(newStream);
|
|
180
|
-
if (messageChanged) changed = true;
|
|
181
|
-
}
|
|
182
|
-
for (const { streamId } of existingStreams) {
|
|
183
|
-
if (!newStreams.find((s) => s.streamId === streamId)) {
|
|
184
|
-
// There's a stream that's no longer active.
|
|
185
|
-
changed = true;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
const messages = sorted(newStreams.map((s) => s.message));
|
|
189
|
-
return [messages, newStreams, changed];
|
|
107
|
+
function transitionToolPart<S extends ToolPart["state"]>(
|
|
108
|
+
part: ToolPart,
|
|
109
|
+
updates: { state: S } & Partial<Extract<ToolPart, { state: S }>>,
|
|
110
|
+
): void {
|
|
111
|
+
Object.assign(part, updates);
|
|
190
112
|
}
|
|
191
113
|
|
|
192
|
-
export
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (cursor >= delta.end) {
|
|
205
|
-
continue;
|
|
206
|
-
} else if (cursor < delta.start) {
|
|
207
|
-
console.warn(
|
|
208
|
-
`Got delta for stream ${delta.streamId} that has a gap ${cursor} -> ${delta.start}`,
|
|
209
|
-
);
|
|
210
|
-
break;
|
|
211
|
-
} else {
|
|
212
|
-
throw new Error(
|
|
213
|
-
`Got unexpected delta for stream ${delta.streamId}: delta: ${delta.start} -> ${delta.end} existing cursor: ${cursor}`,
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
parts.push(...delta.parts);
|
|
218
|
-
cursor = delta.end;
|
|
219
|
-
}
|
|
220
|
-
return { parts, cursor };
|
|
114
|
+
export type IncrementalStreamState = {
|
|
115
|
+
// chunk id -> index of the streaming text part in message.parts
|
|
116
|
+
activeText: Record<string, number>;
|
|
117
|
+
// chunk id -> index of the streaming reasoning part in message.parts
|
|
118
|
+
activeReasoning: Record<string, number>;
|
|
119
|
+
// toolCallId -> raw accumulated input JSON text (kept separate from the
|
|
120
|
+
// parsed `input` so partial JSON can be repair-parsed each batch)
|
|
121
|
+
toolInputText: Record<string, string>;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export function emptyIncrementalStreamState(): IncrementalStreamState {
|
|
125
|
+
return { activeText: {}, activeReasoning: {}, toolInputText: {} };
|
|
221
126
|
}
|
|
222
127
|
|
|
223
128
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
129
|
+
* Apply a batch of new UIMessageChunks to an existing UIMessage without
|
|
130
|
+
* replaying prior chunks. `prev` carries the ephemeral stream state that the
|
|
131
|
+
* UIMessage itself can't hold (which text/reasoning parts are still streaming,
|
|
132
|
+
* and the raw accumulated tool input text). Parts are append-only, so part
|
|
133
|
+
* indices stay stable across the structuredClone between batches. Behavior
|
|
134
|
+
* mirrors the AI SDK's processUIMessageStream.
|
|
226
135
|
*/
|
|
136
|
+
export function applyUIMessageChunksIncremental(
|
|
137
|
+
uiMessage: UIMessage,
|
|
138
|
+
newParts: UIMessageChunk[],
|
|
139
|
+
prev: IncrementalStreamState,
|
|
140
|
+
): { message: UIMessage; streamState: IncrementalStreamState } {
|
|
141
|
+
const message: UIMessage = structuredClone(uiMessage);
|
|
142
|
+
const activeText: Record<string, number> = { ...prev.activeText };
|
|
143
|
+
const activeReasoning: Record<string, number> = { ...prev.activeReasoning };
|
|
144
|
+
const toolInputText: Record<string, string> = { ...prev.toolInputText };
|
|
145
|
+
const touchedTools = new Set<string>();
|
|
227
146
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
)
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
streamId: streamMessage.streamId,
|
|
251
|
-
cursor,
|
|
252
|
-
message: existingMessage,
|
|
253
|
-
},
|
|
254
|
-
false,
|
|
255
|
-
];
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const message: UIMessage = structuredClone(existingMessage);
|
|
259
|
-
message.status = statusFromStreamStatus(streamMessage.status);
|
|
260
|
-
|
|
261
|
-
const textPartsById = new Map<string, TextUIPart>();
|
|
262
|
-
const toolPartsById = new Map<string, ToolUIPart | DynamicToolUIPart>(
|
|
263
|
-
message.parts
|
|
264
|
-
.filter(
|
|
265
|
-
(p): p is ToolUIPart | DynamicToolUIPart =>
|
|
266
|
-
p.type.startsWith("tool-") || p.type === "dynamic-tool",
|
|
267
|
-
)
|
|
268
|
-
.map((p) => [p.toolCallId, p]),
|
|
269
|
-
);
|
|
270
|
-
const reasoningPartsById = new Map<string, ReasoningUIPart>();
|
|
147
|
+
const toolIndexById = new Map<string, number>();
|
|
148
|
+
message.parts.forEach((p, i) => {
|
|
149
|
+
if (
|
|
150
|
+
"toolCallId" in p &&
|
|
151
|
+
(p.type.startsWith("tool-") || p.type === "dynamic-tool")
|
|
152
|
+
) {
|
|
153
|
+
toolIndexById.set((p as ToolPart).toolCallId, i);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
const toolPartAt = (toolCallId: string): ToolPart | undefined => {
|
|
157
|
+
const idx = toolIndexById.get(toolCallId);
|
|
158
|
+
return idx === undefined ? undefined : (message.parts[idx] as ToolPart);
|
|
159
|
+
};
|
|
160
|
+
const mergeMetadata = (metadata: unknown) => {
|
|
161
|
+
if (metadata == null) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
message.metadata = {
|
|
165
|
+
...(message.metadata as Record<string, unknown> | undefined),
|
|
166
|
+
...(metadata as Record<string, unknown>),
|
|
167
|
+
} as typeof message.metadata;
|
|
168
|
+
};
|
|
271
169
|
|
|
272
|
-
for (const part of
|
|
170
|
+
for (const part of newParts) {
|
|
273
171
|
switch (part.type) {
|
|
274
|
-
case "text-start":
|
|
172
|
+
case "text-start": {
|
|
173
|
+
const newPart: TextUIPart = {
|
|
174
|
+
type: "text",
|
|
175
|
+
text: "",
|
|
176
|
+
state: "streaming",
|
|
177
|
+
providerMetadata: part.providerMetadata,
|
|
178
|
+
};
|
|
179
|
+
message.parts.push(newPart);
|
|
180
|
+
activeText[part.id] = message.parts.length - 1;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
275
183
|
case "text-delta": {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
} else {
|
|
281
|
-
const newPart = {
|
|
282
|
-
type: "text",
|
|
283
|
-
text: "",
|
|
284
|
-
providerMetadata: part.providerMetadata,
|
|
285
|
-
} satisfies TextUIPart;
|
|
286
|
-
textPartsById.set(part.id, newPart);
|
|
287
|
-
message.parts.push(newPart);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
if (part.type === "text-delta") {
|
|
291
|
-
const textPart = textPartsById.get(part.id)!;
|
|
292
|
-
textPart.text += part.text;
|
|
184
|
+
const idx = activeText[part.id];
|
|
185
|
+
if (idx !== undefined) {
|
|
186
|
+
const textPart = message.parts[idx] as TextUIPart;
|
|
187
|
+
textPart.text += part.delta;
|
|
293
188
|
textPart.providerMetadata = mergeProviderMetadata(
|
|
294
189
|
textPart.providerMetadata,
|
|
295
190
|
part.providerMetadata,
|
|
@@ -297,139 +192,35 @@ export function updateFromTextStreamParts(
|
|
|
297
192
|
}
|
|
298
193
|
break;
|
|
299
194
|
}
|
|
300
|
-
case "
|
|
301
|
-
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
input: "",
|
|
309
|
-
} satisfies DynamicToolUIPart;
|
|
310
|
-
} else {
|
|
311
|
-
newPart = {
|
|
312
|
-
type: `tool-${part.toolName}`,
|
|
313
|
-
toolCallId: part.id,
|
|
314
|
-
state: "input-streaming",
|
|
315
|
-
input: "",
|
|
316
|
-
providerExecuted: part.providerExecuted,
|
|
317
|
-
} satisfies ToolUIPart;
|
|
318
|
-
}
|
|
319
|
-
toolPartsById.set(part.id, newPart);
|
|
320
|
-
message.parts.push(newPart);
|
|
321
|
-
break;
|
|
322
|
-
}
|
|
323
|
-
case "tool-input-delta":
|
|
324
|
-
{
|
|
325
|
-
const toUpdate = toolPartsById.get(part.id);
|
|
326
|
-
assert(
|
|
327
|
-
toUpdate,
|
|
328
|
-
`Expected to find tool call part ${part.id} to update`,
|
|
329
|
-
);
|
|
330
|
-
toUpdate.input = (toUpdate.input ?? "") + part.delta;
|
|
331
|
-
}
|
|
332
|
-
break;
|
|
333
|
-
case "tool-input-end":
|
|
334
|
-
{
|
|
335
|
-
const toUpdate = toolPartsById.get(part.id);
|
|
336
|
-
assert(
|
|
337
|
-
toUpdate,
|
|
338
|
-
`Expected to find tool call part ${part.id} to update`,
|
|
195
|
+
case "text-end": {
|
|
196
|
+
const idx = activeText[part.id];
|
|
197
|
+
if (idx !== undefined) {
|
|
198
|
+
const textPart = message.parts[idx] as TextUIPart;
|
|
199
|
+
textPart.state = "done";
|
|
200
|
+
textPart.providerMetadata = mergeProviderMetadata(
|
|
201
|
+
textPart.providerMetadata,
|
|
202
|
+
part.providerMetadata,
|
|
339
203
|
);
|
|
340
|
-
|
|
341
|
-
if (part.providerMetadata) {
|
|
342
|
-
const updatable = toUpdate as Extract<
|
|
343
|
-
ToolUIPart | DynamicToolUIPart,
|
|
344
|
-
{ state: "input-available" }
|
|
345
|
-
>;
|
|
346
|
-
updatable.callProviderMetadata = mergeProviderMetadata(
|
|
347
|
-
updatable.callProviderMetadata,
|
|
348
|
-
part.providerMetadata,
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
break;
|
|
353
|
-
case "tool-call": {
|
|
354
|
-
let newPart: ToolUIPart | DynamicToolUIPart;
|
|
355
|
-
if (part.dynamic) {
|
|
356
|
-
newPart = {
|
|
357
|
-
type: "dynamic-tool",
|
|
358
|
-
toolCallId: part.toolCallId,
|
|
359
|
-
toolName: part.toolName,
|
|
360
|
-
input: part.input,
|
|
361
|
-
state: "input-available",
|
|
362
|
-
};
|
|
363
|
-
} else {
|
|
364
|
-
newPart = {
|
|
365
|
-
type: `tool-${part.toolName}`,
|
|
366
|
-
toolCallId: part.toolCallId,
|
|
367
|
-
input: part.input,
|
|
368
|
-
state: "input-available",
|
|
369
|
-
};
|
|
370
|
-
if (part.providerExecuted) {
|
|
371
|
-
newPart.providerExecuted = part.providerExecuted;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
if (part.providerMetadata) {
|
|
375
|
-
newPart.callProviderMetadata = part.providerMetadata;
|
|
376
|
-
}
|
|
377
|
-
if (toolPartsById.has(part.toolCallId)) {
|
|
378
|
-
const toUpdate = toolPartsById.get(part.toolCallId)!;
|
|
379
|
-
Object.assign(toUpdate, newPart);
|
|
380
|
-
} else {
|
|
381
|
-
toolPartsById.set(part.toolCallId, newPart);
|
|
382
|
-
message.parts.push(newPart);
|
|
204
|
+
delete activeText[part.id];
|
|
383
205
|
}
|
|
384
206
|
break;
|
|
385
207
|
}
|
|
386
|
-
case "
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
...toolCall,
|
|
396
|
-
state: "output-available",
|
|
397
|
-
input: part.input ?? toolCall.input,
|
|
398
|
-
output: part.output ?? toolCall.output,
|
|
399
|
-
...pick(part, ["preliminary"]),
|
|
400
|
-
} as DynamicToolUIPart;
|
|
401
|
-
} else {
|
|
402
|
-
newPart = {
|
|
403
|
-
...toolCall,
|
|
404
|
-
state: "output-available",
|
|
405
|
-
input: part.input ?? toolCall.input,
|
|
406
|
-
output: part.output ?? toolCall.output,
|
|
407
|
-
preliminary: part.preliminary,
|
|
408
|
-
} as ToolUIPart;
|
|
409
|
-
}
|
|
410
|
-
Object.assign(toolCall, newPart);
|
|
208
|
+
case "reasoning-start": {
|
|
209
|
+
const newPart: ReasoningUIPart = {
|
|
210
|
+
type: "reasoning",
|
|
211
|
+
text: "",
|
|
212
|
+
state: "streaming",
|
|
213
|
+
providerMetadata: part.providerMetadata,
|
|
214
|
+
};
|
|
215
|
+
message.parts.push(newPart);
|
|
216
|
+
activeReasoning[part.id] = message.parts.length - 1;
|
|
411
217
|
break;
|
|
412
218
|
}
|
|
413
|
-
case "reasoning-start":
|
|
414
219
|
case "reasoning-delta": {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
} else {
|
|
420
|
-
const newPart = {
|
|
421
|
-
type: "reasoning",
|
|
422
|
-
state: "streaming",
|
|
423
|
-
text: "",
|
|
424
|
-
providerMetadata: part.providerMetadata,
|
|
425
|
-
} satisfies ReasoningUIPart;
|
|
426
|
-
reasoningPartsById.set(part.id, newPart);
|
|
427
|
-
message.parts.push(newPart);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
const reasoningPart = reasoningPartsById.get(part.id)!;
|
|
431
|
-
if (part.type === "reasoning-delta") {
|
|
432
|
-
reasoningPart.text += part.text;
|
|
220
|
+
const idx = activeReasoning[part.id];
|
|
221
|
+
if (idx !== undefined) {
|
|
222
|
+
const reasoningPart = message.parts[idx] as ReasoningUIPart;
|
|
223
|
+
reasoningPart.text += part.delta;
|
|
433
224
|
reasoningPart.providerMetadata = mergeProviderMetadata(
|
|
434
225
|
reasoningPart.providerMetadata,
|
|
435
226
|
part.providerMetadata,
|
|
@@ -438,111 +229,281 @@ export function updateFromTextStreamParts(
|
|
|
438
229
|
break;
|
|
439
230
|
}
|
|
440
231
|
case "reasoning-end": {
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
message.parts
|
|
444
|
-
(p): p is ReasoningUIPart =>
|
|
445
|
-
p.type === "reasoning" && p.state === "streaming",
|
|
446
|
-
)!;
|
|
447
|
-
if (reasoningPart) {
|
|
232
|
+
const idx = activeReasoning[part.id];
|
|
233
|
+
if (idx !== undefined) {
|
|
234
|
+
const reasoningPart = message.parts[idx] as ReasoningUIPart;
|
|
448
235
|
reasoningPart.state = "done";
|
|
236
|
+
reasoningPart.providerMetadata = mergeProviderMetadata(
|
|
237
|
+
reasoningPart.providerMetadata,
|
|
238
|
+
part.providerMetadata,
|
|
239
|
+
);
|
|
240
|
+
delete activeReasoning[part.id];
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
case "tool-input-start": {
|
|
245
|
+
const newToolPart: ToolUIPart | DynamicToolUIPart = part.dynamic
|
|
246
|
+
? ({
|
|
247
|
+
type: "dynamic-tool",
|
|
248
|
+
toolCallId: part.toolCallId,
|
|
249
|
+
toolName: part.toolName,
|
|
250
|
+
state: "input-streaming",
|
|
251
|
+
input: undefined,
|
|
252
|
+
} satisfies DynamicToolUIPart)
|
|
253
|
+
: ({
|
|
254
|
+
type: `tool-${part.toolName}`,
|
|
255
|
+
toolCallId: part.toolCallId,
|
|
256
|
+
state: "input-streaming",
|
|
257
|
+
input: undefined,
|
|
258
|
+
providerExecuted: part.providerExecuted,
|
|
259
|
+
} satisfies ToolUIPart);
|
|
260
|
+
message.parts.push(newToolPart);
|
|
261
|
+
toolIndexById.set(part.toolCallId, message.parts.length - 1);
|
|
262
|
+
toolInputText[part.toolCallId] = "";
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
case "tool-input-delta": {
|
|
266
|
+
if (toolIndexById.has(part.toolCallId)) {
|
|
267
|
+
toolInputText[part.toolCallId] =
|
|
268
|
+
(toolInputText[part.toolCallId] ?? "") + part.inputTextDelta;
|
|
269
|
+
touchedTools.add(part.toolCallId);
|
|
449
270
|
} else {
|
|
450
271
|
console.warn(
|
|
451
|
-
`
|
|
272
|
+
`tool-input-delta for unknown toolCallId ${part.toolCallId}`,
|
|
452
273
|
);
|
|
453
274
|
}
|
|
454
275
|
break;
|
|
455
276
|
}
|
|
456
|
-
case "
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
277
|
+
case "tool-input-available": {
|
|
278
|
+
const toolPart = toolPartAt(part.toolCallId);
|
|
279
|
+
if (toolPart) {
|
|
280
|
+
transitionToolPart(toolPart, {
|
|
281
|
+
state: "input-available",
|
|
282
|
+
input: part.input,
|
|
283
|
+
callProviderMetadata: mergeProviderMetadata(
|
|
284
|
+
(toolPart as { callProviderMetadata?: ProviderMetadata })
|
|
285
|
+
.callProviderMetadata,
|
|
286
|
+
part.providerMetadata,
|
|
287
|
+
),
|
|
464
288
|
});
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
289
|
+
}
|
|
290
|
+
touchedTools.delete(part.toolCallId);
|
|
291
|
+
// The raw JSON buffer is no longer needed; drop it so it doesn't get
|
|
292
|
+
// carried through every later batch on the hot path.
|
|
293
|
+
delete toolInputText[part.toolCallId];
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
case "tool-input-error": {
|
|
297
|
+
const toolPart = toolPartAt(part.toolCallId);
|
|
298
|
+
if (toolPart) {
|
|
299
|
+
transitionToolPart(toolPart, {
|
|
300
|
+
state: "output-error",
|
|
301
|
+
errorText: part.errorText,
|
|
302
|
+
providerExecuted: part.providerExecuted,
|
|
303
|
+
...(toolPart.type === "dynamic-tool"
|
|
304
|
+
? { input: part.input }
|
|
305
|
+
: { input: undefined, rawInput: part.input }),
|
|
306
|
+
callProviderMetadata: mergeProviderMetadata(
|
|
307
|
+
(toolPart as { callProviderMetadata?: ProviderMetadata })
|
|
308
|
+
.callProviderMetadata,
|
|
309
|
+
part.providerMetadata,
|
|
310
|
+
),
|
|
473
311
|
});
|
|
474
|
-
} else {
|
|
475
|
-
console.warn("Got source part with unknown source type", part);
|
|
476
312
|
}
|
|
313
|
+
touchedTools.delete(part.toolCallId);
|
|
314
|
+
delete toolInputText[part.toolCallId];
|
|
477
315
|
break;
|
|
478
|
-
|
|
479
|
-
|
|
316
|
+
}
|
|
317
|
+
case "tool-output-available": {
|
|
318
|
+
const toolPart = toolPartAt(part.toolCallId);
|
|
319
|
+
if (toolPart) {
|
|
320
|
+
transitionToolPart(toolPart, {
|
|
321
|
+
state: "output-available",
|
|
322
|
+
output: part.output,
|
|
323
|
+
preliminary: part.preliminary,
|
|
324
|
+
providerExecuted: part.providerExecuted,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
480
327
|
break;
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
328
|
+
}
|
|
329
|
+
case "tool-output-error": {
|
|
330
|
+
const toolPart = toolPartAt(part.toolCallId);
|
|
331
|
+
if (toolPart) {
|
|
332
|
+
transitionToolPart(toolPart, {
|
|
333
|
+
state: "output-error",
|
|
334
|
+
errorText: part.errorText,
|
|
335
|
+
providerExecuted: part.providerExecuted,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
484
338
|
break;
|
|
485
|
-
|
|
486
|
-
|
|
339
|
+
}
|
|
340
|
+
case "tool-output-denied": {
|
|
341
|
+
const toolPart = toolPartAt(part.toolCallId);
|
|
487
342
|
if (toolPart) {
|
|
488
|
-
toolPart
|
|
343
|
+
transitionToolPart(toolPart, { state: "output-denied" });
|
|
489
344
|
}
|
|
490
345
|
break;
|
|
491
346
|
}
|
|
492
347
|
case "tool-approval-request": {
|
|
493
|
-
const
|
|
494
|
-
type: "tool-approval-request";
|
|
495
|
-
toolCallId: string;
|
|
496
|
-
approvalId: string;
|
|
497
|
-
};
|
|
498
|
-
const toolPart = toolPartsById.get(typedPart.toolCallId);
|
|
348
|
+
const toolPart = toolPartAt(part.toolCallId);
|
|
499
349
|
if (toolPart) {
|
|
500
|
-
toolPart
|
|
501
|
-
|
|
502
|
-
id:
|
|
503
|
-
};
|
|
504
|
-
} else {
|
|
505
|
-
console.warn(
|
|
506
|
-
`Expected tool call part ${typedPart.toolCallId} for approval request`,
|
|
507
|
-
);
|
|
350
|
+
transitionToolPart(toolPart, {
|
|
351
|
+
state: "approval-requested",
|
|
352
|
+
approval: { id: part.approvalId },
|
|
353
|
+
});
|
|
508
354
|
}
|
|
509
355
|
break;
|
|
510
356
|
}
|
|
357
|
+
case "source-url":
|
|
358
|
+
message.parts.push({
|
|
359
|
+
type: "source-url",
|
|
360
|
+
url: part.url,
|
|
361
|
+
sourceId: part.sourceId,
|
|
362
|
+
title: part.title,
|
|
363
|
+
providerMetadata: part.providerMetadata,
|
|
364
|
+
});
|
|
365
|
+
break;
|
|
366
|
+
case "source-document":
|
|
367
|
+
message.parts.push({
|
|
368
|
+
type: "source-document",
|
|
369
|
+
mediaType: part.mediaType,
|
|
370
|
+
sourceId: part.sourceId,
|
|
371
|
+
title: part.title,
|
|
372
|
+
filename: part.filename,
|
|
373
|
+
providerMetadata: part.providerMetadata,
|
|
374
|
+
});
|
|
375
|
+
break;
|
|
511
376
|
case "file":
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
377
|
+
message.parts.push({
|
|
378
|
+
type: "file",
|
|
379
|
+
mediaType: part.mediaType,
|
|
380
|
+
url: part.url,
|
|
381
|
+
});
|
|
382
|
+
break;
|
|
516
383
|
case "start-step":
|
|
384
|
+
message.parts.push({ type: "step-start" });
|
|
385
|
+
break;
|
|
386
|
+
case "finish-step":
|
|
387
|
+
// Match the SDK: a new step starts fresh streaming parts; the prior
|
|
388
|
+
// parts keep their state rather than being forced to "done".
|
|
389
|
+
for (const id of Object.keys(activeText)) delete activeText[id];
|
|
390
|
+
for (const id of Object.keys(activeReasoning))
|
|
391
|
+
delete activeReasoning[id];
|
|
392
|
+
break;
|
|
517
393
|
case "start":
|
|
518
|
-
|
|
394
|
+
case "finish":
|
|
395
|
+
case "message-metadata":
|
|
396
|
+
mergeMetadata(part.messageMetadata);
|
|
397
|
+
break;
|
|
398
|
+
case "abort":
|
|
399
|
+
case "error":
|
|
400
|
+
// The stream-level status (statusFromStreamStatus) is authoritative and
|
|
401
|
+
// is applied by the caller; nothing to mutate on the message here.
|
|
519
402
|
break;
|
|
520
403
|
default: {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
404
|
+
if (typeof part.type === "string" && part.type.startsWith("data-")) {
|
|
405
|
+
const dataPart = part as Extract<
|
|
406
|
+
UIMessageChunk,
|
|
407
|
+
{ type: `data-${string}` }
|
|
408
|
+
>;
|
|
409
|
+
const existingIdx =
|
|
410
|
+
dataPart.id != null
|
|
411
|
+
? message.parts.findIndex(
|
|
412
|
+
(p) =>
|
|
413
|
+
p.type === dataPart.type &&
|
|
414
|
+
(p as { id?: string }).id === dataPart.id,
|
|
415
|
+
)
|
|
416
|
+
: -1;
|
|
417
|
+
if (existingIdx >= 0) {
|
|
418
|
+
(message.parts[existingIdx] as { data?: unknown }).data =
|
|
419
|
+
dataPart.data;
|
|
420
|
+
} else {
|
|
421
|
+
message.parts.push(
|
|
422
|
+
dataPart as unknown as UIMessage["parts"][number],
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
console.warn(
|
|
427
|
+
`applyUIMessageChunksIncremental: unhandled chunk type ${String(part.type)}`,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
526
430
|
break;
|
|
527
431
|
}
|
|
528
432
|
}
|
|
529
433
|
}
|
|
530
|
-
|
|
531
|
-
for (
|
|
532
|
-
const
|
|
533
|
-
if (
|
|
534
|
-
|
|
434
|
+
|
|
435
|
+
for (const toolCallId of touchedTools) {
|
|
436
|
+
const toolPart = toolPartAt(toolCallId);
|
|
437
|
+
if (toolPart && toolPart.state === "input-streaming") {
|
|
438
|
+
try {
|
|
439
|
+
toolPart.input = JSON.parse(toolInputText[toolCallId] ?? "");
|
|
440
|
+
} catch {
|
|
441
|
+
// partial JSON — leave input unset until complete
|
|
442
|
+
}
|
|
535
443
|
}
|
|
536
444
|
}
|
|
445
|
+
|
|
537
446
|
message.text = joinText(message.parts);
|
|
538
|
-
return
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
447
|
+
return {
|
|
448
|
+
message,
|
|
449
|
+
streamState: { activeText, activeReasoning, toolInputText },
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export async function deriveUIMessagesFromDeltas(
|
|
454
|
+
threadId: string,
|
|
455
|
+
streamMessages: StreamMessage[],
|
|
456
|
+
allDeltas: StreamDelta[],
|
|
457
|
+
): Promise<UIMessage[]> {
|
|
458
|
+
const messages: UIMessage[] = [];
|
|
459
|
+
for (const streamMessage of streamMessages) {
|
|
460
|
+
if (streamMessage.format !== "UIMessageChunk") {
|
|
461
|
+
throw new Error(
|
|
462
|
+
`deriveUIMessagesFromDeltas: unsupported stream format "${streamMessage.format ?? "text"}" for stream ${streamMessage.streamId}`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const { parts } = getParts<UIMessageChunk>(
|
|
466
|
+
allDeltas.filter((d) => d.streamId === streamMessage.streamId),
|
|
467
|
+
0,
|
|
468
|
+
);
|
|
469
|
+
const uiMessage = await updateFromUIMessageChunks(
|
|
470
|
+
blankUIMessage(streamMessage, threadId),
|
|
471
|
+
parts,
|
|
472
|
+
);
|
|
473
|
+
messages.push(uiMessage);
|
|
474
|
+
}
|
|
475
|
+
return sorted(messages);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function getParts<T extends StreamDelta["parts"][number]>(
|
|
479
|
+
deltas: StreamDelta[],
|
|
480
|
+
fromCursor?: number,
|
|
481
|
+
): { parts: T[]; cursor: number } {
|
|
482
|
+
const parts: T[] = [];
|
|
483
|
+
let cursor = fromCursor ?? 0;
|
|
484
|
+
for (const delta of deltas.sort((a, b) => a.start - b.start)) {
|
|
485
|
+
if (delta.parts.length === 0) {
|
|
486
|
+
console.debug(`Got delta with no parts: ${JSON.stringify(delta)}`);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (cursor !== delta.start) {
|
|
490
|
+
if (cursor >= delta.end) {
|
|
491
|
+
continue;
|
|
492
|
+
} else if (cursor < delta.start) {
|
|
493
|
+
console.warn(
|
|
494
|
+
`Got delta for stream ${delta.streamId} that has a gap ${cursor} -> ${delta.start}`,
|
|
495
|
+
);
|
|
496
|
+
break;
|
|
497
|
+
} else {
|
|
498
|
+
throw new Error(
|
|
499
|
+
`Got unexpected delta for stream ${delta.streamId}: delta: ${delta.start} -> ${delta.end} existing cursor: ${cursor}`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
parts.push(...delta.parts);
|
|
504
|
+
cursor = delta.end;
|
|
505
|
+
}
|
|
506
|
+
return { parts, cursor };
|
|
546
507
|
}
|
|
547
508
|
|
|
548
509
|
function mergeProviderMetadata(
|