@convex-dev/agent 0.3.2 → 0.6.0-alpha.0

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 (99) hide show
  1. package/dist/UIMessages.d.ts +3 -2
  2. package/dist/UIMessages.d.ts.map +1 -1
  3. package/dist/UIMessages.js +100 -19
  4. package/dist/UIMessages.js.map +1 -1
  5. package/dist/client/createTool.d.ts +129 -25
  6. package/dist/client/createTool.d.ts.map +1 -1
  7. package/dist/client/createTool.js +65 -8
  8. package/dist/client/createTool.js.map +1 -1
  9. package/dist/client/definePlaygroundAPI.d.ts +560 -35
  10. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  11. package/dist/client/files.d.ts.map +1 -1
  12. package/dist/client/files.js +4 -2
  13. package/dist/client/files.js.map +1 -1
  14. package/dist/client/index.d.ts +462 -37
  15. package/dist/client/index.d.ts.map +1 -1
  16. package/dist/client/index.js +26 -7
  17. package/dist/client/index.js.map +1 -1
  18. package/dist/client/messages.d.ts +114 -9
  19. package/dist/client/messages.d.ts.map +1 -1
  20. package/dist/client/mockModel.d.ts +17 -17
  21. package/dist/client/mockModel.d.ts.map +1 -1
  22. package/dist/client/mockModel.js +8 -6
  23. package/dist/client/mockModel.js.map +1 -1
  24. package/dist/client/saveInputMessages.d.ts +1 -1
  25. package/dist/client/saveInputMessages.d.ts.map +1 -1
  26. package/dist/client/saveInputMessages.js +1 -1
  27. package/dist/client/saveInputMessages.js.map +1 -1
  28. package/dist/client/search.d.ts +132 -14
  29. package/dist/client/search.d.ts.map +1 -1
  30. package/dist/client/search.js +66 -19
  31. package/dist/client/search.js.map +1 -1
  32. package/dist/client/streamText.d.ts +4 -4
  33. package/dist/client/streamText.d.ts.map +1 -1
  34. package/dist/client/streamText.js.map +1 -1
  35. package/dist/client/streaming.d.ts +1491 -87
  36. package/dist/client/streaming.d.ts.map +1 -1
  37. package/dist/client/types.d.ts +26 -6
  38. package/dist/client/types.d.ts.map +1 -1
  39. package/dist/component/_generated/component.d.ts +862 -19
  40. package/dist/component/_generated/component.d.ts.map +1 -1
  41. package/dist/component/files.d.ts +12 -6
  42. package/dist/component/files.d.ts.map +1 -1
  43. package/dist/component/files.js +10 -2
  44. package/dist/component/files.js.map +1 -1
  45. package/dist/component/messages.d.ts +1247 -92
  46. package/dist/component/messages.d.ts.map +1 -1
  47. package/dist/component/schema.d.ts +2823 -217
  48. package/dist/component/schema.d.ts.map +1 -1
  49. package/dist/component/schema.js +3 -1
  50. package/dist/component/schema.js.map +1 -1
  51. package/dist/component/streams.js +1 -1
  52. package/dist/component/streams.js.map +1 -1
  53. package/dist/component/threads.d.ts +10 -10
  54. package/dist/component/vector/index.d.ts +1 -1
  55. package/dist/deltas.d.ts.map +1 -1
  56. package/dist/deltas.js +40 -5
  57. package/dist/deltas.js.map +1 -1
  58. package/dist/mapping.d.ts.map +1 -1
  59. package/dist/mapping.js +117 -25
  60. package/dist/mapping.js.map +1 -1
  61. package/dist/react/useThreadMessages.d.ts.map +1 -1
  62. package/dist/react/useThreadMessages.js +17 -9
  63. package/dist/react/useThreadMessages.js.map +1 -1
  64. package/dist/shared.d.ts +2 -2
  65. package/dist/shared.d.ts.map +1 -1
  66. package/dist/shared.js.map +1 -1
  67. package/dist/validators.d.ts +12196 -624
  68. package/dist/validators.d.ts.map +1 -1
  69. package/dist/validators.js +152 -9
  70. package/dist/validators.js.map +1 -1
  71. package/package.json +24 -22
  72. package/src/UIMessages.combineUIMessages.test.ts +239 -0
  73. package/src/UIMessages.ts +184 -66
  74. package/src/client/createTool.ts +291 -67
  75. package/src/client/files.ts +4 -2
  76. package/src/client/index.test.ts +1 -0
  77. package/src/client/index.ts +46 -25
  78. package/src/client/mockModel.ts +36 -34
  79. package/src/client/saveInputMessages.ts +2 -2
  80. package/src/client/search.test.ts +166 -0
  81. package/src/client/search.ts +121 -38
  82. package/src/client/streamText.ts +5 -5
  83. package/src/client/types.ts +31 -18
  84. package/src/component/_generated/component.ts +2329 -338
  85. package/src/component/files.ts +11 -2
  86. package/src/component/messages.test.ts +7 -7
  87. package/src/component/schema.ts +3 -1
  88. package/src/component/setup.test.ts +7 -0
  89. package/src/component/streams.ts +1 -1
  90. package/src/deltas.test.ts +90 -0
  91. package/src/deltas.ts +47 -8
  92. package/src/fromUIMessages.test.ts +32 -27
  93. package/src/mapping.test.ts +48 -0
  94. package/src/mapping.ts +234 -142
  95. package/src/react/useThreadMessages.ts +21 -9
  96. package/src/shared.ts +2 -0
  97. package/src/toUIMessages.test.ts +539 -1
  98. package/src/validators.ts +179 -20
  99. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
