@convex-dev/agent 0.6.3 → 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.
Files changed (71) hide show
  1. package/dist/UIMessages.d.ts.map +1 -1
  2. package/dist/UIMessages.js.map +1 -1
  3. package/dist/client/definePlaygroundAPI.d.ts +69 -64
  4. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  5. package/dist/client/definePlaygroundAPI.js +8 -5
  6. package/dist/client/definePlaygroundAPI.js.map +1 -1
  7. package/dist/client/index.d.ts +12 -1
  8. package/dist/client/index.d.ts.map +1 -1
  9. package/dist/client/index.js +11 -2
  10. package/dist/client/index.js.map +1 -1
  11. package/dist/client/saveInputMessages.d.ts.map +1 -1
  12. package/dist/client/saveInputMessages.js.map +1 -1
  13. package/dist/client/types.d.ts.map +1 -1
  14. package/dist/component/apiKeys.js +5 -5
  15. package/dist/component/apiKeys.js.map +1 -1
  16. package/dist/component/files.d.ts.map +1 -1
  17. package/dist/component/files.js +13 -11
  18. package/dist/component/files.js.map +1 -1
  19. package/dist/component/messages.d.ts.map +1 -1
  20. package/dist/component/messages.js +37 -27
  21. package/dist/component/messages.js.map +1 -1
  22. package/dist/component/streams.d.ts.map +1 -1
  23. package/dist/component/streams.js +22 -17
  24. package/dist/component/streams.js.map +1 -1
  25. package/dist/component/threads.js +7 -7
  26. package/dist/component/threads.js.map +1 -1
  27. package/dist/component/users.js +2 -2
  28. package/dist/component/users.js.map +1 -1
  29. package/dist/component/vector/index.d.ts.map +1 -1
  30. package/dist/component/vector/index.js +14 -8
  31. package/dist/component/vector/index.js.map +1 -1
  32. package/dist/deltas.d.ts +16 -27
  33. package/dist/deltas.d.ts.map +1 -1
  34. package/dist/deltas.js +269 -286
  35. package/dist/deltas.js.map +1 -1
  36. package/dist/mapping.d.ts +9 -3
  37. package/dist/mapping.d.ts.map +1 -1
  38. package/dist/mapping.js +16 -14
  39. package/dist/mapping.js.map +1 -1
  40. package/dist/react/useStreamingUIMessages.d.ts.map +1 -1
  41. package/dist/react/useStreamingUIMessages.js +42 -26
  42. package/dist/react/useStreamingUIMessages.js.map +1 -1
  43. package/dist/react/useUIMessages.d.ts +1 -0
  44. package/dist/react/useUIMessages.d.ts.map +1 -1
  45. package/dist/react/useUIMessages.js +7 -3
  46. package/dist/react/useUIMessages.js.map +1 -1
  47. package/package.json +2 -1
  48. package/src/UIMessages.ts +1 -2
  49. package/src/client/approval.test.ts +25 -6
  50. package/src/client/createTool.ts +1 -1
  51. package/src/client/definePlaygroundAPI.ts +33 -17
  52. package/src/client/index.test.ts +91 -0
  53. package/src/client/index.ts +25 -1
  54. package/src/client/saveInputMessages.ts +4 -1
  55. package/src/client/streaming.integration.test.ts +39 -117
  56. package/src/client/types.ts +4 -17
  57. package/src/component/apiKeys.ts +5 -5
  58. package/src/component/files.test.ts +1 -1
  59. package/src/component/files.ts +14 -12
  60. package/src/component/messages.ts +40 -28
  61. package/src/component/streams.ts +33 -17
  62. package/src/component/threads.ts +7 -7
  63. package/src/component/users.ts +2 -2
  64. package/src/component/vector/index.ts +14 -7
  65. package/src/deltas.test.ts +373 -392
  66. package/src/deltas.ts +339 -378
  67. package/src/mapping.test.ts +296 -18
  68. package/src/mapping.ts +17 -11
  69. package/src/react/useStreamingUIMessages.ts +62 -34
  70. package/src/react/useUIMessages.test.ts +80 -1
  71. package/src/react/useUIMessages.ts +11 -3
package/dist/deltas.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import { readUIMessageStream, } from "ai";
2
- import { assert, pick } from "convex-helpers";
2
+ import { assert } from "convex-helpers";
3
3
  import {} from "./UIMessages.js";
