@convex-dev/agent 0.1.18 → 0.2.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 (122) hide show
  1. package/dist/client/createTool.d.ts +30 -11
  2. package/dist/client/createTool.d.ts.map +1 -1
  3. package/dist/client/createTool.js +24 -10
  4. package/dist/client/createTool.js.map +1 -1
  5. package/dist/client/defaultComponent.d.ts +11 -0
  6. package/dist/client/defaultComponent.d.ts.map +1 -0
  7. package/dist/client/defaultComponent.js +7 -0
  8. package/dist/client/defaultComponent.js.map +1 -0
  9. package/dist/client/definePlaygroundAPI.d.ts +68 -23
  10. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  11. package/dist/client/definePlaygroundAPI.js +7 -20
  12. package/dist/client/definePlaygroundAPI.js.map +1 -1
  13. package/dist/client/files.d.ts +14 -4
  14. package/dist/client/files.d.ts.map +1 -1
  15. package/dist/client/files.js +66 -11
  16. package/dist/client/files.js.map +1 -1
  17. package/dist/client/index.d.ts +323 -780
  18. package/dist/client/index.d.ts.map +1 -1
  19. package/dist/client/index.js +445 -543
  20. package/dist/client/index.js.map +1 -1
  21. package/dist/client/messages.d.ts +256 -0
  22. package/dist/client/messages.d.ts.map +1 -0
  23. package/dist/client/messages.js +97 -0
  24. package/dist/client/messages.js.map +1 -0
  25. package/dist/client/search.d.ts +35 -12
  26. package/dist/client/search.d.ts.map +1 -1
  27. package/dist/client/search.js +21 -4
  28. package/dist/client/search.js.map +1 -1
  29. package/dist/client/streaming.d.ts +21 -21
  30. package/dist/client/streaming.d.ts.map +1 -1
  31. package/dist/client/streaming.js +35 -40
  32. package/dist/client/streaming.js.map +1 -1
  33. package/dist/client/threads.d.ts +46 -0
  34. package/dist/client/threads.d.ts.map +1 -0
  35. package/dist/client/threads.js +49 -0
  36. package/dist/client/threads.js.map +1 -0
  37. package/dist/client/types.d.ts +159 -75
  38. package/dist/client/types.d.ts.map +1 -1
  39. package/dist/component/_generated/api.d.ts +261 -209
  40. package/dist/component/messages.d.ts +314 -105
  41. package/dist/component/messages.d.ts.map +1 -1
  42. package/dist/component/messages.js +141 -83
  43. package/dist/component/messages.js.map +1 -1
  44. package/dist/component/schema.d.ts +631 -870
  45. package/dist/component/schema.d.ts.map +1 -1
  46. package/dist/component/schema.js +11 -16
  47. package/dist/component/schema.js.map +1 -1
  48. package/dist/component/streams.d.ts +29 -149
  49. package/dist/component/streams.d.ts.map +1 -1
  50. package/dist/component/streams.js +104 -60
  51. package/dist/component/streams.js.map +1 -1
  52. package/dist/component/threads.d.ts +4 -4
  53. package/dist/component/users.d.ts +3 -3
  54. package/dist/mapping.d.ts +15 -16
  55. package/dist/mapping.d.ts.map +1 -1
  56. package/dist/mapping.js +208 -62
  57. package/dist/mapping.js.map +1 -1
  58. package/dist/parts.d.ts +3 -0
  59. package/dist/parts.d.ts.map +1 -0
  60. package/dist/parts.js +34 -0
  61. package/dist/parts.js.map +1 -0
  62. package/dist/react/SmoothText.d.ts +5 -0
  63. package/dist/react/SmoothText.d.ts.map +1 -0
  64. package/dist/react/SmoothText.js +6 -0
  65. package/dist/react/SmoothText.js.map +1 -0
  66. package/dist/react/deltas.d.ts +3 -5
  67. package/dist/react/deltas.d.ts.map +1 -1
  68. package/dist/react/deltas.js +89 -53
  69. package/dist/react/deltas.js.map +1 -1
  70. package/dist/react/index.d.ts +1 -0
  71. package/dist/react/index.d.ts.map +1 -1
  72. package/dist/react/index.js +31 -11
  73. package/dist/react/index.js.map +1 -1
  74. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  75. package/dist/react/optimisticallySendMessage.js +2 -8
  76. package/dist/react/optimisticallySendMessage.js.map +1 -1
  77. package/dist/react/toUIMessages.d.ts +5 -4
  78. package/dist/react/toUIMessages.d.ts.map +1 -1
  79. package/dist/react/toUIMessages.js +103 -40
  80. package/dist/react/toUIMessages.js.map +1 -1
  81. package/dist/react/useSmoothText.d.ts +13 -12
  82. package/dist/react/useSmoothText.d.ts.map +1 -1
  83. package/dist/react/useSmoothText.js +1 -1
  84. package/dist/react/useSmoothText.js.map +1 -1
  85. package/dist/validators.d.ts +751 -1464
  86. package/dist/validators.d.ts.map +1 -1
  87. package/dist/validators.js +39 -80
  88. package/dist/validators.js.map +1 -1
  89. package/package.json +34 -30
  90. package/src/client/createTool.ts +66 -40
  91. package/src/client/defaultComponent.ts +17 -0
  92. package/src/client/definePlaygroundAPI.ts +17 -31
  93. package/src/client/files.ts +94 -17
  94. package/src/client/index.test.ts +28 -38
  95. package/src/client/index.ts +785 -977
  96. package/src/client/messages.ts +205 -0
  97. package/src/client/search.ts +37 -10
  98. package/src/client/streaming.ts +53 -56
  99. package/src/client/threads.ts +78 -0
  100. package/src/client/types.ts +239 -131
  101. package/src/component/_generated/api.d.ts +261 -209
  102. package/src/component/messages.test.ts +182 -40
  103. package/src/component/messages.ts +165 -108
  104. package/src/component/schema.ts +11 -17
  105. package/src/component/streams.ts +180 -69
  106. package/src/mapping.ts +253 -96
  107. package/src/parts.ts +39 -0
  108. package/src/react/SmoothText.tsx +9 -0
  109. package/src/react/deltas.test.ts +38 -42
  110. package/src/react/deltas.ts +116 -65
  111. package/src/react/index.ts +35 -13
  112. package/src/react/optimisticallySendMessage.ts +2 -8
  113. package/src/react/toUIMessages.test.ts +154 -36
  114. package/src/react/toUIMessages.ts +136 -57
  115. package/src/react/useSmoothText.ts +14 -15
  116. package/src/validators.test.ts +1 -117
  117. package/src/validators.ts +57 -107
  118. package/dist/client/listMessages.d.ts +0 -22
  119. package/dist/client/listMessages.d.ts.map +0 -1
  120. package/dist/client/listMessages.js +0 -25
  121. package/dist/client/listMessages.js.map +0 -1
  122. package/src/client/listMessages.ts +0 -38
