@convex-dev/agent 0.6.3 → 0.7.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 (246) hide show
  1. package/MIGRATION.md +60 -116
  2. package/dist/client/messages.d.ts +1 -398
  3. package/dist/client/messages.d.ts.map +1 -1
  4. package/dist/client/messages.js +3 -47
  5. package/dist/client/messages.js.map +1 -1
  6. package/dist/client/types.d.ts +1 -455
  7. package/dist/client/types.d.ts.map +1 -1
  8. package/dist/component/_generated/component.d.ts +805 -45
  9. package/dist/component/_generated/component.d.ts.map +1 -1
  10. package/dist/component/apiKeys.js +5 -5
  11. package/dist/component/apiKeys.js.map +1 -1
  12. package/dist/component/files.d.ts.map +1 -1
  13. package/dist/component/files.js +13 -11
  14. package/dist/component/files.js.map +1 -1
  15. package/dist/component/messages.d.ts +1252 -201
  16. package/dist/component/messages.d.ts.map +1 -1
  17. package/dist/component/messages.js +83 -47
  18. package/dist/component/messages.js.map +1 -1
  19. package/dist/component/schema.d.ts +2593 -332
  20. package/dist/component/schema.d.ts.map +1 -1
  21. package/dist/component/streams.d.ts +7 -1
  22. package/dist/component/streams.d.ts.map +1 -1
  23. package/dist/component/streams.js +46 -47
  24. package/dist/component/streams.js.map +1 -1
  25. package/dist/component/threads.d.ts.map +1 -1
  26. package/dist/component/threads.js +8 -9
  27. package/dist/component/threads.js.map +1 -1
  28. package/dist/component/users.js +2 -2
  29. package/dist/component/users.js.map +1 -1
  30. package/dist/component/vector/index.d.ts.map +1 -1
  31. package/dist/component/vector/index.js +14 -8
  32. package/dist/component/vector/index.js.map +1 -1
  33. package/dist/react/index.d.ts +5 -5
  34. package/dist/react/index.d.ts.map +1 -1
  35. package/dist/react/index.js +5 -5
  36. package/dist/react/index.js.map +1 -1
  37. package/dist/shared.d.ts +19 -7
  38. package/dist/shared.d.ts.map +1 -1
  39. package/dist/shared.js +4 -4
  40. package/dist/shared.js.map +1 -1
  41. package/dist/streaming/materializePersistedUIMessageChunks.d.ts +16 -0
  42. package/dist/streaming/materializePersistedUIMessageChunks.d.ts.map +1 -0
  43. package/dist/streaming/materializePersistedUIMessageChunks.js +302 -0
  44. package/dist/streaming/materializePersistedUIMessageChunks.js.map +1 -0
  45. package/dist/streaming/persistedUIMessageChunks.d.ts +106 -0
  46. package/dist/streaming/persistedUIMessageChunks.d.ts.map +1 -0
  47. package/dist/streaming/persistedUIMessageChunks.js +475 -0
  48. package/dist/streaming/persistedUIMessageChunks.js.map +1 -0
  49. package/dist/validators.d.ts +10026 -1055
  50. package/dist/validators.d.ts.map +1 -1
  51. package/dist/validators.js +71 -9
  52. package/dist/validators.js.map +1 -1
  53. package/dist/{UIMessages.d.ts → vercel/UIMessages.d.ts} +1 -1
  54. package/dist/vercel/UIMessages.d.ts.map +1 -0
  55. package/dist/{UIMessages.js → vercel/UIMessages.js} +63 -1
  56. package/dist/vercel/UIMessages.js.map +1 -0
  57. package/dist/{client → vercel/client}/createTool.d.ts +23 -16
  58. package/dist/vercel/client/createTool.d.ts.map +1 -0
  59. package/dist/{client → vercel/client}/createTool.js +2 -0
  60. package/dist/vercel/client/createTool.js.map +1 -0
  61. package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts +593 -134
  62. package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts.map +1 -1
  63. package/dist/{client → vercel/client}/definePlaygroundAPI.js +13 -9
  64. package/dist/vercel/client/definePlaygroundAPI.js.map +1 -0
  65. package/dist/{client → vercel/client}/files.d.ts +2 -2
  66. package/dist/vercel/client/files.d.ts.map +1 -0
  67. package/dist/vercel/client/files.js.map +1 -0
  68. package/dist/vercel/client/messages.d.ts +542 -0
  69. package/dist/vercel/client/messages.d.ts.map +1 -0
  70. package/dist/vercel/client/messages.js +66 -0
  71. package/dist/vercel/client/messages.js.map +1 -0
  72. package/dist/{client → vercel/client}/mockModel.d.ts +17 -17
  73. package/dist/vercel/client/mockModel.d.ts.map +1 -0
  74. package/dist/{client → vercel/client}/mockModel.js +12 -13
  75. package/dist/vercel/client/mockModel.js.map +1 -0
  76. package/dist/{client → vercel/client}/saveInputMessages.d.ts +2 -2
  77. package/dist/vercel/client/saveInputMessages.d.ts.map +1 -0
  78. package/dist/vercel/client/saveInputMessages.js.map +1 -0
  79. package/dist/{client → vercel/client}/search.d.ts +119 -22
  80. package/dist/vercel/client/search.d.ts.map +1 -0
  81. package/dist/{client → vercel/client}/search.js +2 -2
  82. package/dist/vercel/client/search.js.map +1 -0
  83. package/dist/{client → vercel/client}/start.d.ts +26 -11
  84. package/dist/vercel/client/start.d.ts.map +1 -0
  85. package/dist/{client → vercel/client}/start.js +64 -22
  86. package/dist/vercel/client/start.js.map +1 -0
  87. package/dist/{client → vercel/client}/streamText.d.ts +13 -12
  88. package/dist/vercel/client/streamText.d.ts.map +1 -0
  89. package/dist/{client → vercel/client}/streamText.js +43 -9
  90. package/dist/vercel/client/streamText.js.map +1 -0
  91. package/dist/{client → vercel/client}/streaming.d.ts +1410 -183
  92. package/dist/vercel/client/streaming.d.ts.map +1 -0
  93. package/dist/{client → vercel/client}/streaming.js +120 -52
  94. package/dist/vercel/client/streaming.js.map +1 -0
  95. package/dist/vercel/client/types.d.ts +456 -0
  96. package/dist/vercel/client/types.d.ts.map +1 -0
  97. package/dist/vercel/client/types.js +3 -0
  98. package/dist/vercel/client/types.js.map +1 -0
  99. package/dist/{client → vercel/client}/utils.d.ts +3 -2
  100. package/dist/vercel/client/utils.d.ts.map +1 -0
  101. package/dist/{client → vercel/client}/utils.js +2 -2
  102. package/dist/vercel/client/utils.js.map +1 -0
  103. package/dist/vercel/deltas.d.ts +31 -0
  104. package/dist/vercel/deltas.d.ts.map +1 -0
  105. package/dist/vercel/deltas.js +525 -0
  106. package/dist/vercel/deltas.js.map +1 -0
  107. package/dist/{client → vercel}/index.d.ts +484 -97
  108. package/dist/vercel/index.d.ts.map +1 -0
  109. package/dist/{client → vercel}/index.js +65 -38
  110. package/dist/vercel/index.js.map +1 -0
  111. package/dist/{mapping.d.ts → vercel/mapping.d.ts} +5 -13
  112. package/dist/vercel/mapping.d.ts.map +1 -0
  113. package/dist/{mapping.js → vercel/mapping.js} +411 -112
  114. package/dist/vercel/mapping.js.map +1 -0
  115. package/dist/{react → vercel/react}/optimisticallySendMessage.d.ts +2 -2
  116. package/dist/vercel/react/optimisticallySendMessage.d.ts.map +1 -0
  117. package/dist/vercel/react/optimisticallySendMessage.js.map +1 -0
  118. package/dist/{react → vercel/react}/types.d.ts +1 -1
  119. package/dist/vercel/react/types.d.ts.map +1 -0
  120. package/dist/{react → vercel/react}/types.js.map +1 -1
  121. package/dist/{react → vercel/react}/useDeltaStreams.d.ts +1 -1
  122. package/dist/vercel/react/useDeltaStreams.d.ts.map +1 -0
  123. package/dist/{react → vercel/react}/useDeltaStreams.js +1 -1
  124. package/dist/vercel/react/useDeltaStreams.js.map +1 -0
  125. package/dist/{react → vercel/react}/useStreamingUIMessages.d.ts +1 -1
  126. package/dist/vercel/react/useStreamingUIMessages.d.ts.map +1 -0
  127. package/dist/vercel/react/useStreamingUIMessages.js +112 -0
  128. package/dist/vercel/react/useStreamingUIMessages.js.map +1 -0
  129. package/dist/{react → vercel/react}/useThreadMessages.d.ts +1 -1
  130. package/dist/vercel/react/useThreadMessages.d.ts.map +1 -0
  131. package/dist/{react → vercel/react}/useThreadMessages.js +1 -1
  132. package/dist/vercel/react/useThreadMessages.js.map +1 -0
  133. package/dist/{react → vercel/react}/useUIMessages.d.ts +2 -1
  134. package/dist/vercel/react/useUIMessages.d.ts.map +1 -0
  135. package/dist/{react → vercel/react}/useUIMessages.js +8 -4
  136. package/dist/vercel/react/useUIMessages.js.map +1 -0
  137. package/package.json +30 -22
  138. package/src/client/messages.ts +5 -107
  139. package/src/client/types.ts +0 -621
  140. package/src/component/_generated/component.ts +1089 -48
  141. package/src/component/apiKeys.ts +5 -5
  142. package/src/component/files.test.ts +1 -1
  143. package/src/component/files.ts +14 -12
  144. package/src/component/messages.test.ts +273 -1
  145. package/src/component/messages.ts +99 -57
  146. package/src/component/streams.ts +80 -64
  147. package/src/component/threads.ts +12 -9
  148. package/src/component/users.ts +2 -2
  149. package/src/component/vector/index.ts +14 -7
  150. package/src/eslintConfig.test.ts +44 -0
  151. package/src/react/index.ts +8 -5
  152. package/src/shared.ts +26 -36
  153. package/src/streaming/materializePersistedUIMessageChunks.test.ts +513 -0
  154. package/src/streaming/materializePersistedUIMessageChunks.ts +398 -0
  155. package/src/streaming/persistedUIMessageChunks.test.ts +205 -0
  156. package/src/streaming/persistedUIMessageChunks.ts +658 -0
  157. package/src/validators.test.ts +4 -1
  158. package/src/validators.ts +89 -8
  159. package/src/{UIMessages.combineUIMessages.test.ts → vercel/UIMessages.combineUIMessages.test.ts} +4 -1
  160. package/src/{UIMessages.ts → vercel/UIMessages.ts} +96 -5
  161. package/src/{client → vercel/client}/approval.test.ts +48 -19
  162. package/src/{client → vercel/client}/createTool.ts +63 -23
  163. package/src/{client → vercel/client}/definePlaygroundAPI.ts +44 -29
  164. package/src/{client → vercel/client}/files.ts +2 -2
  165. package/src/{client → vercel/client}/index.test.ts +162 -3
  166. package/src/vercel/client/messages.ts +182 -0
  167. package/src/{client → vercel/client}/mockModel.ts +38 -39
  168. package/src/{client → vercel/client}/saveInputMessages.test.ts +1 -1
  169. package/src/{client → vercel/client}/saveInputMessages.ts +7 -4
  170. package/src/{client → vercel/client}/search.test.ts +3 -3
  171. package/src/{client → vercel/client}/search.ts +4 -4
  172. package/src/{client → vercel/client}/setup.test.ts +6 -3
  173. package/src/vercel/client/start.test.ts +14 -0
  174. package/src/{client → vercel/client}/start.ts +147 -36
  175. package/src/vercel/client/streamText.test.ts +223 -0
  176. package/src/{client → vercel/client}/streamText.ts +102 -31
  177. package/src/{client → vercel/client}/streaming.integration.test.ts +40 -115
  178. package/src/{client → vercel/client}/streaming.test.ts +178 -4
  179. package/src/{client → vercel/client}/streaming.ts +114 -52
  180. package/src/vercel/client/types.test.ts +13 -0
  181. package/src/vercel/client/types.ts +679 -0
  182. package/src/{client → vercel/client}/utils.ts +13 -7
  183. package/src/vercel/deltas.test.ts +898 -0
  184. package/src/vercel/deltas.ts +633 -0
  185. package/src/{fromUIMessages.test.ts → vercel/fromUIMessages.test.ts} +17 -1
  186. package/src/{client → vercel}/index.ts +169 -62
  187. package/src/vercel/mapping.test.ts +965 -0
  188. package/src/{mapping.ts → vercel/mapping.ts} +501 -131
  189. package/src/{react → vercel/react}/optimisticallySendMessage.ts +2 -2
  190. package/src/{react → vercel/react}/types.ts +1 -1
  191. package/src/{react → vercel/react}/useDeltaStreams.ts +6 -2
  192. package/src/{react → vercel/react}/useStreamingUIMessages.ts +67 -35
  193. package/src/{react → vercel/react}/useThreadMessages.ts +2 -2
  194. package/src/{react → vercel/react}/useUIMessages.test.ts +80 -1
  195. package/src/{react → vercel/react}/useUIMessages.ts +13 -5
  196. package/src/{toUIMessages.test.ts → vercel/toUIMessages.test.ts} +49 -2
  197. package/dist/UIMessages.d.ts.map +0 -1
  198. package/dist/UIMessages.js.map +0 -1
  199. package/dist/client/createTool.d.ts.map +0 -1
  200. package/dist/client/createTool.js.map +0 -1
  201. package/dist/client/definePlaygroundAPI.js.map +0 -1
  202. package/dist/client/files.d.ts.map +0 -1
  203. package/dist/client/files.js.map +0 -1
  204. package/dist/client/index.d.ts.map +0 -1
  205. package/dist/client/index.js.map +0 -1
  206. package/dist/client/mockModel.d.ts.map +0 -1
  207. package/dist/client/mockModel.js.map +0 -1
  208. package/dist/client/saveInputMessages.d.ts.map +0 -1
  209. package/dist/client/saveInputMessages.js.map +0 -1
  210. package/dist/client/search.d.ts.map +0 -1
  211. package/dist/client/search.js.map +0 -1
  212. package/dist/client/start.d.ts.map +0 -1
  213. package/dist/client/start.js.map +0 -1
  214. package/dist/client/streamText.d.ts.map +0 -1
  215. package/dist/client/streamText.js.map +0 -1
  216. package/dist/client/streaming.d.ts.map +0 -1
  217. package/dist/client/streaming.js.map +0 -1
  218. package/dist/client/utils.d.ts.map +0 -1
  219. package/dist/client/utils.js.map +0 -1
  220. package/dist/deltas.d.ts +0 -43
  221. package/dist/deltas.d.ts.map +0 -1
  222. package/dist/deltas.js +0 -446
  223. package/dist/deltas.js.map +0 -1
  224. package/dist/mapping.d.ts.map +0 -1
  225. package/dist/mapping.js.map +0 -1
  226. package/dist/react/optimisticallySendMessage.d.ts.map +0 -1
  227. package/dist/react/optimisticallySendMessage.js.map +0 -1
  228. package/dist/react/types.d.ts.map +0 -1
  229. package/dist/react/useDeltaStreams.d.ts.map +0 -1
  230. package/dist/react/useDeltaStreams.js.map +0 -1
  231. package/dist/react/useStreamingUIMessages.d.ts.map +0 -1
  232. package/dist/react/useStreamingUIMessages.js +0 -92
  233. package/dist/react/useStreamingUIMessages.js.map +0 -1
  234. package/dist/react/useThreadMessages.d.ts.map +0 -1
  235. package/dist/react/useThreadMessages.js.map +0 -1
  236. package/dist/react/useUIMessages.d.ts.map +0 -1
  237. package/dist/react/useUIMessages.js.map +0 -1
  238. package/src/client/streamText.test.ts +0 -102
  239. package/src/deltas.test.ts +0 -626
  240. package/src/deltas.ts +0 -569
  241. package/src/mapping.test.ts +0 -402
  242. /package/dist/{client → vercel/client}/files.js +0 -0
  243. /package/dist/{client → vercel/client}/saveInputMessages.js +0 -0
  244. /package/dist/{react → vercel/react}/optimisticallySendMessage.js +0 -0
  245. /package/dist/{react → vercel/react}/types.js +0 -0
  246. /package/src/{UIMessages.test.ts → vercel/UIMessages.test.ts} +0 -0