@@ -0,0 +1,239 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { combineUIMessages, type UIMessage } from "./UIMessages.js";
3
+
4
+ describe("combineUIMessages", () => {
5
+ it("should preserve all tool calls when combining messages", () => {
6
+ const messages: UIMessage[] = [
7
+ {
8
+ id: "msg1",
9
+ key: "thread-1-0",
10
+ order: 1,
11
+ stepOrder: 0,
12
+ status: "streaming",
13
+ role: "assistant",
14
+ parts: [
15
+ {
16
+ type: "tool-toolA",
17
+ toolCallId: "call_A",
18
+ state: "input-available",
19
+ input: {},
20
+ },
21
+ ],
22
+ text: "",
23
+ _creationTime: Date.now(),
24
+ },
25
+ {
26
+ id: "msg1",
27
+ key: "thread-1-0",
28
+ order: 1,
29
+ stepOrder: 0,
30
+ status: "streaming",
31
+ role: "assistant",
32
+ parts: [
33
+ {
34
+ type: "tool-toolB",
35
+ toolCallId: "call_B",
36
+ state: "input-available",
37
+ input: {},
38
+ },
39
+ ],
40
+ text: "",
41
+ _creationTime: Date.now(),
42
+ },
43
+ ];
44
+
45
+ const result = combineUIMessages(messages);
46
+
47
+ expect(result).toHaveLength(1);
48
+ expect(result[0].parts).toHaveLength(2);
49
+
50
+ const toolCallIds = result[0].parts
51
+ .filter((p) => p.type.startsWith("tool-"))
52
+ .map((p: any) => p.toolCallId);
53
+
54
+ expect(toolCallIds).toContain("call_A");
55
+ expect(toolCallIds).toContain("call_B");
56
+ });
57
+
58
+ it("should accumulate tool calls progressively (issue #182)", () => {
59
+ // Simulating: A(started) → B → C → A(result)
60
+ const messages: UIMessage[] = [
61
+ {
62
+ id: "msg1",
63
+ key: "thread-1-0",
64
+ order: 1,
65
+ stepOrder: 0,
66
+ status: "streaming",
67
+ role: "assistant",
68
+ parts: [
69
+ {
70
+ type: "tool-toolA",
71
+ toolCallId: "call_A",
72
+ state: "input-available",
73
+ input: {},
74
+ },
75
+ ],
76
+ text: "",
77
+ _creationTime: Date.now(),
78
+ },
79
+ {
80
+ id: "msg1",
81
+ key: "thread-1-0",
82
+ order: 1,
83
+ stepOrder: 0,
84
+ status: "streaming",
85
+ role: "assistant",
86
+ parts: [
87
+ {
88
+ type: "tool-toolA",
89
+ toolCallId: "call_A",
90
+ state: "input-available",
91
+ input: {},
92
+ },
93
+ {
94
+ type: "tool-toolB",
95
+ toolCallId: "call_B",
96
+ state: "input-available",
97
+ input: {},
98
+ },
99
+ ],
100
+ text: "",
101
+ _creationTime: Date.now(),
102
+ },
103
+ {
104
+ id: "msg1",
105
+ key: "thread-1-0",
106
+ order: 1,
107
+ stepOrder: 0,
108
+ status: "streaming",
109
+ role: "assistant",
110
+ parts: [
111
+ {
112
+ type: "tool-toolA",
113
+ toolCallId: "call_A",
114
+ state: "input-available",
115
+ input: {},
116
+ },
117
+ {
118
+ type: "tool-toolB",
119
+ toolCallId: "call_B",
120
+ state: "input-available",
121
+ input: {},
122
+ },
123
+ {
124
+ type: "tool-toolC",
125
+ toolCallId: "call_C",
126
+ state: "input-available",
127
+ input: {},
128
+ },
129
+ ],
130
+ text: "",
131
+ _creationTime: Date.now(),
132
+ },
133
+ {
134
+ id: "msg1",
135
+ key: "thread-1-0",
136
+ order: 1,
137
+ stepOrder: 0,
138
+ status: "success",
139
+ role: "assistant",
140
+ parts: [
141
+ {
142
+ type: "tool-toolA",
143
+ toolCallId: "call_A",
144
+ state: "output-available",
145
+ input: {},
146
+ output: "success",
147
+ },
148
+ {
149
+ type: "tool-toolB",
150
+ toolCallId: "call_B",
151
+ state: "input-available",
152
+ input: {},
153
+ },
154
+ {
155
+ type: "tool-toolC",
156
+ toolCallId: "call_C",
157
+ state: "input-available",
158
+ input: {},
159
+ },
160
+ ],
161
+ text: "",
162
+ _creationTime: Date.now(),
163
+ },
164
+ ];
165
+
166
+ const result = combineUIMessages(messages);
167
+
168
+ expect(result).toHaveLength(1);
169
+ expect(result[0].parts).toHaveLength(3);
170
+
171
+ const toolCallIds = result[0].parts
172
+ .filter((p) => p.type.startsWith("tool-"))
173
+ .map((p: any) => p.toolCallId);
174
+
175
+ // All tool calls should be present
176
+ expect(toolCallIds).toContain("call_A");
177
+ expect(toolCallIds).toContain("call_B");
178
+ expect(toolCallIds).toContain("call_C");
179
+
180
+ // Tool A should have the final state (output-available)
181
+ const toolA = result[0].parts.find(
182
+ (p: any) => p.type === "tool-toolA" && p.toolCallId === "call_A",
183
+ ) as any;
184
+ expect(toolA.state).toBe("output-available");
185
+ expect(toolA.output).toBe("success");
186
+ });
187
+
188
+ it("should merge tool calls with same toolCallId", () => {
189
+ const messages: UIMessage[] = [
190
+ {
191
+ id: "msg1",
192
+ key: "thread-1-0",
193
+ order: 1,
194
+ stepOrder: 0,
195
+ status: "streaming",
196
+ role: "assistant",
197
+ parts: [
198
+ {
199
+ type: "tool-toolA",
200
+ toolCallId: "call_A",
201
+ state: "input-available",
202
+ input: { test: "input" },
203
+ },
204
+ ],
205
+ text: "",
206
+ _creationTime: Date.now(),
207
+ },
208
+ {
209
+ id: "msg1",
210
+ key: "thread-1-0",
211
+ order: 1,
212
+ stepOrder: 0,
213
+ status: "success",
214
+ role: "assistant",
215
+ parts: [
216
+ {
217
+ type: "tool-toolA",
218
+ toolCallId: "call_A",
219
+ state: "output-available",
220
+ input: { test: "input" },
221
+ output: "completed",
222
+ },
223
+ ],
224
+ text: "",
225
+ _creationTime: Date.now(),
226
+ },
227
+ ];
228
+
229
+ const result = combineUIMessages(messages);
230
+
231
+ expect(result).toHaveLength(1);
232
+ expect(result[0].parts).toHaveLength(1);
233
+
234
+ const toolCall = result[0].parts[0] as any;
235
+ expect(toolCall.toolCallId).toBe("call_A");
236
+ expect(toolCall.state).toBe("output-available");
237
+ expect(toolCall.output).toBe("completed");
238
+ });
239
+ });
package/src/UIMessages.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  type SourceUrlUIPart,
9
9
  type StepStartUIPart,