@@ -31,7 +31,7 @@ describe("toUIMessages", () => {
31
31
  const uiMessages = toUIMessages(messages);
32
32
  expect(uiMessages).toHaveLength(1);
33
33
  expect(uiMessages[0].role).toBe("user");
34
- expect(uiMessages[0].content).toBe("Hello!");
34
+ expect(uiMessages[0].text).toBe("Hello!");
35
35
  expect(uiMessages[0].parts[0]).toEqual({ type: "text", text: "Hello!" });
36
36
  });
37
37
 
@@ -48,10 +48,11 @@ describe("toUIMessages", () => {
48
48
  const uiMessages = toUIMessages(messages);
49
49
  expect(uiMessages).toHaveLength(1);
50
50
  expect(uiMessages[0].role).toBe("assistant");
51
- expect(uiMessages[0].content).toBe("Hi, how can I help?");
51
+ expect(uiMessages[0].text).toBe("Hi, how can I help?");
52
52
  expect(uiMessages[0].parts[0]).toEqual({
53
53
  type: "text",
54
54
  text: "Hi, how can I help?",
55
+ state: "done",
55
56
  });
56
57
  });
57
58
 
@@ -83,13 +84,13 @@ describe("toUIMessages", () => {
83
84
  {
84
85
  type: "file",
85
86
  mimeType: "text/plain",
86
- data: "asdfasdfasdf",
87
+ data: "https://example.com/file.txt",
87
88
  },
88
89
  {
89
90
  type: "tool-call",
90
91
  toolName: "myTool",
91
92
  toolCallId: "call1",
92
- args: "",
93
+ args: "an arg",
93
94
  },
94
95
  ],
95
96
  },
@@ -109,7 +110,6 @@ describe("toUIMessages", () => {
109
110
  },
110
111
  ],