@@ -0,0 +1,633 @@
1
+ import {
2
+ type CustomContentUIPart,
3
+ type DynamicToolUIPart,
4
+ type ProviderMetadata,
5
+ type ReasoningFileUIPart,
6
+ type ReasoningUIPart,
7
+ type TextUIPart,
8
+ type ToolUIPart,
9
+ type UIMessageChunk,
10
+ } from "ai";
11
+ import { type UIMessage } from "./UIMessages.js";
12
+ import { joinText, sorted } from "../shared.js";
13
+ import {
14
+ type MessageStatus,
15
+ type StreamDelta,
16
+ type StreamMessage,
17
+ } from "../validators.js";
18
+
19
+ export function blankUIMessage<METADATA = unknown>(
20
+ streamMessage: StreamMessage & { metadata?: METADATA },
21
+ threadId: string,
22
+ ): UIMessage<METADATA> {
23
+ return {
24
+ id: `stream:${streamMessage.streamId}`,
25
+ key: `${threadId}-${streamMessage.order}-${streamMessage.stepOrder}`,
26
+ order: streamMessage.order,
27
+ stepOrder: streamMessage.stepOrder,
28
+ status: statusFromStreamStatus(streamMessage.status),
29
+ agentName: streamMessage.agentName,
30
+ text: "",
31
+ _creationTime: Date.now(),
32
+ role: "assistant",
33
+ parts: [],
34
+ ...(streamMessage.metadata ? { metadata: streamMessage.metadata } : {}),
35
+ };
36
+ }
37
+
38
+ export function statusFromStreamStatus(
39
+ status: StreamMessage["status"],
40
+ ): MessageStatus | "streaming" {
41
+ switch (status) {
42
+ case "streaming":
43
+ return "streaming";
44
+ case "finished":
45
+ return "success";
46
+ case "aborted":
47
+ return "failed";
48
+ default:
49
+ return "pending";
50
+ }
51
+ }
52
+
53
+ type ToolPart = ToolUIPart | DynamicToolUIPart;
54
+
55
+ function transitionToolPart<S extends ToolPart["state"]>(
56
+ part: ToolPart,
57
+ updates: { state: S } & Partial<Extract<ToolPart, { state: S }>>,
58
+ ): void {
59
+ Object.assign(part, updates);
60
+ }
61
+
62
+ export type IncrementalStreamState = {
63
+ // chunk id -> index of the streaming text part in message.parts
64
+ activeText: Record<string, number>;
65
+ // chunk id -> index of the streaming reasoning part in message.parts
66
+ activeReasoning: Record<string, number>;
67
+ // toolCallId -> raw accumulated input JSON text (kept separate from the
68
+ // parsed `input` so partial JSON can be repair-parsed each batch)
69
+ toolInputText: Record<string, string>;
70
+ };
71
+
72
+ export function emptyIncrementalStreamState(): IncrementalStreamState {
73
+ return { activeText: {}, activeReasoning: {}, toolInputText: {} };
74
+ }
75
+
76
+ /**
77
+ * Apply a batch of new UIMessageChunks to an existing UIMessage without
78
+ * replaying prior chunks. `prev` carries the ephemeral stream state that the
79
+ * UIMessage itself can't hold (which text/reasoning parts are still streaming,
80
+ * and the raw accumulated tool input text). Parts are append-only, so part
81
+ * indices stay stable across the structuredClone between batches. Behavior
82
+ * mirrors the AI SDK's processUIMessageStream.
83
+ */
84
+ export function applyUIMessageChunksIncremental<
85
+ METADATA = unknown,
86
+ DATA_PARTS extends import("ai").UIDataTypes = import("ai").UIDataTypes,
87
+ TOOLS extends import("ai").UITools = import("ai").UITools,
88
+ >(
89
+ uiMessage: UIMessage<METADATA, DATA_PARTS, TOOLS>,
90
+ newParts: UIMessageChunk[],
91
+ prev: IncrementalStreamState,
92
+ ): {
93
+ message: UIMessage<METADATA, DATA_PARTS, TOOLS>;
94
+ streamState: IncrementalStreamState;
95
+ } {
96
+ const message: UIMessage = structuredClone(uiMessage);
97
+ const activeText: Record<string, number> = { ...prev.activeText };
98
+ const activeReasoning: Record<string, number> = { ...prev.activeReasoning };
99
+ const toolInputText: Record<string, string> = { ...prev.toolInputText };
100
+ const touchedTools = new Set<string>();
101
+
102
+ const toolIndexById = new Map<string, number>();
103
+ message.parts.forEach((p, i) => {
104
+ if (
105
+ "toolCallId" in p &&
106
+ (p.type.startsWith("tool-") || p.type === "dynamic-tool")
107
+ ) {
108
+ toolIndexById.set((p as ToolPart).toolCallId, i);
109
+ }
110
+ });
111
+ const toolPartAt = (toolCallId: string): ToolPart | undefined => {
112
+ const idx = toolIndexById.get(toolCallId);
113
+ return idx === undefined ? undefined : (message.parts[idx] as ToolPart);
114
+ };
115
+ const mergeMetadata = (metadata: unknown) => {
116
+ if (metadata == null) {
117
+ return;
118
+ }
119
+ message.metadata = {
120
+ ...(message.metadata as Record<string, unknown> | undefined),
121
+ ...(metadata as Record<string, unknown>),
122
+ } as typeof message.metadata;
123
+ };
124
+
125
+ for (const part of newParts) {
126
+ switch (part.type) {
127
+ case "text-start": {
128
+ const newPart: TextUIPart = {
129
+ type: "text",
130
+ text: "",
131
+ state: "streaming",
132
+ ...(part.providerMetadata
133
+ ? { providerMetadata: part.providerMetadata }
134
+ : {}),
135
+ };
136
+ message.parts.push(newPart);
137
+ activeText[part.id] = message.parts.length - 1;
138
+ break;
139
+ }
140
+ case "text-delta": {
141
+ const idx = activeText[part.id];
142
+ if (idx !== undefined) {
143
+ const textPart = message.parts[idx] as TextUIPart;
144
+ textPart.text += part.delta;
145
+ const providerMetadata = mergeProviderMetadata(
146
+ textPart.providerMetadata,
147
+ part.providerMetadata,
148
+ );
149
+ if (providerMetadata) textPart.providerMetadata = providerMetadata;
150
+ }
151
+ break;
152
+ }
153
+ case "text-end": {
154
+ const idx = activeText[part.id];
155
+ if (idx !== undefined) {
156
+ const textPart = message.parts[idx] as TextUIPart;
157
+ textPart.state = "done";
158
+ const providerMetadata = mergeProviderMetadata(
159
+ textPart.providerMetadata,
160
+ part.providerMetadata,
161
+ );
162
+ if (providerMetadata) textPart.providerMetadata = providerMetadata;
163
+ delete activeText[part.id];
164
+ }
165
+ break;
166
+ }
167
+ case "reasoning-start": {
168
+ const newPart: ReasoningUIPart = {
169
+ type: "reasoning",
170
+ text: "",
171
+ state: "streaming",
172
+ ...(part.providerMetadata
173
+ ? { providerMetadata: part.providerMetadata }
174
+ : {}),
175
+ };
176
+ message.parts.push(newPart);
177
+ activeReasoning[part.id] = message.parts.length - 1;
178
+ break;
179
+ }
180
+ case "reasoning-delta": {
181
+ const idx = activeReasoning[part.id];
182
+ if (idx !== undefined) {
183
+ const reasoningPart = message.parts[idx] as ReasoningUIPart;
184
+ reasoningPart.text += part.delta;
185
+ const providerMetadata = mergeProviderMetadata(
186
+ reasoningPart.providerMetadata,
187
+ part.providerMetadata,
188
+ );
189
+ if (providerMetadata)
190
+ reasoningPart.providerMetadata = providerMetadata;
191
+ }
192
+ break;
193
+ }
194
+ case "reasoning-end": {
195
+ const idx = activeReasoning[part.id];
196
+ if (idx !== undefined) {
197
+ const reasoningPart = message.parts[idx] as ReasoningUIPart;
198
+ reasoningPart.state = "done";
199
+ const providerMetadata = mergeProviderMetadata(
200
+ reasoningPart.providerMetadata,
201
+ part.providerMetadata,
202
+ );
203
+ if (providerMetadata)
204
+ reasoningPart.providerMetadata = providerMetadata;
205
+ delete activeReasoning[part.id];
206
+ }
207
+ break;
208
+ }
209
+ case "reasoning-file":
210
+ message.parts.push({
211
+ type: "reasoning-file",
212
+ url: part.url,
213
+ mediaType: part.mediaType,
214
+ ...(part.providerMetadata
215
+ ? { providerMetadata: part.providerMetadata }
216
+ : {}),
217
+ } satisfies ReasoningFileUIPart);
218
+ break;
219
+ case "custom":
220
+ message.parts.push({
221
+ type: "custom",
222
+ kind: part.kind,
223
+ ...(part.providerMetadata
224
+ ? { providerMetadata: part.providerMetadata }
225
+ : {}),
226
+ } satisfies CustomContentUIPart);
227
+ break;
228
+ case "tool-input-start": {
229
+ const newToolPart: ToolUIPart | DynamicToolUIPart = part.dynamic
230
+ ? ({
231
+ type: "dynamic-tool",
232
+ toolCallId: part.toolCallId,
233
+ toolName: part.toolName,
234
+ state: "input-streaming",
235
+ input: undefined,
236
+ ...(part.providerExecuted === undefined
237
+ ? {}
238
+ : { providerExecuted: part.providerExecuted }),
239
+ ...(part.title === undefined ? {} : { title: part.title }),
240
+ ...(part.toolMetadata === undefined
241
+ ? {}
242
+ : { toolMetadata: part.toolMetadata }),
243
+ ...(part.providerMetadata
244
+ ? { callProviderMetadata: part.providerMetadata }
245
+ : {}),
246
+ } satisfies DynamicToolUIPart)
247
+ : ({
248
+ type: `tool-${part.toolName}`,
249
+ toolCallId: part.toolCallId,
250
+ state: "input-streaming",
251
+ input: undefined,
252
+ ...(part.providerExecuted === undefined
253
+ ? {}
254
+ : { providerExecuted: part.providerExecuted }),
255
+ ...(part.title === undefined ? {} : { title: part.title }),
256
+ ...(part.toolMetadata === undefined
257
+ ? {}
258
+ : { toolMetadata: part.toolMetadata }),
259
+ ...(part.providerMetadata
260
+ ? { callProviderMetadata: part.providerMetadata }
261
+ : {}),
262
+ } satisfies ToolUIPart);
263
+ message.parts.push(newToolPart);
264
+ toolIndexById.set(part.toolCallId, message.parts.length - 1);
265
+ toolInputText[part.toolCallId] = "";
266
+ break;
267
+ }
268
+ case "tool-input-delta": {
269
+ if (toolIndexById.has(part.toolCallId)) {
270
+ toolInputText[part.toolCallId] =
271
+ (toolInputText[part.toolCallId] ?? "") + part.inputTextDelta;
272
+ touchedTools.add(part.toolCallId);
273
+ } else {
274
+ console.warn(
275
+ `tool-input-delta for unknown toolCallId ${part.toolCallId}`,
276
+ );
277
+ }
278
+ break;
279
+ }
280
+ case "tool-input-available": {
281
+ let toolPart = toolPartAt(part.toolCallId);
282
+ if (!toolPart) {
283
+ toolPart = part.dynamic
284
+ ? ({
285
+ type: "dynamic-tool",
286
+ toolCallId: part.toolCallId,
287
+ toolName: part.toolName,
288
+ state: "input-available",
289
+ input: part.input,
290
+ } satisfies DynamicToolUIPart)
291
+ : ({
292
+ type: `tool-${part.toolName}`,
293
+ toolCallId: part.toolCallId,
294
+ state: "input-available",
295
+ input: part.input,
296
+ } satisfies ToolUIPart);
297
+ message.parts.push(toolPart);
298
+ toolIndexById.set(part.toolCallId, message.parts.length - 1);
299
+ }
300
+ const callProviderMetadata = mergeProviderMetadata(
301
+ (toolPart as { callProviderMetadata?: ProviderMetadata })
302
+ .callProviderMetadata,
303
+ part.providerMetadata,
304
+ );
305
+ transitionToolPart(toolPart, {
306
+ state: "input-available",
307
+ input: part.input,
308
+ ...(part.providerExecuted === undefined
309
+ ? {}
310
+ : { providerExecuted: part.providerExecuted }),
311
+ ...(part.title === undefined ? {} : { title: part.title }),
312
+ ...(part.toolMetadata === undefined
313
+ ? {}
314
+ : { toolMetadata: part.toolMetadata }),
315
+ ...(callProviderMetadata ? { callProviderMetadata } : {}),
316
+ });
317
+ touchedTools.delete(part.toolCallId);
318
+ // The raw JSON buffer is no longer needed; drop it so it doesn't get
319
+ // carried through every later batch on the hot path.
320
+ delete toolInputText[part.toolCallId];
321
+ break;
322
+ }
323
+ case "tool-input-error": {
324
+ let toolPart = toolPartAt(part.toolCallId);
325
+ if (!toolPart) {
326
+ toolPart = part.dynamic
327
+ ? ({
328
+ type: "dynamic-tool",
329
+ toolCallId: part.toolCallId,
330
+ toolName: part.toolName,
331
+ state: "output-error",
332
+ input: part.input,
333
+ errorText: part.errorText,
334
+ } satisfies DynamicToolUIPart)
335
+ : ({
336
+ type: `tool-${part.toolName}`,
337
+ toolCallId: part.toolCallId,
338
+ state: "output-error",
339
+ input: undefined,
340
+ rawInput: part.input,
341
+ errorText: part.errorText,
342
+ } satisfies ToolUIPart);
343
+ message.parts.push(toolPart);
344
+ toolIndexById.set(part.toolCallId, message.parts.length - 1);
345
+ }
346
+ const resultProviderMetadata = mergeProviderMetadata(
347
+ (toolPart as { resultProviderMetadata?: ProviderMetadata })
348
+ .resultProviderMetadata,
349
+ part.providerMetadata,
350
+ );
351
+ transitionToolPart(toolPart, {
352
+ state: "output-error",
353
+ errorText: part.errorText,
354
+ ...(part.providerExecuted === undefined
355
+ ? {}
356
+ : { providerExecuted: part.providerExecuted }),
357
+ ...(toolPart.type === "dynamic-tool"
358
+ ? {
359
+ input: part.input,
360
+ }
361
+ : {
362
+ input: undefined,
363
+ rawInput: part.input,
364
+ }),
365
+ ...(part.title === undefined ? {} : { title: part.title }),
366
+ ...(part.toolMetadata === undefined
367
+ ? {}
368
+ : { toolMetadata: part.toolMetadata }),
369
+ ...(resultProviderMetadata ? { resultProviderMetadata } : {}),
370
+ });
371
+ touchedTools.delete(part.toolCallId);
372
+ delete toolInputText[part.toolCallId];
373
+ break;
374
+ }
375
+ case "tool-output-available": {
376
+ const toolPart = toolPartAt(part.toolCallId);
377
+ if (toolPart) {
378
+ transitionToolPart(toolPart, {
379
+ state: "output-available",
380
+ output: part.output,
381
+ preliminary: part.preliminary,
382
+ ...(part.providerExecuted === undefined
383
+ ? {}
384
+ : { providerExecuted: part.providerExecuted }),
385
+ ...(part.providerMetadata
386
+ ? { resultProviderMetadata: part.providerMetadata }
387
+ : {}),
388
+ ...(part.toolMetadata === undefined
389
+ ? {}
390
+ : { toolMetadata: part.toolMetadata }),
391
+ });
392
+ }
393
+ break;
394
+ }
395
+ case "tool-output-error": {
396
+ const toolPart = toolPartAt(part.toolCallId);
397
+ if (toolPart) {
398
+ transitionToolPart(toolPart, {
399
+ state: "output-error",
400
+ errorText: part.errorText,
401
+ output: undefined,
402
+ ...(part.providerExecuted === undefined
403
+ ? {}
404
+ : { providerExecuted: part.providerExecuted }),
405
+ ...(part.providerMetadata
406
+ ? { resultProviderMetadata: part.providerMetadata }
407
+ : {}),
408
+ ...(part.toolMetadata === undefined
409
+ ? {}
410
+ : { toolMetadata: part.toolMetadata }),
411
+ });
412
+ // The SDK clears a prior preliminary result when the final outcome
413
+ // is an error. `preliminary` is not part of the error-state type.
414
+ Object.assign(toolPart, { preliminary: undefined });
415
+ }
416
+ break;
417
+ }
418
+ case "tool-output-denied": {
419
+ const toolPart = toolPartAt(part.toolCallId);
420
+ if (toolPart) {
421
+ transitionToolPart(toolPart, { state: "output-denied" });
422
+ }
423
+ break;
424
+ }
425
+ case "tool-approval-request": {
426
+ const toolPart = toolPartAt(part.toolCallId);
427
+ if (toolPart) {
428
+ transitionToolPart(toolPart, {
429
+ state: "approval-requested",
430
+ approval: {
431
+ id: part.approvalId,
432
+ ...(part.isAutomatic === undefined
433
+ ? {}
434
+ : { isAutomatic: part.isAutomatic }),
435
+ ...(part.signature === undefined
436
+ ? {}
437
+ : { signature: part.signature }),
438
+ },
439
+ });
440
+ }
441
+ break;
442
+ }
443
+ case "tool-approval-response": {
444
+ const toolPart = message.parts.find(
445
+ (candidate) =>
446
+ "approval" in candidate &&
447
+ (candidate as ToolPart).approval?.id === part.approvalId,
448
+ ) as ToolPart | undefined;
449
+ if (toolPart) {
450
+ const callProviderMetadata = mergeProviderMetadata(
451
+ (toolPart as { callProviderMetadata?: ProviderMetadata })
452
+ .callProviderMetadata,
453
+ part.providerMetadata,
454
+ );
455
+ transitionToolPart(toolPart, {
456
+ state: "approval-responded",
457
+ approval: {
458
+ ...toolPart.approval,
459
+ id: part.approvalId,
460
+ approved: part.approved,
461
+ ...(part.reason === undefined ? {} : { reason: part.reason }),
462
+ },
463
+ ...(part.providerExecuted === undefined
464
+ ? {}
465
+ : { providerExecuted: part.providerExecuted }),
466
+ ...(callProviderMetadata ? { callProviderMetadata } : {}),
467
+ });
468
+ }
469
+ break;
470
+ }
471
+ case "source-url":
472
+ message.parts.push({
473
+ type: "source-url",
474
+ url: part.url,
475
+ sourceId: part.sourceId,
476
+ title: part.title,
477
+ providerMetadata: part.providerMetadata,
478
+ });
479
+ break;
480
+ case "source-document":
481
+ message.parts.push({
482
+ type: "source-document",
483
+ mediaType: part.mediaType,
484
+ sourceId: part.sourceId,
485
+ title: part.title,
486
+ filename: part.filename,
487
+ providerMetadata: part.providerMetadata,
488
+ });
489
+ break;
490
+ case "file":
491
+ message.parts.push({
492
+ type: "file",
493
+ mediaType: part.mediaType,
494
+ url: part.url,
495
+ providerMetadata: part.providerMetadata,
496
+ });
497
+ break;
498
+ case "start-step":
499
+ message.parts.push({ type: "step-start" });
500
+ break;
501
+ case "finish-step":
502
+ // Match the SDK: a new step starts fresh streaming parts; the prior
503
+ // parts keep their state rather than being forced to "done".
504
+ for (const id of Object.keys(activeText)) delete activeText[id];
505
+ for (const id of Object.keys(activeReasoning))
506
+ delete activeReasoning[id];
507
+ break;
508
+ case "start":
509
+ case "finish":
510
+ case "message-metadata":
511
+ mergeMetadata(part.messageMetadata);
512
+ break;
513
+ case "abort":
514
+ case "error":
515
+ // The stream-level status (statusFromStreamStatus) is authoritative and
516
+ // is applied by the caller; nothing to mutate on the message here.
517
+ break;
518
+ default: {
519
+ if (typeof part.type === "string" && part.type.startsWith("data-")) {
520
+ const dataPart = part as Extract<
521
+ UIMessageChunk,
522
+ { type: `data-${string}` }
523
+ >;
524
+ const existingIdx =
525
+ dataPart.id != null
526
+ ? message.parts.findIndex(
527
+ (p) =>
528
+ p.type === dataPart.type &&
529
+ (p as { id?: string }).id === dataPart.id,
530
+ )
531
+ : -1;
532
+ if (dataPart.transient) {
533
+ break;
534
+ }
535
+ if (existingIdx >= 0) {
536
+ (message.parts[existingIdx] as { data?: unknown }).data =
537
+ dataPart.data;
538
+ } else {
539
+ message.parts.push(
540
+ dataPart as unknown as UIMessage["parts"][number],
541
+ );
542
+ }
543
+ } else {
544
+ console.warn(
545
+ `applyUIMessageChunksIncremental: unhandled chunk type ${String(part.type)}`,
546
+ );
547
+ }
548
+ break;
549
+ }
550
+ }
551
+ }
552
+
553
+ for (const toolCallId of touchedTools) {
554
+ const toolPart = toolPartAt(toolCallId);
555
+ if (toolPart && toolPart.state === "input-streaming") {
556
+ try {
557
+ toolPart.input = JSON.parse(toolInputText[toolCallId] ?? "");
558
+ } catch {
559
+ // partial JSON — leave input unset until complete
560
+ }
561
+ }
562
+ }
563
+
564
+ message.text = joinText(message.parts);
565
+ return {
566
+ message: message as UIMessage<METADATA, DATA_PARTS, TOOLS>,
567
+ streamState: { activeText, activeReasoning, toolInputText },
568
+ };
569
+ }
570
+
571
+ export async function deriveUIMessagesFromDeltas(
572
+ threadId: string,
573
+ streamMessages: StreamMessage[],
574
+ allDeltas: StreamDelta[],
575
+ ): Promise<UIMessage[]> {
576
+ const messages: UIMessage[] = [];
577
+ for (const streamMessage of streamMessages) {
578
+ if (streamMessage.format !== "UIMessageChunk") {
579
+ throw new Error(
580
+ `deriveUIMessagesFromDeltas: unsupported stream format "${streamMessage.format ?? "text"}" for stream ${streamMessage.streamId}`,
581
+ );
582
+ }
583
+ const { parts } = getParts<UIMessageChunk>(
584
+ allDeltas.filter((d) => d.streamId === streamMessage.streamId),
585
+ 0,
586
+ );
587
+ const uiMessage = applyUIMessageChunksIncremental(
588
+ blankUIMessage(streamMessage, threadId),
589
+ parts,
590
+ emptyIncrementalStreamState(),
591
+ ).message;
592
+ messages.push(uiMessage);
593
+ }
594
+ return sorted(messages);
595
+ }
596
+
597
+ export function getParts<T extends StreamDelta["parts"][number]>(
598
+ deltas: StreamDelta[],
599
+ fromCursor?: number,
600
+ ): { parts: T[]; cursor: number } {
601
+ const parts: T[] = [];
602
+ let cursor = fromCursor ?? 0;
603
+ for (const delta of deltas.sort((a, b) => a.start - b.start)) {
604
+ if (delta.parts.length === 0) {
605
+ console.debug(`Got delta with no parts: ${JSON.stringify(delta)}`);
606
+ continue;
607
+ }
608
+ if (cursor !== delta.start) {
609
+ if (cursor >= delta.end) {
610
+ continue;
611
+ } else if (cursor < delta.start) {
612
+ console.warn(
613
+ `Got delta for stream ${delta.streamId} that has a gap ${cursor} -> ${delta.start}`,
614
+ );
615
+ break;
616
+ } else {
617
+ throw new Error(
618
+ `Got unexpected delta for stream ${delta.streamId}: delta: ${delta.start} -> ${delta.end} existing cursor: ${cursor}`,
619
+ );
620
+ }
621
+ }
622
+ parts.push(...delta.parts);
623
+ cursor = delta.end;
624
+ }
625
+ return { parts, cursor };
626
+ }
627
+
628
+ function mergeProviderMetadata(
629
+ existing: ProviderMetadata | undefined,
630
+ part: ProviderMetadata | undefined,
631
+ ): ProviderMetadata | undefined {
632
+ return part ?? existing;
633
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import type { MessageDoc } from "./client/index.js";
2
+ import type { MessageDoc } from "../validators.js";
3
3
  import type { UIMessage } from "./UIMessages.js";
4
4
  import { fromUIMessages, toUIMessages } from "./UIMessages.js";
5
5
 
@@ -310,6 +310,7 @@ describe("fromUIMessages round-trip tests", () => {
310
310
  id: "source2",
311
311
  mediaType: "application/pdf",
312
312
  title: "Document Source",
313
+ filename: "document.pdf",
313
314
  },
314
315
  ],
315
316
  }),
@@ -327,6 +328,13 @@ describe("fromUIMessages round-trip tests", () => {
327
328
  (part) => part.type === "source-url" || part.type === "source-document",
328
329
  );
329
330
  expect(sourceParts).toHaveLength(2);
331
+ expect(sourceParts).toContainEqual({
332
+ type: "source-document",
333
+ sourceId: "source2",
334
+ mediaType: "application/pdf",
335
+ title: "Document Source",
336
+ filename: "document.pdf",
337
+ });
330
338
 
331
339
  expect(backToMessageDocs).toHaveLength(1);
332
340
  expect(backToMessageDocs[0].sources).toHaveLength(2);
@@ -337,6 +345,14 @@ describe("fromUIMessages round-trip tests", () => {
337
345
  url: "https://example.com",
338
346
  title: "Example Source",
339
347
  });
348
+ expect(backToMessageDocs[0].sources![1]).toMatchObject({
349
+ type: "source",
350
+ sourceType: "document",
351
+ id: "source2",
352
+ mediaType: "application/pdf",
353
+ title: "Document Source",
354
+ filename: "document.pdf",
355
+ });
340
356
  });
341
357
 
342
358
  it("preserves metadata when provided", async () => {