@tanstack/ai 0.0.1

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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/dist/esm/base-adapter.d.ts +35 -0
  4. package/dist/esm/base-adapter.js +12 -0
  5. package/dist/esm/base-adapter.js.map +1 -0
  6. package/dist/esm/core/chat-common-options.d.ts +52 -0
  7. package/dist/esm/core/chat.d.ts +30 -0
  8. package/dist/esm/core/chat.js +533 -0
  9. package/dist/esm/core/chat.js.map +1 -0
  10. package/dist/esm/core/embedding.d.ts +8 -0
  11. package/dist/esm/core/embedding.js +33 -0
  12. package/dist/esm/core/embedding.js.map +1 -0
  13. package/dist/esm/core/summarize.d.ts +9 -0
  14. package/dist/esm/core/summarize.js +36 -0
  15. package/dist/esm/core/summarize.js.map +1 -0
  16. package/dist/esm/event-client.d.ts +311 -0
  17. package/dist/esm/event-client.js +62 -0
  18. package/dist/esm/event-client.js.map +1 -0
  19. package/dist/esm/index.d.ts +16 -0
  20. package/dist/esm/index.js +50 -0
  21. package/dist/esm/index.js.map +1 -0
  22. package/dist/esm/message-converters.d.ts +52 -0
  23. package/dist/esm/message-converters.js +162 -0
  24. package/dist/esm/message-converters.js.map +1 -0
  25. package/dist/esm/stream/index.d.ts +11 -0
  26. package/dist/esm/stream/json-parser.d.ts +38 -0
  27. package/dist/esm/stream/json-parser.js +28 -0
  28. package/dist/esm/stream/json-parser.js.map +1 -0
  29. package/dist/esm/stream/message-updaters.d.ts +44 -0
  30. package/dist/esm/stream/message-updaters.js +141 -0
  31. package/dist/esm/stream/message-updaters.js.map +1 -0
  32. package/dist/esm/stream/processor.d.ts +242 -0
  33. package/dist/esm/stream/processor.js +693 -0
  34. package/dist/esm/stream/processor.js.map +1 -0
  35. package/dist/esm/stream/strategies.d.ts +43 -0
  36. package/dist/esm/stream/strategies.js +54 -0
  37. package/dist/esm/stream/strategies.js.map +1 -0
  38. package/dist/esm/stream/types.d.ts +71 -0
  39. package/dist/esm/tools/tool-calls.d.ts +112 -0
  40. package/dist/esm/tools/tool-calls.js +302 -0
  41. package/dist/esm/tools/tool-calls.js.map +1 -0
  42. package/dist/esm/tools/tool-definition.d.ts +125 -0
  43. package/dist/esm/tools/tool-definition.js +25 -0
  44. package/dist/esm/tools/tool-definition.js.map +1 -0
  45. package/dist/esm/tools/zod-converter.d.ts +30 -0
  46. package/dist/esm/tools/zod-converter.js +36 -0
  47. package/dist/esm/tools/zod-converter.js.map +1 -0
  48. package/dist/esm/types.d.ts +619 -0
  49. package/dist/esm/utilities/agent-loop-strategies.d.ts +59 -0
  50. package/dist/esm/utilities/agent-loop-strategies.js +23 -0
  51. package/dist/esm/utilities/agent-loop-strategies.js.map +1 -0
  52. package/dist/esm/utilities/chat-options.d.ts +6 -0
  53. package/dist/esm/utilities/chat-options.js +7 -0
  54. package/dist/esm/utilities/chat-options.js.map +1 -0
  55. package/dist/esm/utilities/messages.d.ts +30 -0
  56. package/dist/esm/utilities/messages.js +7 -0
  57. package/dist/esm/utilities/messages.js.map +1 -0
  58. package/dist/esm/utilities/stream-to-response.d.ts +48 -0
  59. package/dist/esm/utilities/stream-to-response.js +62 -0
  60. package/dist/esm/utilities/stream-to-response.js.map +1 -0
  61. package/package.json +65 -0
  62. package/src/base-adapter.ts +85 -0
  63. package/src/core/chat-common-options.ts +55 -0
  64. package/src/core/chat.ts +771 -0
  65. package/src/core/embedding.ts +54 -0
  66. package/src/core/summarize.ts +56 -0
  67. package/src/event-client.ts +389 -0
  68. package/src/index.ts +68 -0
  69. package/src/message-converters.ts +285 -0
  70. package/src/stream/index.ts +41 -0
  71. package/src/stream/json-parser.ts +58 -0
  72. package/src/stream/message-updaters.ts +275 -0
  73. package/src/stream/processor.ts +1092 -0
  74. package/src/stream/strategies.ts +78 -0
  75. package/src/stream/types.ts +94 -0
  76. package/src/tools/tool-calls.ts +471 -0
  77. package/src/tools/tool-definition.ts +206 -0
  78. package/src/tools/zod-converter.ts +85 -0
  79. package/src/types.ts +872 -0
  80. package/src/utilities/agent-loop-strategies.ts +85 -0
  81. package/src/utilities/chat-options.ts +35 -0
  82. package/src/utilities/messages.ts +63 -0
  83. package/src/utilities/stream-to-response.ts +116 -0
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Message Converters
3
+ *
4
+ * Functions for converting between UIMessage and ModelMessage formats.
5
+ */
6
+
7
+ import type {
8
+ ContentPart,
9
+ MessagePart,
10
+ ModelMessage,
11
+ TextPart,
12
+ ToolCallPart,
13
+ ToolResultPart,
14
+ UIMessage,
15
+ } from './types'
16
+
17
+ /**
18
+ * Helper to extract text content from string or ContentPart array
19
+ * For multimodal content, this extracts only the text parts
20
+ */
21
+ function getTextContent(content: string | null | Array<ContentPart>): string {
22
+ if (content === null) {
23
+ return ''
24
+ }
25
+ if (typeof content === 'string') {
26
+ return content
27
+ }
28
+ // Extract text from ContentPart array
29
+ return content
30
+ .filter((part) => part.type === 'text')
31
+ .map((part) => part.content)
32
+ .join('')
33
+ }
34
+
35
+ /**
36
+ * Convert UIMessages or ModelMessages to ModelMessages
37
+ */
38
+ export function convertMessagesToModelMessages(
39
+ messages: Array<UIMessage | ModelMessage>,
40
+ ): Array<ModelMessage> {
41
+ const modelMessages: Array<ModelMessage> = []
42
+ for (const msg of messages) {
43
+ if ('parts' in msg) {
44
+ // UIMessage - convert to ModelMessages
45
+ modelMessages.push(...uiMessageToModelMessages(msg))
46
+ } else {
47
+ // Already ModelMessage
48
+ modelMessages.push(msg)
49
+ }
50
+ }
51
+ return modelMessages
52
+ }
53
+
54
+ /**
55
+ * Convert a UIMessage to ModelMessage(s)
56
+ *
57
+ * This conversion handles the parts-based structure:
58
+ * - Text parts → content field
59
+ * - ToolCall parts → toolCalls array
60
+ * - ToolResult parts → separate role="tool" messages
61
+ *
62
+ * @param uiMessage - The UIMessage to convert
63
+ * @returns An array of ModelMessages (may be multiple if tool results are present)
64
+ */
65
+ export function uiMessageToModelMessages(
66
+ uiMessage: UIMessage,
67
+ ): Array<ModelMessage> {
68
+ const messages: Array<ModelMessage> = []
69
+
70
+ // Skip system messages - they're handled via systemPrompts, not ModelMessages
71
+ if (uiMessage.role === 'system') {
72
+ return messages
73
+ }
74
+
75
+ // Separate parts by type
76
+ // Note: thinking parts are UI-only and not included in ModelMessages
77
+ const textParts: Array<TextPart> = []
78
+ const toolCallParts: Array<ToolCallPart> = []
79
+ const toolResultParts: Array<ToolResultPart> = []
80
+
81
+ for (const part of uiMessage.parts) {
82
+ if (part.type === 'text') {
83
+ textParts.push(part)
84
+ } else if (part.type === 'tool-call') {
85
+ toolCallParts.push(part)
86
+ } else if (part.type === 'tool-result') {
87
+ toolResultParts.push(part)
88
+ }
89
+ // thinking parts are skipped - they're UI-only
90
+ }
91
+
92
+ // Build the main message (user or assistant)
93
+ const content = textParts.map((p) => p.content).join('') || null
94
+ const toolCalls =
95
+ toolCallParts.length > 0
96
+ ? toolCallParts
97
+ .filter(
98
+ (p) =>
99
+ p.state === 'input-complete' ||
100
+ p.state === 'approval-responded' ||
101
+ p.output !== undefined, // Include if has output (client tool result)
102
+ )
103
+ .map((p) => ({
104
+ id: p.id,
105
+ type: 'function' as const,
106
+ function: {
107
+ name: p.name,
108
+ arguments: p.arguments,
109
+ },
110
+ }))
111
+ : undefined
112
+
113
+ // Create the main message
114
+ if (uiMessage.role !== 'assistant' || content || !toolCalls) {
115
+ messages.push({
116
+ role: uiMessage.role,
117
+ content,
118
+ ...(toolCalls && toolCalls.length > 0 && { toolCalls }),
119
+ })
120
+ } else if (toolCalls.length > 0) {
121
+ // Assistant message with only tool calls
122
+ messages.push({
123
+ role: 'assistant',
124
+ content,
125
+ toolCalls,
126
+ })
127
+ }
128
+
129
+ // Add tool result messages (only completed ones)
130
+ for (const toolResultPart of toolResultParts) {
131
+ if (
132
+ toolResultPart.state === 'complete' ||
133
+ toolResultPart.state === 'error'
134
+ ) {
135
+ messages.push({
136
+ role: 'tool',
137
+ content: toolResultPart.content,
138
+ toolCallId: toolResultPart.toolCallId,
139
+ })
140
+ }
141
+ }
142
+
143
+ return messages
144
+ }
145
+
146
+ /**
147
+ * Convert a ModelMessage to UIMessage
148
+ *
149
+ * This conversion creates a parts-based structure:
150
+ * - content field → TextPart
151
+ * - toolCalls array → ToolCallPart[]
152
+ * - role="tool" messages should be converted separately and merged
153
+ *
154
+ * @param modelMessage - The ModelMessage to convert
155
+ * @param id - Optional ID for the UIMessage (generated if not provided)
156
+ * @returns A UIMessage with parts
157
+ */
158
+ export function modelMessageToUIMessage(
159
+ modelMessage: ModelMessage,
160
+ id?: string,
161
+ ): UIMessage {
162
+ const parts: Array<MessagePart> = []
163
+
164
+ // Handle content (convert multimodal content to text for UI)
165
+ const textContent = getTextContent(modelMessage.content)
166
+ if (textContent) {
167
+ parts.push({
168
+ type: 'text',
169
+ content: textContent,
170
+ })
171
+ }
172
+
173
+ // Handle tool calls
174
+ if (modelMessage.toolCalls && modelMessage.toolCalls.length > 0) {
175
+ for (const toolCall of modelMessage.toolCalls) {
176
+ parts.push({
177
+ type: 'tool-call',
178
+ id: toolCall.id,
179
+ name: toolCall.function.name,
180
+ arguments: toolCall.function.arguments,
181
+ state: 'input-complete', // Model messages have complete arguments
182
+ })
183
+ }
184
+ }
185
+
186
+ // Handle tool results (when role is "tool")
187
+ if (modelMessage.role === 'tool' && modelMessage.toolCallId) {
188
+ parts.push({
189
+ type: 'tool-result',
190
+ toolCallId: modelMessage.toolCallId,
191
+ content: getTextContent(modelMessage.content),
192
+ state: 'complete',
193
+ })
194
+ }
195
+
196
+ return {
197
+ id: id || generateMessageId(),
198
+ role: modelMessage.role === 'tool' ? 'assistant' : modelMessage.role,
199
+ parts,
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Convert an array of ModelMessages to UIMessages
205
+ *
206
+ * This handles merging tool result messages with their corresponding assistant messages
207
+ *
208
+ * @param modelMessages - Array of ModelMessages to convert
209
+ * @returns Array of UIMessages
210
+ */
211
+ export function modelMessagesToUIMessages(
212
+ modelMessages: Array<ModelMessage>,
213
+ ): Array<UIMessage> {
214
+ const uiMessages: Array<UIMessage> = []
215
+ let currentAssistantMessage: UIMessage | null = null
216
+
217
+ for (const msg of modelMessages) {
218
+ if (msg.role === 'tool') {
219
+ // Tool result - merge into the last assistant message if possible
220
+ if (
221
+ currentAssistantMessage &&
222
+ currentAssistantMessage.role === 'assistant'
223
+ ) {
224
+ currentAssistantMessage.parts.push({
225
+ type: 'tool-result',
226
+ toolCallId: msg.toolCallId!,
227
+ content: getTextContent(msg.content),
228
+ state: 'complete',
229
+ })
230
+ } else {
231
+ // No assistant message to merge into, create a standalone one
232
+ const toolResultUIMessage = modelMessageToUIMessage(msg)
233
+ uiMessages.push(toolResultUIMessage)
234
+ }
235
+ } else {
236
+ // Regular message
237
+ const uiMessage = modelMessageToUIMessage(msg)
238
+ uiMessages.push(uiMessage)
239
+
240
+ // Track assistant messages for potential tool result merging
241
+ if (msg.role === 'assistant') {
242
+ currentAssistantMessage = uiMessage
243
+ } else {
244
+ currentAssistantMessage = null
245
+ }
246
+ }
247
+ }
248
+
249
+ return uiMessages
250
+ }
251
+
252
+ /**
253
+ * Normalize a message (UIMessage or ModelMessage) to a UIMessage
254
+ * Ensures the message has an ID and createdAt timestamp
255
+ *
256
+ * @param message - Either a UIMessage or ModelMessage
257
+ * @param generateId - Function to generate a message ID if needed
258
+ * @returns A UIMessage with guaranteed id and createdAt
259
+ */
260
+ export function normalizeToUIMessage(
261
+ message: UIMessage | ModelMessage,
262
+ generateId: () => string,
263
+ ): UIMessage {
264
+ if ('parts' in message) {
265
+ // Already a UIMessage
266
+ return {
267
+ ...message,
268
+ id: message.id || generateId(),
269
+ createdAt: message.createdAt || new Date(),
270
+ }
271
+ } else {
272
+ // ModelMessage - convert to UIMessage
273
+ return {
274
+ ...modelMessageToUIMessage(message, generateId()),
275
+ createdAt: new Date(),
276
+ }
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Generate a unique message ID
282
+ */
283
+ export function generateMessageId(): string {
284
+ return `msg-${Date.now()}-${Math.random().toString(36).substring(7)}`
285
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Stream Processing Module
3
+ *
4
+ * Unified stream processing for both server and client.
5
+ */
6
+
7
+ // Core processor
8
+ export { StreamProcessor, createReplayStream } from './processor'
9
+ export type {
10
+ StreamProcessorEvents,
11
+ StreamProcessorHandlers,
12
+ StreamProcessorOptions,
13
+ } from './processor'
14
+
15
+ // Strategies
16
+ export {
17
+ ImmediateStrategy,
18
+ PunctuationStrategy,
19
+ BatchStrategy,
20
+ WordBoundaryStrategy,
21
+ CompositeStrategy,
22
+ } from './strategies'
23
+
24
+ // JSON parser
25
+ export {
26
+ PartialJSONParser,
27
+ defaultJSONParser,
28
+ parsePartialJSON,
29
+ } from './json-parser'
30
+ export type { JSONParser } from './json-parser'
31
+
32
+ // Types
33
+ export type {
34
+ ChunkStrategy,
35
+ ChunkRecording,
36
+ InternalToolCallState,
37
+ ProcessorResult,
38
+ ProcessorState,
39
+ ToolCallState,
40
+ ToolResultState,
41
+ } from './types'
@@ -0,0 +1,58 @@
1
+ /**
2
+ * JSON Parser for partial/incomplete JSON strings
3
+ *
4
+ * Used during streaming to parse tool call arguments that may be incomplete.
5
+ */
6
+
7
+ import { parse as parsePartialJSONLib } from 'partial-json'
8
+
9
+ /**
10
+ * JSON Parser interface - allows for custom parser implementations
11
+ */
12
+ export interface JSONParser {
13
+ /**
14
+ * Parse a JSON string (may be incomplete/partial)
15
+ * @param jsonString - The JSON string to parse
16
+ * @returns The parsed object, or undefined if parsing fails
17
+ */
18
+ parse: (jsonString: string) => any
19
+ }
20
+
21
+ /**
22
+ * Partial JSON Parser implementation using the partial-json library
23
+ * This parser can handle incomplete JSON strings during streaming
24
+ */
25
+ export class PartialJSONParser implements JSONParser {
26
+ /**
27
+ * Parse a potentially incomplete JSON string
28
+ * @param jsonString - The JSON string to parse (may be incomplete)
29
+ * @returns The parsed object, or undefined if parsing fails
30
+ */
31
+ parse(jsonString: string): any {
32
+ if (!jsonString || jsonString.trim() === '') {
33
+ return undefined
34
+ }
35
+
36
+ try {
37
+ return parsePartialJSONLib(jsonString)
38
+ } catch {
39
+ // If partial parsing fails, return undefined
40
+ // This is expected during early streaming when we have very little data
41
+ return undefined
42
+ }
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Default parser instance
48
+ */
49
+ export const defaultJSONParser = new PartialJSONParser()
50
+
51
+ /**
52
+ * Parse partial JSON string (convenience function)
53
+ * @param jsonString - The JSON string to parse (may be incomplete)
54
+ * @returns The parsed object, or undefined if parsing fails
55
+ */
56
+ export function parsePartialJSON(jsonString: string): any {
57
+ return defaultJSONParser.parse(jsonString)
58
+ }
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Message Updaters (Internal)
3
+ *
4
+ * Internal helper functions for updating UIMessage parts.
5
+ * These are used by StreamProcessor to manage the message array.
6
+ */
7
+
8
+ import type {
9
+ ThinkingPart,
10
+ ToolCallPart,
11
+ ToolResultPart,
12
+ UIMessage,
13
+ } from '../types'
14
+ import type { ToolCallState, ToolResultState } from './types'
15
+
16
+ /**
17
+ * Update or add a text part to a message.
18
+ *
19
+ * If the last part is a text part, update it (continuing the same text segment).
20
+ * Otherwise, create a new text part (starting a new text segment after tool calls).
21
+ */
22
+ export function updateTextPart(
23
+ messages: Array<UIMessage>,
24
+ messageId: string,
25
+ content: string,
26
+ ): Array<UIMessage> {
27
+ return messages.map((msg) => {
28
+ if (msg.id !== messageId) {
29
+ return msg
30
+ }
31
+
32
+ const parts = [...msg.parts]
33
+ const lastPart = parts.length > 0 ? parts[parts.length - 1] : null
34
+
35
+ if (lastPart && lastPart.type === 'text') {
36
+ // Update the last text part (continuing same text segment)
37
+ parts[parts.length - 1] = { type: 'text', content }
38
+ } else {
39
+ // Create new text part (starting new text segment after tool calls/results)
40
+ parts.push({ type: 'text', content })
41
+ }
42
+
43
+ return { ...msg, parts }
44
+ })
45
+ }
46
+
47
+ /**
48
+ * Update or add a tool call part to a message.
49
+ */
50
+ export function updateToolCallPart(
51
+ messages: Array<UIMessage>,
52
+ messageId: string,
53
+ toolCall: {
54
+ id: string
55
+ name: string
56
+ arguments: string
57
+ state: ToolCallState
58
+ },
59
+ ): Array<UIMessage> {
60
+ return messages.map((msg) => {
61
+ if (msg.id !== messageId) {
62
+ return msg
63
+ }
64
+
65
+ const parts = [...msg.parts]
66
+ // Find by ID, not index!
67
+ const existingPartIndex = parts.findIndex(
68
+ (p): p is ToolCallPart => p.type === 'tool-call' && p.id === toolCall.id,
69
+ )
70
+
71
+ const toolCallPart: ToolCallPart = {
72
+ type: 'tool-call',
73
+ id: toolCall.id,
74
+ name: toolCall.name,
75
+ arguments: toolCall.arguments,
76
+ state: toolCall.state,
77
+ }
78
+
79
+ if (existingPartIndex >= 0) {
80
+ // Update existing tool call
81
+ parts[existingPartIndex] = toolCallPart
82
+ } else {
83
+ // Add new tool call at the end (preserve natural streaming order)
84
+ parts.push(toolCallPart)
85
+ }
86
+
87
+ return { ...msg, parts }
88
+ })
89
+ }
90
+
91
+ /**
92
+ * Update or add a tool result part to a message.
93
+ */
94
+ export function updateToolResultPart(
95
+ messages: Array<UIMessage>,
96
+ messageId: string,
97
+ toolCallId: string,
98
+ content: string,
99
+ state: ToolResultState,
100
+ error?: string,
101
+ ): Array<UIMessage> {
102
+ return messages.map((msg) => {
103
+ if (msg.id !== messageId) {
104
+ return msg
105
+ }
106
+
107
+ const parts = [...msg.parts]
108
+ const resultPartIndex = parts.findIndex(
109
+ (p): p is ToolResultPart =>
110
+ p.type === 'tool-result' && p.toolCallId === toolCallId,
111
+ )
112
+
113
+ const toolResultPart: ToolResultPart = {
114
+ type: 'tool-result',
115
+ toolCallId,
116
+ content,
117
+ state,
118
+ ...(error && { error }),
119
+ }
120
+
121
+ if (resultPartIndex >= 0) {
122
+ parts[resultPartIndex] = toolResultPart
123
+ } else {
124
+ parts.push(toolResultPart)
125
+ }
126
+
127
+ return { ...msg, parts }
128
+ })
129
+ }
130
+
131
+ /**
132
+ * Update a tool call part with approval request metadata.
133
+ */
134
+ export function updateToolCallApproval(
135
+ messages: Array<UIMessage>,
136
+ messageId: string,
137
+ toolCallId: string,
138
+ approvalId: string,
139
+ ): Array<UIMessage> {
140
+ return messages.map((msg) => {
141
+ if (msg.id !== messageId) {
142
+ return msg
143
+ }
144
+
145
+ const parts = [...msg.parts]
146
+ const toolCallPart = parts.find(
147
+ (p): p is ToolCallPart => p.type === 'tool-call' && p.id === toolCallId,
148
+ )
149
+
150
+ if (toolCallPart) {
151
+ toolCallPart.state = 'approval-requested'
152
+ toolCallPart.approval = {
153
+ id: approvalId,
154
+ needsApproval: true,
155
+ }
156
+ }
157
+
158
+ return { ...msg, parts }
159
+ })
160
+ }
161
+
162
+ /**
163
+ * Update a tool call part's state (e.g., to "input-complete").
164
+ */
165
+ export function updateToolCallState(
166
+ messages: Array<UIMessage>,
167
+ messageId: string,
168
+ toolCallId: string,
169
+ state: ToolCallState,
170
+ ): Array<UIMessage> {
171
+ return messages.map((msg) => {
172
+ if (msg.id !== messageId) {
173
+ return msg
174
+ }
175
+
176
+ const parts = [...msg.parts]
177
+ const toolCallPart = parts.find(
178
+ (p): p is ToolCallPart => p.type === 'tool-call' && p.id === toolCallId,
179
+ )
180
+
181
+ if (toolCallPart) {
182
+ toolCallPart.state = state
183
+ }
184
+
185
+ return { ...msg, parts }
186
+ })
187
+ }
188
+
189
+ /**
190
+ * Update a tool call part with output.
191
+ * Searches all messages to find the tool call by ID.
192
+ */
193
+ export function updateToolCallWithOutput(
194
+ messages: Array<UIMessage>,
195
+ toolCallId: string,
196
+ output: any,
197
+ state?: ToolCallState,
198
+ errorText?: string,
199
+ ): Array<UIMessage> {
200
+ return messages.map((msg) => {
201
+ const parts = [...msg.parts]
202
+ const toolCallPart = parts.find(
203
+ (p): p is ToolCallPart => p.type === 'tool-call' && p.id === toolCallId,
204
+ )
205
+
206
+ if (toolCallPart) {
207
+ toolCallPart.output = errorText ? { error: errorText } : output
208
+ if (state) {
209
+ toolCallPart.state = state
210
+ } else {
211
+ toolCallPart.state = 'input-complete'
212
+ }
213
+ }
214
+
215
+ return { ...msg, parts }
216
+ })
217
+ }
218
+
219
+ /**
220
+ * Update a tool call part with approval response.
221
+ * Searches all messages to find the tool call by approval ID.
222
+ */
223
+ export function updateToolCallApprovalResponse(
224
+ messages: Array<UIMessage>,
225
+ approvalId: string,
226
+ approved: boolean,
227
+ ): Array<UIMessage> {
228
+ return messages.map((msg) => {
229
+ const parts = [...msg.parts]
230
+ const toolCallPart = parts.find(
231
+ (p): p is ToolCallPart =>
232
+ p.type === 'tool-call' && p.approval?.id === approvalId,
233
+ )
234
+
235
+ if (toolCallPart && toolCallPart.approval) {
236
+ toolCallPart.approval.approved = approved
237
+ toolCallPart.state = 'approval-responded'
238
+ }
239
+
240
+ return { ...msg, parts }
241
+ })
242
+ }
243
+
244
+ /**
245
+ * Update or add a thinking part to a message.
246
+ */
247
+ export function updateThinkingPart(
248
+ messages: Array<UIMessage>,
249
+ messageId: string,
250
+ content: string,
251
+ ): Array<UIMessage> {
252
+ return messages.map((msg) => {
253
+ if (msg.id !== messageId) {
254
+ return msg
255
+ }
256
+
257
+ const parts = [...msg.parts]
258
+ const thinkingPartIndex = parts.findIndex((p) => p.type === 'thinking')
259
+
260
+ const thinkingPart: ThinkingPart = {
261
+ type: 'thinking',
262
+ content,
263
+ }
264
+
265
+ if (thinkingPartIndex >= 0) {
266
+ // Update existing thinking part
267
+ parts[thinkingPartIndex] = thinkingPart
268
+ } else {
269
+ // Add new thinking part at the end (preserve natural streaming order)
270
+ parts.push(thinkingPart)
271
+ }
272
+
273
+ return { ...msg, parts }
274
+ })
275
+ }