111
112
  },
112
- text: "42",
113
113
  tool: true,
114
114
  }),
115
115
  ];
@@ -121,18 +121,15 @@ describe("toUIMessages", () => {
121
121
  );
122
122
  expect(uiMessages[1].role).toBe("assistant");
123
123
  expect(
124
- uiMessages[1].parts.filter((p) => p.type === "tool-invocation"),
124
+ uiMessages[1].parts.filter((p) => p.type === "tool-myTool"),
125
125
  ).toHaveLength(1);
126
126
  expect(
127
- uiMessages[1].parts.filter((p) => p.type === "tool-invocation")[0]
128
- .toolInvocation,
129
- ).toEqual({
130
- toolName: "myTool",
127
+ uiMessages[1].parts.filter((p) => p.type === "tool-myTool")[0],
128
+ ).toMatchObject({
129
+ type: "tool-myTool",
131
130
  toolCallId: "call1",
132
- args: "",
133
- state: "result",
134
- result: "42",
135
- step: 0,
131
+ state: "output-available",
132
+ output: "42",
136
133
  });
137
134
  });
138
135
 
@@ -167,21 +164,39 @@ describe("toUIMessages", () => {
167
164
  const uiMessages = toUIMessages(messages);
168
165
  expect(uiMessages).toHaveLength(1);
169
166
  expect(uiMessages[0].role).toBe("assistant");
170
- expect(uiMessages[0].content).toBe("Here's one idea. Here's another idea.");
171
- expect(
172
- uiMessages[0].parts.filter((p) => p.type === "reasoning"),
173
- ).toHaveLength(1);
167
+ expect(uiMessages[0].text).toBe("Here's one idea. Here's another idea.");
168
+ expect(uiMessages[0].parts.filter((p) => p.type === "reasoning")).toEqual([
169
+ {
170
+ providerMetadata: undefined,
171
+ state: undefined,
172
+ text: "I'm thinking...",
173
+ type: "reasoning",
174
+ },
175
+ {
176
+ providerMetadata: undefined,
177
+ state: undefined,
178
+ text: "I'm thinking...",
179
+ type: "reasoning",
180
+ },
181
+ ]);
174
182
  expect(uiMessages[0].parts[0].type).toBe("reasoning");
175
183
  assert(uiMessages[0].parts[0].type === "reasoning");
176
- expect(uiMessages[0].parts[0].reasoning).toBe(
177
- "I'm thinking...I'm thinking...",
178
- );
184
+ expect(uiMessages[0].parts[0].text).toBe("I'm thinking...");
185
+ expect(uiMessages[0].parts[1].type).toBe("text");
186
+ assert(uiMessages[0].parts[1].type === "text");
187
+ expect(uiMessages[0].parts[1].text).toBe("Here's one idea.");
188
+ expect(uiMessages[0].parts[2].type).toBe("reasoning");
189
+ assert(uiMessages[0].parts[2].type === "reasoning");
190
+ expect(uiMessages[0].parts[2].text).toBe("I'm thinking...");
179
191
 
180
192
  expect(uiMessages[0].parts.filter((p) => p.type === "text")).toHaveLength(
181
- 1,
193
+ 2,
182
194
  );
183
195
  expect(uiMessages[0].parts.filter((p) => p.type === "text")[0].text).toBe(
184
- "Here's one idea. Here's another idea.",
196
+ "Here's one idea.",
197
+ );
198
+ expect(uiMessages[0].parts.filter((p) => p.type === "text")[1].text).toBe(
199
+ "Here's another idea.",
185
200
  );
186
201
  });
187
202
 
