@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.
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/esm/base-adapter.d.ts +35 -0
- package/dist/esm/base-adapter.js +12 -0
- package/dist/esm/base-adapter.js.map +1 -0
- package/dist/esm/core/chat-common-options.d.ts +52 -0
- package/dist/esm/core/chat.d.ts +30 -0
- package/dist/esm/core/chat.js +533 -0
- package/dist/esm/core/chat.js.map +1 -0
- package/dist/esm/core/embedding.d.ts +8 -0
- package/dist/esm/core/embedding.js +33 -0
- package/dist/esm/core/embedding.js.map +1 -0
- package/dist/esm/core/summarize.d.ts +9 -0
- package/dist/esm/core/summarize.js +36 -0
- package/dist/esm/core/summarize.js.map +1 -0
- package/dist/esm/event-client.d.ts +311 -0
- package/dist/esm/event-client.js +62 -0
- package/dist/esm/event-client.js.map +1 -0
- package/dist/esm/index.d.ts +16 -0
- package/dist/esm/index.js +50 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/message-converters.d.ts +52 -0
- package/dist/esm/message-converters.js +162 -0
- package/dist/esm/message-converters.js.map +1 -0
- package/dist/esm/stream/index.d.ts +11 -0
- package/dist/esm/stream/json-parser.d.ts +38 -0
- package/dist/esm/stream/json-parser.js +28 -0
- package/dist/esm/stream/json-parser.js.map +1 -0
- package/dist/esm/stream/message-updaters.d.ts +44 -0
- package/dist/esm/stream/message-updaters.js +141 -0
- package/dist/esm/stream/message-updaters.js.map +1 -0
- package/dist/esm/stream/processor.d.ts +242 -0
- package/dist/esm/stream/processor.js +693 -0
- package/dist/esm/stream/processor.js.map +1 -0
- package/dist/esm/stream/strategies.d.ts +43 -0
- package/dist/esm/stream/strategies.js +54 -0
- package/dist/esm/stream/strategies.js.map +1 -0
- package/dist/esm/stream/types.d.ts +71 -0
- package/dist/esm/tools/tool-calls.d.ts +112 -0
- package/dist/esm/tools/tool-calls.js +302 -0
- package/dist/esm/tools/tool-calls.js.map +1 -0
- package/dist/esm/tools/tool-definition.d.ts +125 -0
- package/dist/esm/tools/tool-definition.js +25 -0
- package/dist/esm/tools/tool-definition.js.map +1 -0
- package/dist/esm/tools/zod-converter.d.ts +30 -0
- package/dist/esm/tools/zod-converter.js +36 -0
- package/dist/esm/tools/zod-converter.js.map +1 -0
- package/dist/esm/types.d.ts +619 -0
- package/dist/esm/utilities/agent-loop-strategies.d.ts +59 -0
- package/dist/esm/utilities/agent-loop-strategies.js +23 -0
- package/dist/esm/utilities/agent-loop-strategies.js.map +1 -0
- package/dist/esm/utilities/chat-options.d.ts +6 -0
- package/dist/esm/utilities/chat-options.js +7 -0
- package/dist/esm/utilities/chat-options.js.map +1 -0
- package/dist/esm/utilities/messages.d.ts +30 -0
- package/dist/esm/utilities/messages.js +7 -0
- package/dist/esm/utilities/messages.js.map +1 -0
- package/dist/esm/utilities/stream-to-response.d.ts +48 -0
- package/dist/esm/utilities/stream-to-response.js +62 -0
- package/dist/esm/utilities/stream-to-response.js.map +1 -0
- package/package.json +65 -0
- package/src/base-adapter.ts +85 -0
- package/src/core/chat-common-options.ts +55 -0
- package/src/core/chat.ts +771 -0
- package/src/core/embedding.ts +54 -0
- package/src/core/summarize.ts +56 -0
- package/src/event-client.ts +389 -0
- package/src/index.ts +68 -0
- package/src/message-converters.ts +285 -0
- package/src/stream/index.ts +41 -0
- package/src/stream/json-parser.ts +58 -0
- package/src/stream/message-updaters.ts +275 -0
- package/src/stream/processor.ts +1092 -0
- package/src/stream/strategies.ts +78 -0
- package/src/stream/types.ts +94 -0
- package/src/tools/tool-calls.ts +471 -0
- package/src/tools/tool-definition.ts +206 -0
- package/src/tools/zod-converter.ts +85 -0
- package/src/types.ts +872 -0
- package/src/utilities/agent-loop-strategies.ts +85 -0
- package/src/utilities/chat-options.ts +35 -0
- package/src/utilities/messages.ts +63 -0
- 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
|
+
}
|