10
10
  type TextUIPart,
11
+ type ToolResultPart,
11
12
  type ToolUIPart,
12
13
  type UIDataTypes,
13
14
  type UITools,
@@ -42,6 +43,7 @@ export type UIMessage<
42
43
  stepOrder: number;
43
44
  status: UIStatus;
44
45
  agentName?: string;
46
+ userId?: string;
45
47
  text: string;
46
48
  _creationTime: number;
47
49
  };
@@ -53,7 +55,7 @@ export type UIMessage<
53
55
  * @param meta - The metadata to add to the MessageDocs.
54
56
  * @returns
55
57
  */
56
- export function fromUIMessages<METADATA = unknown>(
58
+ export async function fromUIMessages<METADATA = unknown>(
57
59
  messages: UIMessage<METADATA>[],
58
60
  meta: {
59
61
  threadId: string;
@@ -63,58 +65,71 @@ export function fromUIMessages<METADATA = unknown>(
63
65
  providerOptions?: ProviderOptions;
64
66
  metadata?: METADATA;
65
67
  },
66
- ): (MessageDoc & { streaming: boolean; metadata?: METADATA })[] {
67
- return messages.flatMap((uiMessage) => {
68
- const stepOrder = uiMessage.stepOrder;
69
- const commonFields = {
70
- ...pick(meta, [
71
- "threadId",
72
- "userId",
73
- "model",
74
- "provider",
75
- "providerOptions",
76
- "metadata",
77
- ]),
78
- ...omit(uiMessage, ["parts", "role", "key", "text"]),
79
- status: uiMessage.status === "streaming" ? "pending" : "success",
80
- streaming: uiMessage.status === "streaming",
81
- // to override
82
- _id: uiMessage.id,
83
- tool: false,
84
- } satisfies MessageDoc & { streaming: boolean; metadata?: METADATA };
85
- const modelMessages = convertToModelMessages([uiMessage]);
86
- return modelMessages
87
- .map((modelMessage, i) => {
88
- if (modelMessage.content.length === 0) {
89
- return undefined;
90
- }
91
- const message = fromModelMessage(modelMessage);
92
- const tool = isTool(message);
93
- const doc: MessageDoc & { streaming: boolean; metadata?: METADATA } = {
94
- ...commonFields,
95
- _id: uiMessage.id + `-${i}`,
96
- stepOrder: stepOrder + i,
97
- message,
98
- tool,
99
- text: extractText(message),
100
- reasoning: extractReasoning(message),
101
- finishReason: tool ? "tool-calls" : "stop",
102
- sources: fromSourceParts(uiMessage.parts),
103
- };
104
- if (Array.isArray(modelMessage.content)) {
105
- const providerOptions = modelMessage.content.find(
106
- (c) => c.providerOptions,
107
- )?.providerOptions;
108
- if (providerOptions) {
109
- // convertToModelMessages changes providerMetadata to providerOptions
110
- doc.providerMetadata = providerOptions;
111
- doc.providerOptions ??= providerOptions;
68
+ ): Promise<(MessageDoc & { streaming: boolean; metadata?: METADATA })[]> {
69
+ const nested = await Promise.all(
70
+ messages.map(async (uiMessage) => {
71
+ const stepOrder = uiMessage.stepOrder;
72
+ const commonFields = {
73
+ ...pick(meta, [
74
+ "threadId",
75
+ "userId",
76
+ "model",
77
+ "provider",
78
+ "providerOptions",
79
+ "metadata",
80
+ ]),
81
+ ...omit(uiMessage, ["parts", "role", "key", "text", "userId"]),
82
+ userId: uiMessage.userId ?? meta.userId,
83
+ status: uiMessage.status === "streaming" ? "pending" : "success",
84
+ streaming: uiMessage.status === "streaming",
85
+ // to override
86
+ _id: uiMessage.id,
87
+ tool: false,
88
+ } satisfies MessageDoc & { streaming: boolean; metadata?: METADATA };
89
+ const modelMessages = await convertToModelMessages([uiMessage]);
90
+ return modelMessages
91
+ .map((modelMessage, i) => {
92
+ if (modelMessage.content.length === 0) {
93
+ return undefined;
112
94
  }
113
- }
114
- return doc;
115
- })
116
- .filter((d) => d !== undefined);
117
- });
95
+ const message = fromModelMessage(modelMessage);
96
+ const tool = isTool(message);
97
+ const doc: MessageDoc & { streaming: boolean; metadata?: METADATA } =
98
+ {
99
+ ...commonFields,
100
+ _id: uiMessage.id + `-${i}`,
101
+ stepOrder: stepOrder + i,
102
+ message,
103
+ tool,
104
+ text: extractText(message),
105
+ reasoning: extractReasoning(message),
106
+ finishReason: tool ? "tool-calls" : "stop",
107
+ sources: fromSourceParts(uiMessage.parts),
108
+ };
109
+ if (Array.isArray(modelMessage.content)) {
110
+ // Find a content part with providerOptions (type assertion needed for SDK compatibility)
111
+ const partWithProviderOptions = modelMessage.content.find(
112
+ (c): c is typeof c & { providerOptions: unknown } =>
113
+ "providerOptions" in c && c.providerOptions !== undefined,
114
+ );
115
+ if (partWithProviderOptions?.providerOptions) {
116
+ // convertToModelMessages changes providerMetadata to providerOptions
117
+ const providerOptions =
118
+ partWithProviderOptions.providerOptions as
119
+ | Record<string, Record<string, unknown>>
120
+ | undefined;
121
+ if (providerOptions) {
122
+ doc.providerMetadata = providerOptions;
123
+ doc.providerOptions ??= providerOptions;
124
+ }
125
+ }
126
+ }
127
+ return doc;
128
+ })
129
+ .filter((d) => d !== undefined);
130
+ }),
131
+ );
132
+ return nested.flat();
118
133
  }
119
134
 
120
135
  function fromSourceParts(parts: UIMessage["parts"]): Infer<typeof vSource>[] {
@@ -288,6 +303,7 @@ function createSystemUIMessage<
288
303
  text,
289
304
  role: "system",
290
305
  agentName: message.agentName,
306
+ userId: message.userId,
291
307
  parts: [{ type: "text", text, ...partCommon } satisfies TextUIPart],
292
308
  metadata: message.metadata,
293
309
  };
@@ -347,6 +363,7 @@ function createUserUIMessage<
347
363
  key: `${message.threadId}-${message.order}-${message.stepOrder}`,
348
364
  text,
349
365
  role: "user",
366
+ userId: message.userId,
350
367
  parts,
351
368
  metadata: message.metadata,
352
369
  };
@@ -370,6 +387,7 @@ function createAssistantUIMessage<
370
387
  stepOrder: firstMessage.stepOrder,
371
388
  key: `${firstMessage.threadId}-${firstMessage.order}-${firstMessage.stepOrder}`,
372
389
  agentName: firstMessage.agentName,
390
+ userId: firstMessage.userId,
373
391
  };
374
392
 
375
393
  // Get status from last message
@@ -459,10 +477,50 @@ function createAssistantUIMessage<
459
477
  break;
460
478
  }