@@ -198,10 +213,58 @@ describe("toUIMessages", () => {
198
213
  const uiMessages = toUIMessages(messages);
199
214
  expect(uiMessages).toHaveLength(1);
200
215
  expect(uiMessages[0].role).toBe("system");
201
- expect(uiMessages[0].content).toBe("System message here");
216
+ expect(uiMessages[0].text).toBe("System message here");
202
217
  expect(uiMessages[0].parts[0]).toEqual({
203
218
  type: "text",
204
219
  text: "System message here",
220
+ state: "done",
221
+ providerMetadata: undefined,
222
+ });
223
+ });
224
+
225
+ it("handles wrapped JSON tool output", () => {
226
+ const messages = [
227
+ baseMessageDoc({
228
+ message: {
229
+ role: "assistant",
230
+ content: [
231
+ {
232
+ type: "tool-call",
233
+ toolName: "myTool",
234
+ toolCallId: "call1",
235
+ args: { query: "test" },
236
+ },
237
+ ],
238
+ },
239
+ tool: true,
240
+ }),
241
+ baseMessageDoc({
242
+ message: {
243
+ role: "tool",
244
+ content: [
245
+ {
246
+ type: "tool-result",
247
+ toolName: "myTool",
248
+ toolCallId: "call1",
249
+ result: {
250
+ type: "json",
251
+ value: { data: "wrapped result", success: true },
252
+ },
253
+ },
254
+ ],
255
+ },
256
+ tool: true,
257
+ }),
258
+ ];
259
+ const uiMessages = toUIMessages(messages);
260
+ expect(uiMessages).toHaveLength(1);
261
+ const toolPart = uiMessages[0].parts.find((p) => p.type === "tool-myTool");
262
+ expect(toolPart).toMatchObject({
263
+ type: "tool-myTool",
264
+ toolCallId: "call1",
265
+ state: "output-available",
266
+ input: { query: "test" },
267
+ output: { data: "wrapped result", success: true }, // Should be unwrapped
205
268
  });
206
269
  });
207
270
 
@@ -215,7 +278,7 @@ describe("toUIMessages", () => {
215
278
  type: "tool-call",
216
279
  toolName: "myTool",
217
280
  toolCallId: "call1",
218
- args: "",
281
+ args: "hi",
219
282
  },
220
283
  ],
221
284
  },
@@ -226,17 +289,12 @@ describe("toUIMessages", () => {
226
289
  expect(uiMessages).toHaveLength(1);
227
290
  expect(uiMessages[0].role).toBe("assistant");
228
291
  expect(
229
- uiMessages[0].parts.filter((p) => p.type === "tool-invocation"),
230
- ).toHaveLength(1);
231
- expect(
232
- uiMessages[0].parts.filter((p) => p.type === "tool-invocation")[0]
233
- .toolInvocation,
234
- ).toEqual({
235
- toolName: "myTool",
292
+ uiMessages[0].parts.filter((p) => p.type === "tool-myTool")[0],
293
+ ).toMatchObject({
294
+ type: "tool-myTool",
236
295
  toolCallId: "call1",
237
- args: "",
238
- state: "call",
239
- step: 0,
296
+ input: "hi",
297
+ state: "input-available",
240
298
  });
241
299
  });
242
300
 
@@ -276,7 +334,7 @@ describe("toUIMessages", () => {
276
334
  expect(uiMessages).toHaveLength(1);
277
335
  expect(uiMessages[0].role).toBe("assistant");
278
336
  // Should have a tool-invocation part
279
- expect(uiMessages[0].parts.some((p) => p.type === "tool-invocation")).toBe(
337
+ expect(uiMessages[0].parts.some((p) => p.type === "tool-myTool")).toBe(
280
338
  true,
281
339
  );
282
340
  });
@@ -299,4 +357,64 @@ describe("toUIMessages", () => {
299
357
  });
300
358
 
301
359
  // Add more tests for array content, tool calls, etc. as needed
360
+
361
+ it("should update tool call state from input-available to output-available", () => {
362
+ const messages = [
363
+ baseMessageDoc({
364
+ message: {
365
+ role: "assistant",
366
+ content: [
367
+ {
368
+ type: "tool-call",
369
+ toolName: "calculator",
370
+ toolCallId: "call1",
371
+ args: { operation: "add", a: 1, b: 2 },
372
+ },
373
+ ],
374
+ },
375
+ tool: true,
376
+ }),
377
+ baseMessageDoc({
378
+ message: {
379
+ role: "tool",
380
+ content: [
381
+ {
382
+ type: "tool-result",
383
+ toolCallId: "call1",
384
+ toolName: "calculator",
385
+ result: { sum: 3 },
386
+ },
387
+ ],
388
+ },
389
+ tool: true,
390
+ }),
391
+ ];
392
+
393
+ const uiMessages = toUIMessages(messages);
394
+
395
+ // Should have one assistant message
396
+ expect(uiMessages).toHaveLength(1);
397
+ expect(uiMessages[0].role).toBe("assistant");
398
+
399
+ // Should have a single tool-calculator part (not separate tool-call and tool-result parts)
400
+ const toolParts = uiMessages[0].parts.filter(
401
+ (p) => p.type === "tool-calculator",
402
+ );
403
+ expect(toolParts).toHaveLength(1);
404
+
405
+ const toolPart = toolParts[0];
406
+ expect(toolPart).toMatchObject({
407
+ type: "tool-calculator",
408
+ toolCallId: "call1",
409
+ state: "output-available",
410
+ input: { operation: "add", a: 1, b: 2 },
411
+ output: { sum: 3 },
412
+ });
413
+
414
+ // Should NOT have a tool-call part (which is what currently happens)
415
+ const toolCallParts = uiMessages[0].parts.filter(
416
+ (p) => p.type === "tool-call",
417
+ );
418
+ expect(toolCallParts).toHaveLength(0);
419
+ });
302
420
  });