4
4
  import { joinText, sorted } from "./shared.js";
5
5
  import {} from "./validators.js";
6
- import { getErrorMessage } from "@ai-sdk/provider-utils";
7
6
  export function blankUIMessage(streamMessage, threadId) {
8
7
  return {
9
8
  id: `stream:${streamMessage.streamId}`,
@@ -32,6 +31,9 @@ export function statusFromStreamStatus(status) {
32
31
  }
33
32
  }
34
33
  export async function updateFromUIMessageChunks(uiMessage, parts) {
34
+ if (parts.length === 0) {
35
+ return uiMessage;
36
+ }
35
37
  const partsStream = new ReadableStream({
36
38
  start(controller) {
37
39
  for (const part of parts) {
@@ -47,11 +49,7 @@ export async function updateFromUIMessageChunks(uiMessage, parts) {
47
49
  stream: partsStream,
48
50
  onError: (e) => {
49
51
  const errorMessage = e instanceof Error ? e.message : String(e);
50
- // Tool invocation errors can be safely ignored when streaming continuation
51
- // after tool approval - the stored messages have the complete tool context
52
52
  if (errorMessage.toLowerCase().includes("no tool invocation found")) {
53
- // Silently suppress - this is expected after tool approval when the
54
- // continuation stream has tool-result without the original tool-call
55
53
  suppressError = true;
56
54
  return;
57
55
  }
@@ -68,8 +66,6 @@ export async function updateFromUIMessageChunks(uiMessage, parts) {
68
66
  }
69
67
  }
70
68
  catch (e) {
71
- // If we've already handled this error in onError and marked it as suppressed,
72
- // don't rethrow - the stored messages provide the fallback
73
69
  if (!suppressError) {
74
70
  throw e;
75
71
  }
@@ -80,349 +76,336 @@ export async function updateFromUIMessageChunks(uiMessage, parts) {
80
76
  message.text = joinText(message.parts);
81
77
  return message;
82
78
  }
83
- export async function deriveUIMessagesFromDeltas(threadId, streamMessages, allDeltas) {
84
- const messages = [];
85
- for (const streamMessage of streamMessages) {
86
- if (streamMessage.format === "UIMessageChunk") {
87
- const { parts } = getParts(allDeltas.filter((d) => d.streamId === streamMessage.streamId), 0);
88
- const uiMessage = await updateFromUIMessageChunks(blankUIMessage(streamMessage, threadId), parts);
89
- messages.push(uiMessage);
90
- }
91
- else {
92
- const [uiMessages] = deriveUIMessagesFromTextStreamParts(threadId, [streamMessage], [], allDeltas);
93
- messages.push(...uiMessages);
94
- }
95
- }
96
- return sorted(messages);
79
+ function transitionToolPart(part, updates) {
80
+ Object.assign(part, updates);
81
+ }
82
+ export function emptyIncrementalStreamState() {
83
+ return { activeText: {}, activeReasoning: {}, toolInputText: {} };
97
84
  }
98
85
  /**
99
- *
86
+ * Apply a batch of new UIMessageChunks to an existing UIMessage without
87
+ * replaying prior chunks. `prev` carries the ephemeral stream state that the
88
+ * UIMessage itself can't hold (which text/reasoning parts are still streaming,
89
+ * and the raw accumulated tool input text). Parts are append-only, so part
90
+ * indices stay stable across the structuredClone between batches. Behavior
91
+ * mirrors the AI SDK's processUIMessageStream.
100
92
  */
101
- export function deriveUIMessagesFromTextStreamParts(threadId, streamMessages, existingStreams, allDeltas) {
102
- const newStreams = [];
103
- // Seed the existing chunks
104
- let changed = false;
105
- for (const streamMessage of streamMessages) {
106
- const deltas = allDeltas.filter((d) => d.streamId === streamMessage.streamId);
107
- const existing = existingStreams.find((s) => s.streamId === streamMessage.streamId);
108
- const [newStream, messageChanged] = updateFromTextStreamParts(threadId, streamMessage, existing, deltas);
109
- newStreams.push(newStream);
110
- if (messageChanged)
111
- changed = true;
112
- }
113
- for (const { streamId } of existingStreams) {
114
- if (!newStreams.find((s) => s.streamId === streamId)) {
115
- // There's a stream that's no longer active.
116
- changed = true;
93
+ export function applyUIMessageChunksIncremental(uiMessage, newParts, prev) {
94
+ const message = structuredClone(uiMessage);
95
+ const activeText = { ...prev.activeText };
96
+ const activeReasoning = { ...prev.activeReasoning };
97
+ const toolInputText = { ...prev.toolInputText };
98
+ const touchedTools = new Set();
99
+ const toolIndexById = new Map();
100
+ message.parts.forEach((p, i) => {
101
+ if ("toolCallId" in p &&
102
+ (p.type.startsWith("tool-") || p.type === "dynamic-tool")) {
103
+ toolIndexById.set(p.toolCallId, i);
117
104
  }
118
- }
119
- const messages = sorted(newStreams.map((s) => s.message));
120
- return [messages, newStreams, changed];
121
- }
122
- export function getParts(deltas, fromCursor) {
123
- const parts = [];
124
- let cursor = fromCursor ?? 0;
125
- for (const delta of deltas.sort((a, b) => a.start - b.start)) {
126
- if (delta.parts.length === 0) {
127
- console.debug(`Got delta with no parts: ${JSON.stringify(delta)}`);
128
- continue;
105
+ });
106
+ const toolPartAt = (toolCallId) => {
107
+ const idx = toolIndexById.get(toolCallId);
108
+ return idx === undefined ? undefined : message.parts[idx];
109
+ };
110
+ const mergeMetadata = (metadata) => {
111
+ if (metadata == null) {
112
+ return;
129
113
  }
130
- if (cursor !== delta.start) {
131
- if (cursor >= delta.end) {
132
- continue;
133
- }
134
- else if (cursor < delta.start) {
135
- console.warn(`Got delta for stream ${delta.streamId} that has a gap ${cursor} -> ${delta.start}`);
114
+ message.metadata = {
115
+ ...message.metadata,
116
+ ...metadata,
117
+ };
118
+ };
119
+ for (const part of newParts) {
120
+ switch (part.type) {
121
+ case "text-start": {
122
+ const newPart = {
123
+ type: "text",
124
+ text: "",
125
+ state: "streaming",
126
+ providerMetadata: part.providerMetadata,
127
+ };
128
+ message.parts.push(newPart);
129
+ activeText[part.id] = message.parts.length - 1;
136
130
  break;
137
131
  }
138
- else {
139
- throw new Error(`Got unexpected delta for stream ${delta.streamId}: delta: ${delta.start} -> ${delta.end} existing cursor: ${cursor}`);
140
- }
141
- }
142
- parts.push(...delta.parts);
143
- cursor = delta.end;
144
- }
145
- return { parts, cursor };
146
- }
147
- /**
148
- * This is historically from when we would use the onChunk callback instead of
149
- * consuming the full UIMessageStream.
150
- */
151
- // exported for testing
152
- export function updateFromTextStreamParts(threadId, streamMessage, existing, deltas) {
153
- const { cursor, parts } = getParts(deltas, existing?.cursor);
154
- const changed = parts.length > 0 ||
155
- (existing &&
156
- statusFromStreamStatus(streamMessage.status) !== existing.message.status);
157
- const existingMessage = existing?.message ?? blankUIMessage(streamMessage, threadId);
158
- if (!changed) {
159
- return [
160
- existing ?? {
161
- streamId: streamMessage.streamId,
162
- cursor,
163
- message: existingMessage,
164
- },
165
- false,
166
- ];
167
- }
168
- const message = structuredClone(existingMessage);
169
- message.status = statusFromStreamStatus(streamMessage.status);
170
- const textPartsById = new Map();
171
- const toolPartsById = new Map(message.parts
172
- .filter((p) => p.type.startsWith("tool-") || p.type === "dynamic-tool")
173
- .map((p) => [p.toolCallId, p]));
174
- const reasoningPartsById = new Map();
175
- for (const part of parts) {
176
- switch (part.type) {
177
- case "text-start":
178
132
  case "text-delta": {
179
- if (!textPartsById.has(part.id)) {
180
- const lastPart = message.parts.at(-1);
181
- if (lastPart?.type === "text") {
182
- textPartsById.set(part.id, lastPart);
183
- }
184
- else {
185
- const newPart = {
186
- type: "text",
187
- text: "",
188
- providerMetadata: part.providerMetadata,
189
- };
190
- textPartsById.set(part.id, newPart);
191
- message.parts.push(newPart);
192
- }
193
- }
194
- if (part.type === "text-delta") {
195
- const textPart = textPartsById.get(part.id);
196
- textPart.text += part.text;
133
+ const idx = activeText[part.id];
134
+ if (idx !== undefined) {
135
+ const textPart = message.parts[idx];
136
+ textPart.text += part.delta;
197
137
  textPart.providerMetadata = mergeProviderMetadata(textPart.providerMetadata, part.providerMetadata);
198
138
  }
199
139
  break;
200
140
  }
201
- case "tool-input-start": {
202
- let newPart;
203
- if (part.dynamic) {
204
- newPart = {
205
- type: "dynamic-tool",
206
- toolCallId: part.id,
207
- toolName: part.toolName,
208
- state: "input-streaming",
209
- input: "",
210
- };
211
- }
212
- else {
213
- newPart = {
214
- type: `tool-${part.toolName}`,
215
- toolCallId: part.id,
216
- state: "input-streaming",
217
- input: "",
218
- providerExecuted: part.providerExecuted,
219
- };
141
+ case "text-end": {
142
+ const idx = activeText[part.id];
143
+ if (idx !== undefined) {
144
+ const textPart = message.parts[idx];
145
+ textPart.state = "done";
146
+ textPart.providerMetadata = mergeProviderMetadata(textPart.providerMetadata, part.providerMetadata);
147
+ delete activeText[part.id];
220
148
  }
221
- toolPartsById.set(part.id, newPart);
149
+ break;
150
+ }
151
+ case "reasoning-start": {
152
+ const newPart = {
153
+ type: "reasoning",
154
+ text: "",
155
+ state: "streaming",
156
+ providerMetadata: part.providerMetadata,
157
+ };
222
158
  message.parts.push(newPart);
159
+ activeReasoning[part.id] = message.parts.length - 1;
223
160
  break;
224
161
  }
225
- case "tool-input-delta":
226
- {
227
- const toUpdate = toolPartsById.get(part.id);
228
- assert(toUpdate, `Expected to find tool call part ${part.id} to update`);
229
- toUpdate.input = (toUpdate.input ?? "") + part.delta;
162
+ case "reasoning-delta": {
163
+ const idx = activeReasoning[part.id];
164
+ if (idx !== undefined) {
165
+ const reasoningPart = message.parts[idx];
166
+ reasoningPart.text += part.delta;
167
+ reasoningPart.providerMetadata = mergeProviderMetadata(reasoningPart.providerMetadata, part.providerMetadata);
230
168
  }
231
169
  break;
232
- case "tool-input-end":
233
- {
234
- const toUpdate = toolPartsById.get(part.id);
235
- assert(toUpdate, `Expected to find tool call part ${part.id} to update`);
236
- toUpdate.state = "input-available";
237
- if (part.providerMetadata) {
238
- const updatable = toUpdate;
239
- updatable.callProviderMetadata = mergeProviderMetadata(updatable.callProviderMetadata, part.providerMetadata);
240
- }
170
+ }
171
+ case "reasoning-end": {
172
+ const idx = activeReasoning[part.id];
173
+ if (idx !== undefined) {
174
+ const reasoningPart = message.parts[idx];
175
+ reasoningPart.state = "done";
176
+ reasoningPart.providerMetadata = mergeProviderMetadata(reasoningPart.providerMetadata, part.providerMetadata);
177
+ delete activeReasoning[part.id];
241
178
  }
242
179
  break;
243
- case "tool-call": {
244
- let newPart;
245
- if (part.dynamic) {
246
- newPart = {
180
+ }
181
+ case "tool-input-start": {
182
+ const newToolPart = part.dynamic
183
+ ? {
247
184
  type: "dynamic-tool",
248
185
  toolCallId: part.toolCallId,
249
186
  toolName: part.toolName,
250
- input: part.input,
251
- state: "input-available",
252
- };
253
- }
254
- else {
255
- newPart = {
187
+ state: "input-streaming",
188
+ input: undefined,
189
+ }
190
+ : {
256
191
  type: `tool-${part.toolName}`,
257
192
  toolCallId: part.toolCallId,
258
- input: part.input,
259
- state: "input-available",
193
+ state: "input-streaming",
194
+ input: undefined,
195
+ providerExecuted: part.providerExecuted,
260
196
  };
261
- if (part.providerExecuted) {
262
- newPart.providerExecuted = part.providerExecuted;
263
- }
264
- }
265
- if (part.providerMetadata) {
266
- newPart.callProviderMetadata = part.providerMetadata;
267
- }
268
- if (toolPartsById.has(part.toolCallId)) {
269
- const toUpdate = toolPartsById.get(part.toolCallId);
270
- Object.assign(toUpdate, newPart);
271
- }
272
- else {
273
- toolPartsById.set(part.toolCallId, newPart);
274
- message.parts.push(newPart);
275
- }
197
+ message.parts.push(newToolPart);
198
+ toolIndexById.set(part.toolCallId, message.parts.length - 1);
199
+ toolInputText[part.toolCallId] = "";
276
200
  break;
277
201
  }
278
- case "tool-result": {
279
- const toolCall = toolPartsById.get(part.toolCallId);
280
- assert(toolCall, `Expected to find tool call part ${part.toolCallId} to update with result`);
281
- let newPart;
282
- if (toolCall.type === "dynamic-tool") {
283
- newPart = {
284
- ...toolCall,
285
- state: "output-available",
286
- input: part.input ?? toolCall.input,
287
- output: part.output ?? toolCall.output,
288
- ...pick(part, ["preliminary"]),
289
- };
202
+ case "tool-input-delta": {
203
+ if (toolIndexById.has(part.toolCallId)) {
204
+ toolInputText[part.toolCallId] =
205
+ (toolInputText[part.toolCallId] ?? "") + part.inputTextDelta;
206
+ touchedTools.add(part.toolCallId);
290
207
  }
291
208
  else {
292
- newPart = {
293
- ...toolCall,
294
- state: "output-available",
295
- input: part.input ?? toolCall.input,
296
- output: part.output ?? toolCall.output,
297
- preliminary: part.preliminary,
298
- };
209
+ console.warn(`tool-input-delta for unknown toolCallId ${part.toolCallId}`);
299
210
  }
300
- Object.assign(toolCall, newPart);
301
211
  break;
302
212
  }
303
- case "reasoning-start":
304
- case "reasoning-delta": {
305
- if (!reasoningPartsById.has(part.id)) {
306
- const lastPart = message.parts.at(-1);
307
- if (lastPart?.type === "reasoning") {
308
- reasoningPartsById.set(part.id, lastPart);
309
- }
310
- else {
311
- const newPart = {
312
- type: "reasoning",
313
- state: "streaming",
314
- text: "",
315
- providerMetadata: part.providerMetadata,
316
- };
317
- reasoningPartsById.set(part.id, newPart);
318
- message.parts.push(newPart);
319
- }
320
- }
321
- const reasoningPart = reasoningPartsById.get(part.id);
322
- if (part.type === "reasoning-delta") {
323
- reasoningPart.text += part.text;
324
- reasoningPart.providerMetadata = mergeProviderMetadata(reasoningPart.providerMetadata, part.providerMetadata);
213
+ case "tool-input-available": {
214
+ const toolPart = toolPartAt(part.toolCallId);
215
+ if (toolPart) {
216
+ transitionToolPart(toolPart, {
217
+ state: "input-available",
218
+ input: part.input,
219
+ callProviderMetadata: mergeProviderMetadata(toolPart
220
+ .callProviderMetadata, part.providerMetadata),
221
+ });
325
222
  }
223
+ touchedTools.delete(part.toolCallId);
224
+ // The raw JSON buffer is no longer needed; drop it so it doesn't get
225
+ // carried through every later batch on the hot path.
226
+ delete toolInputText[part.toolCallId];
326
227
  break;
327
228
  }
328
- case "reasoning-end": {
329
- const reasoningPart = reasoningPartsById.get(part.id) ??
330
- message.parts.find((p) => p.type === "reasoning" && p.state === "streaming");
331
- if (reasoningPart) {
332
- reasoningPart.state = "done";
333
- }
334
- else {
335
- console.warn(`Expected to find reasoning part ${part.id} to finish, but found none`);
229
+ case "tool-input-error": {
230
+ const toolPart = toolPartAt(part.toolCallId);
231
+ if (toolPart) {
232
+ transitionToolPart(toolPart, {
233
+ state: "output-error",
234
+ errorText: part.errorText,
235
+ providerExecuted: part.providerExecuted,
236
+ ...(toolPart.type === "dynamic-tool"
237
+ ? { input: part.input }
238
+ : { input: undefined, rawInput: part.input }),
239
+ callProviderMetadata: mergeProviderMetadata(toolPart
240
+ .callProviderMetadata, part.providerMetadata),
241
+ });
336
242
  }
243
+ touchedTools.delete(part.toolCallId);
244
+ delete toolInputText[part.toolCallId];
337
245
  break;
338
246
  }
339
- case "source":
340
- if (part.sourceType === "url") {
341
- message.parts.push({
342
- type: "source-url",
343
- url: part.url,
344
- sourceId: part.id,
345
- providerMetadata: part.providerMetadata,
346
- title: part.title,
247
+ case "tool-output-available": {
248
+ const toolPart = toolPartAt(part.toolCallId);
249
+ if (toolPart) {
250
+ transitionToolPart(toolPart, {
251
+ state: "output-available",
252
+ output: part.output,
253
+ preliminary: part.preliminary,
254
+ providerExecuted: part.providerExecuted,
347
255
  });
348
256
  }
349
- else if (part.sourceType === "document") {
350
- message.parts.push({
351
- type: "source-document",
352
- mediaType: part.mediaType,
353
- sourceId: part.id,
354
- title: part.title,
355
- filename: part.filename,
356
- providerMetadata: part.providerMetadata,
257
+ break;
258
+ }
259
+ case "tool-output-error": {
260
+ const toolPart = toolPartAt(part.toolCallId);
261
+ if (toolPart) {
262
+ transitionToolPart(toolPart, {
263
+ state: "output-error",
264
+ errorText: part.errorText,
265
+ providerExecuted: part.providerExecuted,
357
266
  });
358
267
  }
359
- else {
360
- console.warn("Got source part with unknown source type", part);
361
- }
362
- break;
363
- case "abort":
364
- message.status = "failed";
365
- break;
366
- case "error":
367
- message.status = "failed";
368
- console.warn("Generation failed with error", part.error);
369
268
  break;
370
- case "tool-error": {
371
- const toolPart = toolPartsById.get(part.toolCallId);
269
+ }
270
+ case "tool-output-denied": {
271
+ const toolPart = toolPartAt(part.toolCallId);
372
272
  if (toolPart) {
373
- toolPart.errorText = getErrorMessage(part.error);
273
+ transitionToolPart(toolPart, { state: "output-denied" });
374
274
  }
375
275
  break;
376
276
  }
377
277
  case "tool-approval-request": {
378
- const typedPart = part;
379
- const toolPart = toolPartsById.get(typedPart.toolCallId);
278
+ const toolPart = toolPartAt(part.toolCallId);
380
279
  if (toolPart) {
381
- toolPart.state = "approval-requested";
382
- toolPart.approval = {
383
- id: typedPart.approvalId,
384
- };
385
- }
386
- else {
387
- console.warn(`Expected tool call part ${typedPart.toolCallId} for approval request`);
280
+ transitionToolPart(toolPart, {
281
+ state: "approval-requested",
282
+ approval: { id: part.approvalId },
283
+ });
388
284
  }
389
285
  break;
390
286
  }
287
+ case "source-url":
288
+ message.parts.push({
289
+ type: "source-url",
290
+ url: part.url,
291
+ sourceId: part.sourceId,
292
+ title: part.title,
293
+ providerMetadata: part.providerMetadata,
294
+ });
295
+ break;
296
+ case "source-document":
297
+ message.parts.push({
298
+ type: "source-document",
299
+ mediaType: part.mediaType,
300
+ sourceId: part.sourceId,
301
+ title: part.title,
302
+ filename: part.filename,
303
+ providerMetadata: part.providerMetadata,
304
+ });
305
+ break;
391
306
  case "file":
392
- case "text-end":
393
- case "finish-step":
394
- case "finish":
395
- case "raw":
307
+ message.parts.push({
308
+ type: "file",
309
+ mediaType: part.mediaType,
310
+ url: part.url,
311
+ });
312
+ break;
396
313
  case "start-step":
314
+ message.parts.push({ type: "step-start" });
315
+ break;
316
+ case "finish-step":
317
+ // Match the SDK: a new step starts fresh streaming parts; the prior
318
+ // parts keep their state rather than being forced to "done".
319
+ for (const id of Object.keys(activeText))
320
+ delete activeText[id];
321
+ for (const id of Object.keys(activeReasoning))
322
+ delete activeReasoning[id];
323
+ break;
397
324
  case "start":
398
- // ignore
325
+ case "finish":
326
+ case "message-metadata":
327
+ mergeMetadata(part.messageMetadata);
328
+ break;
329
+ case "abort":
330
+ case "error":
331
+ // The stream-level status (statusFromStreamStatus) is authoritative and
332
+ // is applied by the caller; nothing to mutate on the message here.
399
333
  break;
400
334
  default: {
401
- // Exhaustiveness check disabled intentionally for forwards compatibility.
402
- // New TextStreamPart types from future AI SDK versions will trigger a
403
- // runtime warning rather than a compile error, allowing graceful degradation.
404
- // const _: never = part;
405
- console.warn(`Received unexpected part: ${JSON.stringify(part)}`);
335
+ if (typeof part.type === "string" && part.type.startsWith("data-")) {
336
+ const dataPart = part;
337
+ const existingIdx = dataPart.id != null
338
+ ? message.parts.findIndex((p) => p.type === dataPart.type &&
339
+ p.id === dataPart.id)
340
+ : -1;
341
+ if (existingIdx >= 0) {
342
+ message.parts[existingIdx].data =
343
+ dataPart.data;
344
+ }
345
+ else {
346
+ message.parts.push(dataPart);
347
+ }
348
+ }
349
+ else {
350
+ console.warn(`applyUIMessageChunksIncremental: unhandled chunk type ${String(part.type)}`);
351
+ }
406
352
  break;
407
353
  }
408
354
  }
409
355
  }
410
- // Consider reasoning done once something else happens
411
- for (let i = 0; i < message.parts.length - 1; i++) {
412
- const part = message.parts[i];
413
- if (part.type === "reasoning") {
414
- part.state = "done";
356
+ for (const toolCallId of touchedTools) {
357
+ const toolPart = toolPartAt(toolCallId);
358
+ if (toolPart && toolPart.state === "input-streaming") {
359
+ try {
360
+ toolPart.input = JSON.parse(toolInputText[toolCallId] ?? "");
361
+ }
362
+ catch {
363
+ // partial JSON — leave input unset until complete
364
+ }
415
365
  }
416
366
  }
417
367
  message.text = joinText(message.parts);
418
- return [
419
- {
420
- streamId: streamMessage.streamId,
421
- cursor,
422
- message,
423
- },
424
- true,
425
- ];
368
+ return {
369
+ message,
370
+ streamState: { activeText, activeReasoning, toolInputText },
371
+ };
372
+ }
373
+ export async function deriveUIMessagesFromDeltas(threadId, streamMessages, allDeltas) {
374
+ const messages = [];
375
+ for (const streamMessage of streamMessages) {
376
+ if (streamMessage.format !== "UIMessageChunk") {
377
+ throw new Error(`deriveUIMessagesFromDeltas: unsupported stream format "${streamMessage.format ?? "text"}" for stream ${streamMessage.streamId}`);
378
+ }
379
+ const { parts } = getParts(allDeltas.filter((d) => d.streamId === streamMessage.streamId), 0);
380
+ const uiMessage = await updateFromUIMessageChunks(blankUIMessage(streamMessage, threadId), parts);
381
+ messages.push(uiMessage);
382
+ }
383
+ return sorted(messages);
384
+ }
385
+ export function getParts(deltas, fromCursor) {
386
+ const parts = [];
387
+ let cursor = fromCursor ?? 0;
388
+ for (const delta of deltas.sort((a, b) => a.start - b.start)) {
389
+ if (delta.parts.length === 0) {
390
+ console.debug(`Got delta with no parts: ${JSON.stringify(delta)}`);
391
+ continue;
392
+ }
393
+ if (cursor !== delta.start) {
394
+ if (cursor >= delta.end) {
395
+ continue;
396
+ }
397
+ else if (cursor < delta.start) {
398
+ console.warn(`Got delta for stream ${delta.streamId} that has a gap ${cursor} -> ${delta.start}`);
399
+ break;
400
+ }
401
+ else {
402
+ throw new Error(`Got unexpected delta for stream ${delta.streamId}: delta: ${delta.start} -> ${delta.end} existing cursor: ${cursor}`);
403
+ }
404
+ }
405
+ parts.push(...delta.parts);
406
+ cursor = delta.end;
407
+ }
408
+ return { parts, cursor };
426
409
  }
427
410
  function mergeProviderMetadata(existing, part) {
428
411
  if (!existing && !part) {