461
479
  case "tool-result": {
480
+ const typedPart = contentPart as unknown as ToolResultPart & {
481
+ output: { type: string; value?: unknown; reason?: string };
482
+ };
483
+
484
+ // Check if this is an execution-denied result
485
+ if (typedPart.output?.type === "execution-denied") {
486
+ const call = allParts.find(
487
+ (part) =>
488
+ part.type === `tool-${contentPart.toolName}` &&
489
+ "toolCallId" in part &&
490
+ part.toolCallId === contentPart.toolCallId,
491
+ ) as ToolUIPart | undefined;
492
+
493
+ if (call) {
494
+ call.state = "output-denied";
495
+ if (!("approval" in call) || !call.approval) {
496
+ (call as ToolUIPart & { approval?: object }).approval = {
497
+ id: "",
498
+ approved: false,
499
+ reason: typedPart.output.reason,
500
+ };
501
+ } else {
502
+ const approval = (
503
+ call as ToolUIPart & {
504
+ approval: { approved?: boolean; reason?: string };
505
+ }
506
+ ).approval;
507
+ approval.approved = false;
508
+ approval.reason = typedPart.output.reason;
509
+ }
510
+ }
511
+ break;
512
+ }
513
+
462
514
  const output =
463
- typeof contentPart.output?.type === "string"
464
- ? contentPart.output.value
465
- : contentPart.output;
515
+ typeof typedPart.output?.type === "string"
516
+ ? typedPart.output.value
517
+ : typedPart.output;
518
+ // Check for error at both the content part level (isError) and message level
519
+ // isError may exist on stored tool results but isn't in ToolResultPart type
520
+ const hasError =
521
+ (contentPart as { isError?: boolean }).isError || message.error;
522
+ const errorText =
523
+ message.error || (hasError ? String(output) : undefined);
466
524
  const call = allParts.find(
467
525
  (part) =>
468
526
  part.type === `tool-${contentPart.toolName}` &&
@@ -470,26 +528,23 @@ function createAssistantUIMessage<
470
528
  part.toolCallId === contentPart.toolCallId,
471
529
  ) as ToolUIPart | undefined;
472
530
  if (call) {
473
- if (message.error) {
531
+ if (hasError) {
474
532
  call.state = "output-error";
475
- call.errorText = message.error;
533
+ call.errorText = errorText ?? "Unknown error";
476
534
  call.output = output;
477
535
  } else {
478
536
  call.state = "output-available";
479
537
  call.output = output;
480
538
  }
481
539
  } else {
482
- console.warn(
483
- "Tool result without preceding tool call.. adding anyways",
484
- contentPart,
485
- );
486
- if (message.error) {
540
+ // Tool call is on a previous page - create standalone tool part
541
+ if (hasError) {
487
542
  allParts.push({
488
543
  type: `tool-${contentPart.toolName}`,
489
544
  toolCallId: contentPart.toolCallId,
490
545
  state: "output-error",
491
546
  input: undefined,
492
- errorText: message.error,
547
+ errorText: errorText ?? "Unknown error",
493
548
  callProviderMetadata: message.providerMetadata,
494
549
  } satisfies ToolUIPart<TOOLS>);
495
550
  } else {
@@ -505,8 +560,70 @@ function createAssistantUIMessage<
505
560
  }
506
561
  break;
507
562
  }
563
+ case "tool-approval-request": {
564
+ // Find the matching tool call
565
+ const typedPart = contentPart as {
566
+ toolCallId: string;
567
+ approvalId: string;
568
+ };
569
+ const toolCallPart = allParts.find(
570
+ (part) =>
571
+ "toolCallId" in part && part.toolCallId === typedPart.toolCallId,
572
+ ) as ToolUIPart | undefined;
573
+
574
+ if (toolCallPart) {
575
+ toolCallPart.state = "approval-requested";
576
+ (toolCallPart as ToolUIPart & { approval?: object }).approval = {
577
+ id: typedPart.approvalId,
578
+ };
579
+ } else {
580
+ console.warn(
581
+ "Tool approval request without preceding tool call",
582
+ contentPart,
583
+ );
584
+ }
585
+ break;
586
+ }
587
+ case "tool-approval-response": {
588
+ // Find the tool call that has this approval by matching approval.id
589
+ const typedPart = contentPart as {
590
+ approvalId: string;
591
+ approved: boolean;
592
+ reason?: string;
593
+ };
594
+ const toolCallPart = allParts.find(
595
+ (part) =>
596
+ "approval" in part &&
597
+ (part as ToolUIPart & { approval?: { id: string } }).approval
598
+ ?.id === typedPart.approvalId,
599
+ ) as ToolUIPart | undefined;
600
+
601
+ if (toolCallPart) {
602
+ if (typedPart.approved) {
603
+ toolCallPart.state = "approval-responded";
604
+ (toolCallPart as ToolUIPart & { approval?: object }).approval = {
605
+ id: typedPart.approvalId,
606
+ approved: true,
607
+ reason: typedPart.reason,
608
+ };
609
+ } else {
610
+ toolCallPart.state = "output-denied";
611
+ (toolCallPart as ToolUIPart & { approval?: object }).approval = {
612
+ id: typedPart.approvalId,
613
+ approved: false,
614
+ reason: typedPart.reason,
615
+ };
616
+ }
617
+ } else {
618
+ console.warn(
619
+ "Tool approval response without matching approval request",
620
+ contentPart,
621
+ );
622
+ }
623
+ break;
624
+ }
508
625
  default: {
509
- const maybeSource = contentPart as SourcePart;
626
+ const maybeSource = contentPart as unknown as SourcePart;
510
627
  if (maybeSource.type === "source") {
511
628
  allParts.push(toSourcePart(maybeSource));
512
629
  } else {
@@ -583,11 +700,12 @@ export function combineUIMessages(messages: UIMessage[]): UIMessage[] {
583
700
  const previousPartIndex = newParts.findIndex(
584
701
  (p) => getToolCallId(p) === toolCallId,
585
702
  );
586
- const previousPart = newParts.splice(previousPartIndex, 1)[0];
587
- if (!previousPart) {
703
+ if (previousPartIndex === -1) {
704
+ // Tool call not found in previous parts, add it as new
588
705
  newParts.push(part);
589
706
  continue;
590
707
  }
708
+ const previousPart = newParts.splice(previousPartIndex, 1)[0];
591
709
  newParts.push(mergeParts(previousPart, part));
592
710
  }
593
711
  acc.push({