@@ -1,22 +1,41 @@
1
- import type { ToolInvocationUIPart } from "@ai-sdk/ui-utils";
2
- import type { UIMessage as AIUIMessage } from "ai";
1
+ import type {
2
+ UIMessage as AIUIMessage,
3
+ DeepPartial,
4
+ ReasoningUIPart,
5
+ SourceDocumentUIPart,
6
+ SourceUrlUIPart,
7
+ StepStartUIPart,
8
+ TextUIPart,
9
+ ToolUIPart,
10
+ UIDataTypes,
11
+ UITools,
12
+ } from "ai";
3
13
  import type { MessageDoc } from "../client/index.js";
4
14
  import { deserializeMessage, toUIFilePart } from "../mapping.js";
5
15
  import type { MessageStatus } from "../validators.js";
6
16
 
7
- export type UIMessage = AIUIMessage & {
17
+ export type UIMessage<
18
+ METADATA = unknown,
19
+ DATA_PARTS extends UIDataTypes = UIDataTypes,
20
+ TOOLS extends UITools = UITools,
21
+ > = AIUIMessage<METADATA, DATA_PARTS, TOOLS> & {
8
22
  key: string;
9
23
  order: number;
10
24
  stepOrder: number;
11
25
  status: "streaming" | MessageStatus;
12
26
  agentName?: string;
27
+ text: string;
13
28
  };
14
29
 
15
- export function toUIMessages(
30
+ export function toUIMessages<
31
+ METADATA = unknown,
32
+ DATA_PARTS extends UIDataTypes = UIDataTypes,
33
+ TOOLS extends UITools = UITools,
34
+ >(
16
35
  messages: (MessageDoc & { streaming?: boolean })[],
17
- ): UIMessage[] {
18
- const uiMessages: UIMessage[] = [];
19
- let assistantMessage: UIMessage | undefined;
36
+ ): UIMessage<METADATA, DATA_PARTS, TOOLS>[] {
37
+ const uiMessages: UIMessage<METADATA, DATA_PARTS, TOOLS>[] = [];
38
+ let assistantMessage: UIMessage<METADATA, DATA_PARTS, TOOLS> | undefined;
20
39
  for (const message of messages) {
21
40
  const coreMessage = message.message && deserializeMessage(message.message);
22
41
  const text = message.text ?? "";
@@ -31,17 +50,23 @@ export function toUIMessages(
31
50
  stepOrder: message.stepOrder,
32
51
  status: message.streaming ? ("streaming" as const) : message.status,
33
52
  key: `${message.threadId}-${message.order}-${message.stepOrder}`,
53
+ text,
54
+ };
55
+ const partCommon = {
56
+ state: message.streaming ? ("streaming" as const) : ("done" as const),
57
+ ...(message.providerMetadata
58
+ ? { providerMetadata: message.providerMetadata }
59
+ : {}),
34
60
  };
35
61
  if (coreMessage.role === "system") {
36
62
  uiMessages.push({
37
63
  ...common,
38
64
  role: "system",
39
- content: text,
40
65
  agentName: message.agentName,
41
- parts: [{ type: "text", text }],
66
+ parts: [{ type: "text", text, ...partCommon } satisfies TextUIPart],
42
67
  });
43
68
  } else if (coreMessage.role === "user") {
44
- const parts: UIMessage["parts"] = [];
69
+ const parts: UIMessage<METADATA, DATA_PARTS, TOOLS>["parts"] = [];
45
70
  if (text) {
46
71
  parts.push({ type: "text", text });
47
72
  }
@@ -56,7 +81,6 @@ export function toUIMessages(
56
81
  uiMessages.push({
57
82
  ...common,
58
83
  role: "user",
59
- content: message.text ?? "",
60
84
  parts,
61
85
  });
62
86
  } else {
@@ -72,7 +96,6 @@ export function toUIMessages(
72
96
  ...common,
73
97
  role: "assistant",
74
98
  agentName: message.agentName,
75
- content: "",
76
99
  parts: [],
77
100
  };
78
101
  uiMessages.push(assistantMessage);
@@ -83,78 +106,134 @@ export function toUIMessages(
83
106
  }
84
107
  // update it to the last message's id
85
108
  assistantMessage.id = message._id;
86
- if (message.reasoning) {
109
+ if (
110
+ message.reasoning &&
111
+ !nonStringContent.some((c) => c.type === "reasoning")
112
+ ) {
87
113
  assistantMessage.parts.push({
88
114
  type: "reasoning",
89
- reasoning: message.reasoning,
90
- details: message.reasoningDetails ?? [],
91
- });
115
+ text: message.reasoning,
116
+ ...partCommon,
117
+ } satisfies ReasoningUIPart);
92
118
  }
93
- if (message.text) {
119
+ if (message.text && !nonStringContent.length) {
94
120
  assistantMessage.parts.push({
95
121
  type: "text",
96
122
  text: message.text,
97
- });
98
- assistantMessage.content += message.text;
123
+ ...partCommon,
124
+ } satisfies TextUIPart);
99
125
  }
100
126
  for (const source of message.sources ?? []) {
101
- assistantMessage.parts.push({
102
- type: "source",
103
- source,
104
- });
127
+ if (source.sourceType === "url") {
128
+ assistantMessage.parts.push({
129
+ type: "source-url",
130
+ url: source.url!,
131
+ sourceId: source.id,
132
+ providerMetadata: message.providerMetadata,
133
+ title: source.title,
134
+ } satisfies SourceUrlUIPart);
135
+ } else {
136
+ assistantMessage.parts.push({
137
+ type: "source-document",
138
+ mediaType: source.mediaType,
139
+ sourceId: source.id,
140
+ title: source.title,
141
+ filename: source.filename,
142
+ providerMetadata: message.providerMetadata,
143
+ } satisfies SourceDocumentUIPart);
144
+ }
105
145
  }
106
146
  for (const contentPart of nonStringContent) {
107
147
  switch (contentPart.type) {
148
+ case "text":
149
+ assistantMessage.parts.push({
150
+ ...partCommon,
151
+ ...contentPart,
152
+ } satisfies TextUIPart);
153
+ break;
154
+ case "reasoning":
155
+ assistantMessage.parts.push({
156
+ ...partCommon,
157
+ ...contentPart,
158
+ } satisfies ReasoningUIPart);
159
+ break;
108
160
  case "file":
109
161
  case "image":
110
162
  assistantMessage.parts.push(toUIFilePart(contentPart));
111
163
  break;
112
- case "tool-call":
164
+ case "tool-call": {
113
165
  assistantMessage.parts.push({
114
166
  type: "step-start",
115
- });
116
- assistantMessage.parts.push({
117
- type: "tool-invocation",
118
- toolInvocation: {
119
- state: "call",
120
- step: assistantMessage.parts.filter(
121
- (part) => part.type === "tool-invocation",
122
- ).length,
123
- toolCallId: contentPart.toolCallId,
124
- toolName: contentPart.toolName,
125
- args: contentPart.args,
126
- },
127
- });
167
+ } satisfies StepStartUIPart);
168
+ const toolPart: ToolUIPart<TOOLS> = {
169
+ type: `tool-${contentPart.toolName as keyof TOOLS & string}`,
170
+ toolCallId: contentPart.toolCallId,
171
+ input: contentPart.input as DeepPartial<
172
+ TOOLS[keyof TOOLS & string]["input"]
173
+ >,
174
+ providerExecuted: contentPart.providerExecuted,
175
+ ...(message.streaming
176
+ ? { state: "input-streaming" }
177
+ : {
178
+ state: "input-available",
179
+ callProviderMetadata: message.providerMetadata,
180
+ }),
181
+ };
182
+ assistantMessage.parts.push(toolPart);
128
183
  break;
184
+ }
129
185
  case "tool-result": {
130
186
  const call = assistantMessage.parts.find(
131
187
  (part) =>
132
- part.type === "tool-invocation" &&
133
- part.toolInvocation.toolCallId === contentPart.toolCallId,
134
- ) as ToolInvocationUIPart | undefined;
135
- const toolInvocation: ToolInvocationUIPart["toolInvocation"] = {
136
- state: "result",
137
- toolCallId: contentPart.toolCallId,
138
- toolName: contentPart.toolName,
139
- args: call?.toolInvocation.args,
140
- result: contentPart.result,
141
- step:
142
- call?.toolInvocation.step ??
143
- assistantMessage.parts.filter(
144
- (part) => part.type === "tool-invocation",
145
- ).length,
146
- };
188
+ part.type === `tool-${contentPart.toolName}` &&
189
+ "toolCallId" in part &&
190
+ part.toolCallId === contentPart.toolCallId,
191
+ ) as ToolUIPart | undefined;
147
192
  if (call) {
148
- (call as ToolInvocationUIPart).toolInvocation = toolInvocation;
193
+ if (message.error) {
194
+ call.state = "output-error";
195
+ call.errorText = message.error;
196
+ call.output = contentPart.output;
197
+ } else {
198
+ call.state = "output-available";
199
+ call.output =
200
+ contentPart.output?.type === "json"
201
+ ? contentPart.output.value
202
+ : contentPart.output;
203
+ // Technically we could pull this from the doc.message
204
+ // but the ModelMessage doesn't have it
205
+ // call.providerExecuted = contentPart.providerExecuted;
206
+ }
149
207
  } else {
150
208
  console.warn(
151
209
  "Tool result without preceding tool call.. adding anyways",
152
210
  contentPart,
153
211
  );
154
- assistantMessage.parts.push({
155
- type: "tool-invocation",
156
- toolInvocation,
157
- });
212
+ if (message.error) {
213
+ assistantMessage.parts.push({
214
+ type: `tool-${contentPart.toolName}`,
215
+ toolCallId: contentPart.toolCallId,
216
+ state: "output-error",
217
+ input: undefined,
218
+ errorText: message.error,
219
+ // Technically we could pull this from the doc.message
220
+ // but the ModelMessage doesn't have it
221
+ // providerExecuted: contentPart.providerExecuted,
222
+ callProviderMetadata: message.providerMetadata,
223
+ } satisfies ToolUIPart<TOOLS>);
224
+ } else {
225
+ assistantMessage.parts.push({
226
+ type: `tool-${contentPart.toolName}`,
227
+ toolCallId: contentPart.toolCallId,
228
+ state: "output-available",
229
+ input: undefined,
230
+ output:
231
+ contentPart.output?.type === "json"
232
+ ? contentPart.output.value
233
+ : contentPart.output,
234
+ callProviderMetadata: message.providerMetadata,
235
+ } satisfies ToolUIPart<TOOLS>);
236
+ }
158
237
  }
159
238
  break;
160
239
  }
@@ -3,6 +3,19 @@ import { useEffect, useRef, useState } from "react";
3
3
  const FPS = 20;
4
4
  const MS_PER_FRAME = 1000 / FPS;
5
5
  const MAX_TIME_JUMP_MS = 250;
6
+
7
+ export type SmoothTextOptions = {
8
+ /**
9
+ * The number of characters to display per second.
10
+ */
11
+ charsPerSec?: number;
12
+ /**
13
+ * Whether to initially start streaming.
14
+ * If this later turns to false, it'll continue streaming.
15
+ * This will start streaming the first value it sees.
16
+ */
17
+ startStreaming?: boolean;
18
+ };
6
19
  /**
7
20
  * A hook that smoothly displays text as it is streamed.
8
21
  *
@@ -15,21 +28,7 @@ const MAX_TIME_JUMP_MS = 250;
15
28
  */
16
29
  export function useSmoothText(
17
30
  text: string,
18
- {
19
- charsPerSec = 256,
20
- startStreaming = false,
21
- }: {
22
- /**
23
- * The number of characters to display per second.
24
- */
25
- charsPerSec?: number;
26
- /**
27
- * Whether to initially start streaming.
28
- * If this later turns to false, it'll continue streaming.
29
- * This will start streaming the first value it sees.
30
- */
31
- startStreaming?: boolean;
32
- } = {},
31
+ { charsPerSec = 256, startStreaming = false }: SmoothTextOptions = {},
33
32
  ): [string, { cursor: number; isStreaming: boolean }] {
34
33
  const [visibleText, setVisibleText] = useState(startStreaming ? "" : text);
35
34
  const smoothState = useRef({