@tanstack/ai 0.49.1 → 0.51.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.
- package/dist/esm/activities/chat/index.js +4 -4
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.d.ts +7 -0
- package/dist/esm/activities/chat/messages.js +270 -64
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +2 -0
- package/dist/esm/activities/chat/stream/processor.js +46 -19
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/client.js +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +4 -3
- package/dist/esm/types.d.ts +18 -0
- package/dist/esm/utilities/ag-ui-wire.d.ts +20 -44
- package/dist/esm/utilities/ag-ui-wire.js +143 -34
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/errors.d.ts +27 -0
- package/dist/esm/utilities/errors.js +19 -1
- package/dist/esm/utilities/errors.js.map +1 -1
- package/package.json +2 -2
- package/skills/ai-core/SKILL.md +17 -0
- package/src/activities/chat/index.ts +3 -4
- package/src/activities/chat/messages.ts +344 -61
- package/src/activities/chat/stream/processor.ts +70 -16
- package/src/index.ts +2 -0
- package/src/types.ts +18 -0
- package/src/utilities/ag-ui-wire.ts +278 -73
- package/src/utilities/errors.ts +42 -0
|
@@ -103,7 +103,6 @@ import type {
|
|
|
103
103
|
Interrupt,
|
|
104
104
|
JSONSchema,
|
|
105
105
|
LazyToolsConfig,
|
|
106
|
-
MessagesSnapshotEvent,
|
|
107
106
|
ModelMessage,
|
|
108
107
|
ProviderTool,
|
|
109
108
|
RunFinishedEvent,
|
|
@@ -2756,9 +2755,9 @@ class TextEngine<
|
|
|
2756
2755
|
return {
|
|
2757
2756
|
type: EventType.MESSAGES_SNAPSHOT,
|
|
2758
2757
|
timestamp: Date.now(),
|
|
2759
|
-
messages: uiMessagesToWire(
|
|
2760
|
-
|
|
2761
|
-
)
|
|
2758
|
+
messages: uiMessagesToWire(modelMessagesToUIMessages(withIds), {
|
|
2759
|
+
includeSnapshotStructuredOutput: true,
|
|
2760
|
+
}),
|
|
2762
2761
|
}
|
|
2763
2762
|
}
|
|
2764
2763
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { isProviderExecutedToolCall } from '../../utilities/provider-executed'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
isContentPartArray,
|
|
4
|
+
normalizeToolResult,
|
|
5
|
+
} from '../../utilities/tool-result'
|
|
3
6
|
import { tanstackMetadata } from '../../utilities/merge-metadata'
|
|
4
7
|
import type { Message as AGUIMessage } from '@ag-ui/core'
|
|
5
8
|
import type {
|
|
@@ -7,11 +10,17 @@ import type {
|
|
|
7
10
|
MessagePart,
|
|
8
11
|
ModelMessage,
|
|
9
12
|
StructuredOutputPart,
|
|
13
|
+
TanStackMessageMetadata,
|
|
10
14
|
TextPart,
|
|
11
15
|
ToolCall,
|
|
12
16
|
ToolCallPart,
|
|
13
17
|
UIMessage,
|
|
18
|
+
UIResourcePart,
|
|
14
19
|
} from '../../types'
|
|
20
|
+
|
|
21
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
22
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
23
|
+
}
|
|
15
24
|
// ===========================
|
|
16
25
|
// Message Converters
|
|
17
26
|
// ===========================
|
|
@@ -56,6 +65,9 @@ function toolCallFromWire(toolCall: ToolCall, bag: unknown): ToolCall {
|
|
|
56
65
|
? bag
|
|
57
66
|
: undefined
|
|
58
67
|
const encrypted = encryptedValueFrom(toolCall)
|
|
68
|
+
if (bag === null && encrypted === undefined) {
|
|
69
|
+
return { ...toolCall, metadata: null }
|
|
70
|
+
}
|
|
59
71
|
if (fromBag === undefined && encrypted === undefined) return toolCall
|
|
60
72
|
return {
|
|
61
73
|
...toolCall,
|
|
@@ -112,6 +124,12 @@ function getTextContent(
|
|
|
112
124
|
.join('')
|
|
113
125
|
}
|
|
114
126
|
|
|
127
|
+
function toolResultContent(
|
|
128
|
+
content: string | null | undefined | Array<ContentPart>,
|
|
129
|
+
): string | Array<ContentPart> {
|
|
130
|
+
return Array.isArray(content) ? content : getTextContent(content)
|
|
131
|
+
}
|
|
132
|
+
|
|
115
133
|
/**
|
|
116
134
|
* Convert UIMessages or ModelMessages to ModelMessages
|
|
117
135
|
*/
|
|
@@ -140,12 +158,15 @@ export function convertMessagesToModelMessages(
|
|
|
140
158
|
continue
|
|
141
159
|
}
|
|
142
160
|
|
|
143
|
-
const
|
|
161
|
+
const modelMessage = restoreModelMessageCreatedAt(
|
|
162
|
+
restoreToolResultOwnership(msg),
|
|
163
|
+
)
|
|
164
|
+
const role = (modelMessage as { role: string }).role
|
|
144
165
|
|
|
145
166
|
if (
|
|
146
167
|
role === 'tool' &&
|
|
147
|
-
|
|
148
|
-
anchoredToolCallIds.has(
|
|
168
|
+
modelMessage.toolCallId &&
|
|
169
|
+
anchoredToolCallIds.has(modelMessage.toolCallId)
|
|
149
170
|
) {
|
|
150
171
|
continue
|
|
151
172
|
}
|
|
@@ -170,6 +191,11 @@ export function convertMessagesToModelMessages(
|
|
|
170
191
|
modelMessages.push({
|
|
171
192
|
role: 'system' as ModelMessage['role'],
|
|
172
193
|
content: (msg as { content: string }).content,
|
|
194
|
+
...optionalName(modelMessage),
|
|
195
|
+
...optionalCreatedAt(modelMessage),
|
|
196
|
+
...(modelMessage.metadata !== undefined && {
|
|
197
|
+
metadata: modelMessage.metadata,
|
|
198
|
+
}),
|
|
173
199
|
})
|
|
174
200
|
continue
|
|
175
201
|
}
|
|
@@ -187,7 +213,7 @@ export function convertMessagesToModelMessages(
|
|
|
187
213
|
part.type === 'text' && 'text' in part && !('content' in part),
|
|
188
214
|
)
|
|
189
215
|
) {
|
|
190
|
-
modelMessages.push(
|
|
216
|
+
modelMessages.push(modelMessage)
|
|
191
217
|
continue
|
|
192
218
|
}
|
|
193
219
|
const parts = aguiUserContentToParts(
|
|
@@ -200,12 +226,17 @@ export function convertMessagesToModelMessages(
|
|
|
200
226
|
...((msg as { id?: string }).id !== undefined && {
|
|
201
227
|
id: (msg as { id: string }).id,
|
|
202
228
|
}),
|
|
229
|
+
...optionalName(modelMessage),
|
|
230
|
+
...optionalCreatedAt(modelMessage),
|
|
231
|
+
...(modelMessage.metadata !== undefined && {
|
|
232
|
+
metadata: modelMessage.metadata,
|
|
233
|
+
}),
|
|
203
234
|
})
|
|
204
235
|
continue
|
|
205
236
|
}
|
|
206
237
|
|
|
207
238
|
if (role === 'assistant') {
|
|
208
|
-
const source =
|
|
239
|
+
const source = modelMessage
|
|
209
240
|
const toolCallMetadata = tanstackMetadata(msg)?.toolCallMetadata
|
|
210
241
|
const toolCalls = source.toolCalls?.map((toolCall) =>
|
|
211
242
|
toolCallFromWire(toolCall, toolCallMetadata?.[toolCall.id]),
|
|
@@ -223,11 +254,155 @@ export function convertMessagesToModelMessages(
|
|
|
223
254
|
continue
|
|
224
255
|
}
|
|
225
256
|
|
|
226
|
-
modelMessages.push(
|
|
257
|
+
modelMessages.push(modelMessage)
|
|
227
258
|
}
|
|
228
259
|
return modelMessages
|
|
229
260
|
}
|
|
230
261
|
|
|
262
|
+
function restoreModelMessageCreatedAt(message: ModelMessage): ModelMessage {
|
|
263
|
+
const createdAt =
|
|
264
|
+
coerceCreatedAt(message.createdAt) ?? createdAtFromMetadata(message)
|
|
265
|
+
if (createdAt === undefined) {
|
|
266
|
+
if (message.createdAt === undefined) return message
|
|
267
|
+
const { createdAt: _invalid, ...rest } = message
|
|
268
|
+
return rest
|
|
269
|
+
}
|
|
270
|
+
return Object.is(message.createdAt, createdAt)
|
|
271
|
+
? message
|
|
272
|
+
: { ...message, createdAt }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function restoreToolResultOwnership<T extends object>(message: T): T {
|
|
276
|
+
if (!('role' in message) || message.role !== 'tool') return message
|
|
277
|
+
const source = message
|
|
278
|
+
const metadata = tanstackMetadata(source)
|
|
279
|
+
const owned = metadata?.toolResult
|
|
280
|
+
if (!isRecord(owned)) return message
|
|
281
|
+
if ('content' in owned && !isContentPartArray(owned.content)) return message
|
|
282
|
+
const next = { ...message }
|
|
283
|
+
if (!('id' in owned)) Reflect.deleteProperty(next, 'id')
|
|
284
|
+
if (!('createdAt' in owned)) Reflect.deleteProperty(next, 'createdAt')
|
|
285
|
+
if (typeof owned.id === 'string') Reflect.set(next, 'id', owned.id)
|
|
286
|
+
if (typeof owned.createdAt === 'string') {
|
|
287
|
+
const createdAt = coerceCreatedAt(owned.createdAt)
|
|
288
|
+
if (createdAt) Reflect.set(next, 'createdAt', createdAt)
|
|
289
|
+
}
|
|
290
|
+
if (isContentPartArray(owned.content)) {
|
|
291
|
+
Reflect.set(next, 'content', owned.content)
|
|
292
|
+
}
|
|
293
|
+
const sourceMetadata: unknown = Reflect.get(source, 'metadata')
|
|
294
|
+
if (isRecord(sourceMetadata)) {
|
|
295
|
+
const tanstack = sourceMetadata.tanstack
|
|
296
|
+
if (isRecord(tanstack)) {
|
|
297
|
+
const { toolResult: _toolResult, ...restTanstack } = tanstack
|
|
298
|
+
const restMetadata = { ...sourceMetadata }
|
|
299
|
+
if (Object.keys(restTanstack).length) restMetadata.tanstack = restTanstack
|
|
300
|
+
else delete restMetadata.tanstack
|
|
301
|
+
if (Object.keys(restMetadata).length)
|
|
302
|
+
Reflect.set(next, 'metadata', restMetadata)
|
|
303
|
+
else Reflect.deleteProperty(next, 'metadata')
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return next
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Rebuild a `Date` from a live `Date` or from an ISO string.
|
|
311
|
+
* `JSON.stringify` turns `Date` into a string, so persistence reload
|
|
312
|
+
* and AG-UI wire both land here as strings.
|
|
313
|
+
*/
|
|
314
|
+
export function coerceCreatedAt(value: unknown): Date | undefined {
|
|
315
|
+
if (value instanceof Date) {
|
|
316
|
+
return Number.isNaN(value.getTime()) ? undefined : value
|
|
317
|
+
}
|
|
318
|
+
if (typeof value !== 'string') return undefined
|
|
319
|
+
const createdAt = new Date(value)
|
|
320
|
+
return Number.isNaN(createdAt.getTime()) ? undefined : createdAt
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function normalizeMessagePart(part: MessagePart): MessagePart {
|
|
324
|
+
if (part.type !== 'tool-result') return part
|
|
325
|
+
const createdAt = coerceCreatedAt(part.createdAt)
|
|
326
|
+
const { createdAt: _createdAt, ...rest } = part
|
|
327
|
+
return createdAt === undefined ? rest : { ...rest, createdAt }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function createdAtFromMetadata(source: object): Date | undefined {
|
|
331
|
+
return coerceCreatedAt(tanstackMetadata(source)?.createdAt)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function optionalCreatedAt(source: { createdAt?: unknown }): {
|
|
335
|
+
createdAt?: Date
|
|
336
|
+
} {
|
|
337
|
+
const createdAt = coerceCreatedAt(source.createdAt)
|
|
338
|
+
return createdAt !== undefined ? { createdAt } : {}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function optionalName(source: { name?: string }): { name?: string } {
|
|
342
|
+
return source.name !== undefined ? { name: source.name } : {}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function isUiResourcePart(value: unknown): value is UIResourcePart {
|
|
346
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
347
|
+
return false
|
|
348
|
+
}
|
|
349
|
+
if (!('type' in value) || value.type !== 'ui-resource') return false
|
|
350
|
+
if (!('toolCallId' in value) || typeof value.toolCallId !== 'string') {
|
|
351
|
+
return false
|
|
352
|
+
}
|
|
353
|
+
if (!('toolName' in value) || typeof value.toolName !== 'string') {
|
|
354
|
+
return false
|
|
355
|
+
}
|
|
356
|
+
if (
|
|
357
|
+
!('resource' in value) ||
|
|
358
|
+
value.resource == null ||
|
|
359
|
+
typeof value.resource !== 'object' ||
|
|
360
|
+
Array.isArray(value.resource)
|
|
361
|
+
) {
|
|
362
|
+
return false
|
|
363
|
+
}
|
|
364
|
+
const resource = value.resource
|
|
365
|
+
return (
|
|
366
|
+
'uri' in resource &&
|
|
367
|
+
typeof resource.uri === 'string' &&
|
|
368
|
+
'mimeType' in resource &&
|
|
369
|
+
typeof resource.mimeType === 'string'
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function uiResourceKey(part: UIResourcePart): string {
|
|
374
|
+
return `${part.toolCallId}\0${part.toolName}\0${part.resource.uri}`
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function appendUiResources(
|
|
378
|
+
ui: UIMessage,
|
|
379
|
+
resources: ReadonlyArray<UIResourcePart>,
|
|
380
|
+
): UIMessage {
|
|
381
|
+
if (resources.length === 0) return ui
|
|
382
|
+
const seen = new Set(
|
|
383
|
+
ui.parts.filter(isUiResourcePart).map((part) => uiResourceKey(part)),
|
|
384
|
+
)
|
|
385
|
+
const extra = resources.filter((part) => !seen.has(uiResourceKey(part)))
|
|
386
|
+
if (extra.length === 0) return ui
|
|
387
|
+
return { ...ui, parts: [...ui.parts, ...extra] }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function assistantMetadata(
|
|
391
|
+
uiMessage: UIMessage,
|
|
392
|
+
): UIMessage['metadata'] | undefined {
|
|
393
|
+
const fromParts = uiMessage.parts.filter(isUiResourcePart)
|
|
394
|
+
const current = uiMessage.metadata ?? {}
|
|
395
|
+
const previous = tanstackMetadata(uiMessage)
|
|
396
|
+
const tanstack: TanStackMessageMetadata = {}
|
|
397
|
+
if (previous?.model !== undefined) tanstack.model = previous.model
|
|
398
|
+
if (previous?.signature !== undefined) tanstack.signature = previous.signature
|
|
399
|
+
if (fromParts.length > 0) tanstack.uiResources = fromParts
|
|
400
|
+
const result = { ...current }
|
|
401
|
+
if (Object.keys(tanstack).length > 0) result.tanstack = tanstack
|
|
402
|
+
else delete result.tanstack
|
|
403
|
+
return Object.keys(result).length > 0 ? result : undefined
|
|
404
|
+
}
|
|
405
|
+
|
|
231
406
|
/**
|
|
232
407
|
* Convert a UIMessage to ModelMessage(s)
|
|
233
408
|
*
|
|
@@ -281,9 +456,9 @@ function buildUserOrToolMessage(uiMessage: UIMessage): ModelMessage {
|
|
|
281
456
|
id: uiMessage.id,
|
|
282
457
|
role: uiMessage.role as 'user' | 'assistant' | 'tool',
|
|
283
458
|
content: collapseContentParts(contentParts),
|
|
284
|
-
...(uiMessage
|
|
285
|
-
|
|
286
|
-
}),
|
|
459
|
+
...optionalName(uiMessage),
|
|
460
|
+
...optionalCreatedAt(uiMessage),
|
|
461
|
+
...(uiMessage.metadata !== undefined && { metadata: uiMessage.metadata }),
|
|
287
462
|
}
|
|
288
463
|
}
|
|
289
464
|
|
|
@@ -337,16 +512,26 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
|
|
|
337
512
|
// A tool call can have BOTH an explicit tool-result part AND an output
|
|
338
513
|
// field on the tool-call part. We only want one per tool call ID.
|
|
339
514
|
const emittedToolResultIds = new Set<string>()
|
|
515
|
+
const identityFields = {
|
|
516
|
+
id: uiMessage.id,
|
|
517
|
+
...optionalName(uiMessage),
|
|
518
|
+
...optionalCreatedAt(uiMessage),
|
|
519
|
+
}
|
|
520
|
+
const metadata = assistantMetadata(uiMessage)
|
|
521
|
+
const assistantFields = {
|
|
522
|
+
...identityFields,
|
|
523
|
+
...(metadata !== undefined && { metadata }),
|
|
524
|
+
}
|
|
340
525
|
|
|
341
|
-
function flushSegment(): void {
|
|
526
|
+
function flushSegment(force = false): void {
|
|
342
527
|
const content = collapseContentParts(current.contentParts)
|
|
343
528
|
const hasContent = content !== null
|
|
344
529
|
const hasToolCalls = current.toolCalls.length > 0
|
|
345
530
|
const hasThinking = pendingThinking.length > 0
|
|
346
531
|
|
|
347
|
-
if (hasContent || hasToolCalls || hasThinking) {
|
|
532
|
+
if (force || hasContent || hasToolCalls || hasThinking) {
|
|
348
533
|
messageList.push({
|
|
349
|
-
|
|
534
|
+
...assistantFields,
|
|
350
535
|
role: 'assistant',
|
|
351
536
|
content,
|
|
352
537
|
...(hasToolCalls && { toolCalls: current.toolCalls }),
|
|
@@ -354,9 +539,6 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
|
|
|
354
539
|
...(current.structuredOutput && {
|
|
355
540
|
structuredOutput: current.structuredOutput,
|
|
356
541
|
}),
|
|
357
|
-
...(uiMessage.createdAt !== undefined && {
|
|
358
|
-
createdAt: uiMessage.createdAt,
|
|
359
|
-
}),
|
|
360
542
|
})
|
|
361
543
|
pendingThinking = []
|
|
362
544
|
}
|
|
@@ -389,7 +571,7 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
|
|
|
389
571
|
|
|
390
572
|
case 'tool-result':
|
|
391
573
|
// Flush the current assistant segment before emitting the tool result
|
|
392
|
-
flushSegment()
|
|
574
|
+
flushSegment(messageList.length === 0)
|
|
393
575
|
|
|
394
576
|
// Emit the tool result
|
|
395
577
|
if (
|
|
@@ -397,13 +579,14 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
|
|
|
397
579
|
!emittedToolResultIds.has(part.toolCallId)
|
|
398
580
|
) {
|
|
399
581
|
messageList.push({
|
|
400
|
-
id:
|
|
582
|
+
...(part.id !== undefined && { id: part.id }),
|
|
583
|
+
...optionalCreatedAt(part),
|
|
401
584
|
role: 'tool',
|
|
402
585
|
content: part.content,
|
|
403
586
|
toolCallId: part.toolCallId,
|
|
404
|
-
...(
|
|
405
|
-
|
|
406
|
-
}),
|
|
587
|
+
...(part.name !== undefined && { name: part.name }),
|
|
588
|
+
...(part.metadata !== undefined && { metadata: part.metadata }),
|
|
589
|
+
...(part.error !== undefined && { error: part.error }),
|
|
407
590
|
})
|
|
408
591
|
emittedToolResultIds.add(part.toolCallId)
|
|
409
592
|
}
|
|
@@ -466,13 +649,9 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
|
|
|
466
649
|
// emit the concrete output regardless of approval metadata.
|
|
467
650
|
if (part.output !== undefined && !emittedToolResultIds.has(part.id)) {
|
|
468
651
|
messageList.push({
|
|
469
|
-
id: uiMessage.id,
|
|
470
652
|
role: 'tool',
|
|
471
653
|
content: normalizeToolResult(part.output),
|
|
472
654
|
toolCallId: part.id,
|
|
473
|
-
...(uiMessage.createdAt !== undefined && {
|
|
474
|
-
createdAt: uiMessage.createdAt,
|
|
475
|
-
}),
|
|
476
655
|
})
|
|
477
656
|
emittedToolResultIds.add(part.id)
|
|
478
657
|
}
|
|
@@ -486,7 +665,6 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
|
|
|
486
665
|
) {
|
|
487
666
|
const approved = part.approval.approved
|
|
488
667
|
messageList.push({
|
|
489
|
-
id: uiMessage.id,
|
|
490
668
|
role: 'tool',
|
|
491
669
|
content: JSON.stringify({
|
|
492
670
|
approved,
|
|
@@ -496,9 +674,6 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
|
|
|
496
674
|
: 'User denied this action',
|
|
497
675
|
}),
|
|
498
676
|
toolCallId: part.id,
|
|
499
|
-
...(uiMessage.createdAt !== undefined && {
|
|
500
|
-
createdAt: uiMessage.createdAt,
|
|
501
|
-
}),
|
|
502
677
|
})
|
|
503
678
|
emittedToolResultIds.add(part.id)
|
|
504
679
|
}
|
|
@@ -507,12 +682,9 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
|
|
|
507
682
|
// If no messages were produced (e.g., empty parts), emit a minimal assistant message
|
|
508
683
|
if (messageList.length === 0) {
|
|
509
684
|
messageList.push({
|
|
510
|
-
|
|
685
|
+
...assistantFields,
|
|
511
686
|
role: 'assistant',
|
|
512
687
|
content: null,
|
|
513
|
-
...(uiMessage.createdAt !== undefined && {
|
|
514
|
-
createdAt: uiMessage.createdAt,
|
|
515
|
-
}),
|
|
516
688
|
})
|
|
517
689
|
}
|
|
518
690
|
|
|
@@ -536,6 +708,7 @@ export function modelMessageToUIMessage(
|
|
|
536
708
|
id?: string,
|
|
537
709
|
): UIMessage {
|
|
538
710
|
const parts: Array<MessagePart> = []
|
|
711
|
+
const createdAt = coerceCreatedAt(modelMessage.createdAt)
|
|
539
712
|
|
|
540
713
|
if (modelMessage.role === 'assistant' && modelMessage.thinking?.length) {
|
|
541
714
|
for (const thinking of modelMessage.thinking) {
|
|
@@ -550,14 +723,36 @@ export function modelMessageToUIMessage(
|
|
|
550
723
|
|
|
551
724
|
// Handle tool results (when role is "tool") - only produce tool-result part,
|
|
552
725
|
// not a text part (the content IS the tool result, not display text)
|
|
553
|
-
|
|
554
|
-
|
|
726
|
+
const structuredOutput =
|
|
727
|
+
modelMessage.structuredOutput ??
|
|
728
|
+
snapshotStructuredOutput(tanstackMetadata(modelMessage)?.structuredOutput)
|
|
729
|
+
if (modelMessage.role === 'assistant' && structuredOutput) {
|
|
730
|
+
if (
|
|
731
|
+
typeof modelMessage.content === 'string' &&
|
|
732
|
+
modelMessage.content &&
|
|
733
|
+
modelMessage.content !== structuredOutput.raw
|
|
734
|
+
) {
|
|
735
|
+
const suffix = structuredOutput.raw
|
|
736
|
+
const text =
|
|
737
|
+
suffix !== '' && modelMessage.content.endsWith(suffix)
|
|
738
|
+
? modelMessage.content.slice(0, -suffix.length)
|
|
739
|
+
: modelMessage.content
|
|
740
|
+
if (text) parts.push({ type: 'text', content: text })
|
|
741
|
+
}
|
|
742
|
+
parts.push(structuredOutput)
|
|
555
743
|
} else if (modelMessage.role === 'tool' && modelMessage.toolCallId) {
|
|
556
744
|
parts.push({
|
|
557
745
|
type: 'tool-result',
|
|
558
746
|
toolCallId: modelMessage.toolCallId,
|
|
559
|
-
content:
|
|
560
|
-
state: 'complete',
|
|
747
|
+
content: toolResultContent(modelMessage.content),
|
|
748
|
+
state: modelMessage.error === undefined ? 'complete' : 'error',
|
|
749
|
+
...(modelMessage.id !== undefined && { id: modelMessage.id }),
|
|
750
|
+
...(modelMessage.name !== undefined && { name: modelMessage.name }),
|
|
751
|
+
...(modelMessage.metadata !== undefined && {
|
|
752
|
+
metadata: modelMessage.metadata,
|
|
753
|
+
}),
|
|
754
|
+
...(createdAt !== undefined && { createdAt }),
|
|
755
|
+
...(modelMessage.error !== undefined && { error: modelMessage.error }),
|
|
561
756
|
})
|
|
562
757
|
} else if (Array.isArray(modelMessage.content)) {
|
|
563
758
|
// Multimodal content - preserve all content parts as MessageParts
|
|
@@ -598,14 +793,23 @@ export function modelMessageToUIMessage(
|
|
|
598
793
|
}
|
|
599
794
|
}
|
|
600
795
|
|
|
601
|
-
|
|
796
|
+
const ui: UIMessage = {
|
|
602
797
|
id: id || generateMessageId(),
|
|
603
798
|
role: modelMessage.role === 'tool' ? 'assistant' : modelMessage.role,
|
|
604
799
|
parts,
|
|
605
|
-
...(modelMessage
|
|
606
|
-
|
|
800
|
+
...optionalName(modelMessage),
|
|
801
|
+
...(createdAt !== undefined && { createdAt }),
|
|
802
|
+
...(modelMessage.metadata !== undefined && {
|
|
803
|
+
metadata: modelMessage.metadata,
|
|
607
804
|
}),
|
|
608
805
|
}
|
|
806
|
+
const storedResources = tanstackMetadata(modelMessage)?.uiResources
|
|
807
|
+
return appendUiResources(
|
|
808
|
+
ui,
|
|
809
|
+
Array.isArray(storedResources)
|
|
810
|
+
? storedResources.filter(isUiResourcePart)
|
|
811
|
+
: [],
|
|
812
|
+
)
|
|
609
813
|
}
|
|
610
814
|
|
|
611
815
|
/**
|
|
@@ -644,13 +848,19 @@ export function aguiSnapshotMessageToUIMessage(
|
|
|
644
848
|
parts: aguiUserContentToParts(message.content),
|
|
645
849
|
})
|
|
646
850
|
case 'assistant': {
|
|
647
|
-
const
|
|
851
|
+
const metadata = tanstackMetadata(message)
|
|
852
|
+
const toolCallMetadata = metadata?.toolCallMetadata
|
|
853
|
+
const structuredOutput = snapshotStructuredOutput(
|
|
854
|
+
metadata?.structuredOutput,
|
|
855
|
+
)
|
|
648
856
|
const toolCalls = message.toolCalls?.map((toolCall) => {
|
|
649
|
-
const
|
|
857
|
+
const callMetadata =
|
|
650
858
|
toolCallMetadata != null && typeof toolCallMetadata === 'object'
|
|
651
859
|
? (toolCallMetadata as Record<string, unknown>)[toolCall.id]
|
|
652
860
|
: undefined
|
|
653
|
-
return
|
|
861
|
+
return callMetadata !== undefined
|
|
862
|
+
? { ...toolCall, metadata: callMetadata }
|
|
863
|
+
: toolCall
|
|
654
864
|
})
|
|
655
865
|
return applySnapshotMetadata(
|
|
656
866
|
message,
|
|
@@ -658,13 +868,20 @@ export function aguiSnapshotMessageToUIMessage(
|
|
|
658
868
|
{
|
|
659
869
|
role: 'assistant',
|
|
660
870
|
content: message.content ?? null,
|
|
871
|
+
...optionalName(message),
|
|
661
872
|
...(toolCalls && { toolCalls }),
|
|
873
|
+
...(structuredOutput && { structuredOutput }),
|
|
662
874
|
},
|
|
663
875
|
id,
|
|
664
876
|
),
|
|
665
877
|
)
|
|
666
878
|
}
|
|
667
|
-
case 'tool':
|
|
879
|
+
case 'tool': {
|
|
880
|
+
message = restoreToolResultOwnership(message)
|
|
881
|
+
const createdAt =
|
|
882
|
+
coerceCreatedAt(
|
|
883
|
+
'createdAt' in message ? message.createdAt : undefined,
|
|
884
|
+
) ?? createdAtFromMetadata(message)
|
|
668
885
|
return applySnapshotMetadata(
|
|
669
886
|
message,
|
|
670
887
|
modelMessageToUIMessage(
|
|
@@ -672,10 +889,22 @@ export function aguiSnapshotMessageToUIMessage(
|
|
|
672
889
|
role: 'tool',
|
|
673
890
|
content: message.content,
|
|
674
891
|
toolCallId: message.toolCallId,
|
|
892
|
+
...('name' in message && typeof message.name === 'string'
|
|
893
|
+
? { name: message.name }
|
|
894
|
+
: {}),
|
|
895
|
+
...('id' in message && typeof message.id === 'string'
|
|
896
|
+
? { id: message.id }
|
|
897
|
+
: {}),
|
|
898
|
+
...('metadata' in message && message.metadata != null
|
|
899
|
+
? { metadata: message.metadata }
|
|
900
|
+
: {}),
|
|
901
|
+
...(createdAt !== undefined ? { createdAt } : {}),
|
|
902
|
+
...(message.error !== undefined && { error: message.error }),
|
|
675
903
|
},
|
|
676
904
|
id,
|
|
677
905
|
),
|
|
678
906
|
)
|
|
907
|
+
}
|
|
679
908
|
case 'system':
|
|
680
909
|
case 'developer':
|
|
681
910
|
// `ModelMessage` has no system/developer role; build the part directly.
|
|
@@ -715,19 +944,62 @@ export function aguiSnapshotMessageToUIMessage(
|
|
|
715
944
|
|
|
716
945
|
/** Copy snapshot metadata when it is a record. Rebuild createdAt from tanstack.createdAt. */
|
|
717
946
|
function applySnapshotMetadata(source: object, ui: UIMessage): UIMessage {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
947
|
+
const normalizedParts = ui.parts.map((part) => normalizeMessagePart(part))
|
|
948
|
+
if (normalizedParts !== ui.parts) ui = { ...ui, parts: normalizedParts }
|
|
949
|
+
const name =
|
|
950
|
+
'name' in source && typeof source.name === 'string'
|
|
951
|
+
? source.name
|
|
952
|
+
: undefined
|
|
953
|
+
let next = name !== undefined ? { ...ui, name } : ui
|
|
954
|
+
|
|
955
|
+
let metadata: NonNullable<UIMessage['metadata']> | undefined
|
|
956
|
+
if ('metadata' in source) {
|
|
957
|
+
const raw = source.metadata
|
|
958
|
+
if (raw != null && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
959
|
+
metadata = raw as NonNullable<UIMessage['metadata']>
|
|
960
|
+
next = { ...next, metadata }
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
723
964
|
const createdAt =
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
965
|
+
coerceCreatedAt(next.createdAt) ??
|
|
966
|
+
(metadata !== undefined ? createdAtFromMetadata(metadata) : undefined)
|
|
967
|
+
if (createdAt !== undefined && !Object.is(createdAt, next.createdAt)) {
|
|
968
|
+
next = { ...next, createdAt }
|
|
969
|
+
} else if (createdAt === undefined && next.createdAt !== undefined) {
|
|
970
|
+
const { createdAt: _invalid, ...rest } = next
|
|
971
|
+
next = rest
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const uiResources = tanstackMetadata(metadata)?.uiResources
|
|
975
|
+
const resources = Array.isArray(uiResources)
|
|
976
|
+
? uiResources.filter(isUiResourcePart)
|
|
977
|
+
: []
|
|
978
|
+
return appendUiResources(next, resources)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function snapshotStructuredOutput(
|
|
982
|
+
value: TanStackMessageMetadata['structuredOutput'],
|
|
983
|
+
): StructuredOutputPart | undefined {
|
|
984
|
+
if (
|
|
985
|
+
value == null ||
|
|
986
|
+
(value.status !== 'streaming' &&
|
|
987
|
+
value.status !== 'complete' &&
|
|
988
|
+
value.status !== 'error') ||
|
|
989
|
+
typeof value.raw !== 'string'
|
|
990
|
+
) {
|
|
991
|
+
return undefined
|
|
992
|
+
}
|
|
727
993
|
return {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
994
|
+
type: 'structured-output',
|
|
995
|
+
status: value.status,
|
|
996
|
+
raw: value.raw,
|
|
997
|
+
...(value.partial !== undefined ? { partial: value.partial } : {}),
|
|
998
|
+
...(value.data !== undefined ? { data: value.data } : {}),
|
|
999
|
+
...(value.reasoning ? { reasoning: value.reasoning } : {}),
|
|
1000
|
+
...(value.errorMessage !== undefined
|
|
1001
|
+
? { errorMessage: value.errorMessage }
|
|
1002
|
+
: {}),
|
|
731
1003
|
}
|
|
732
1004
|
}
|
|
733
1005
|
|
|
@@ -780,22 +1052,31 @@ export function modelMessagesToUIMessages(
|
|
|
780
1052
|
currentAssistantMessage &&
|
|
781
1053
|
currentAssistantMessage.role === 'assistant'
|
|
782
1054
|
) {
|
|
783
|
-
const content =
|
|
1055
|
+
const content = toolResultContent(msg.content)
|
|
784
1056
|
const toolCallPart = currentAssistantMessage.parts.find(
|
|
785
1057
|
(part): part is ToolCallPart =>
|
|
786
1058
|
part.type === 'tool-call' && part.id === msg.toolCallId,
|
|
787
1059
|
)
|
|
788
1060
|
|
|
789
1061
|
if (toolCallPart) {
|
|
790
|
-
toolCallPart.output =
|
|
791
|
-
|
|
1062
|
+
toolCallPart.output =
|
|
1063
|
+
typeof content === 'string'
|
|
1064
|
+
? parseToolResultContent(content)
|
|
1065
|
+
: content
|
|
1066
|
+
toolCallPart.state = msg.error === undefined ? 'complete' : 'error'
|
|
792
1067
|
}
|
|
793
1068
|
|
|
794
1069
|
currentAssistantMessage.parts.push({
|
|
795
1070
|
type: 'tool-result',
|
|
796
1071
|
toolCallId: msg.toolCallId,
|
|
797
1072
|
content,
|
|
798
|
-
state: 'complete',
|
|
1073
|
+
state: msg.error === undefined ? 'complete' : 'error',
|
|
1074
|
+
...(msg.id !== undefined &&
|
|
1075
|
+
msg.id !== currentAssistantMessage.id && { id: msg.id }),
|
|
1076
|
+
...(msg.name !== undefined && { name: msg.name }),
|
|
1077
|
+
...(msg.metadata !== undefined && { metadata: msg.metadata }),
|
|
1078
|
+
...optionalCreatedAt(msg),
|
|
1079
|
+
...(msg.error !== undefined && { error: msg.error }),
|
|
799
1080
|
})
|
|
800
1081
|
} else {
|
|
801
1082
|
// No assistant message to merge into, create a standalone one
|
|
@@ -834,16 +1115,18 @@ export function normalizeToUIMessage(
|
|
|
834
1115
|
): UIMessage {
|
|
835
1116
|
if ('parts' in message) {
|
|
836
1117
|
// Already a UIMessage
|
|
1118
|
+
const parts = message.parts.map((part) => normalizeMessagePart(part))
|
|
837
1119
|
return {
|
|
838
1120
|
...message,
|
|
1121
|
+
parts,
|
|
839
1122
|
id: message.id || generateId(),
|
|
840
|
-
createdAt: message.createdAt
|
|
1123
|
+
createdAt: coerceCreatedAt(message.createdAt) ?? new Date(),
|
|
841
1124
|
}
|
|
842
1125
|
} else {
|
|
843
1126
|
// ModelMessage - convert to UIMessage
|
|
844
1127
|
return {
|
|
845
1128
|
...modelMessageToUIMessage(message, generateId()),
|
|
846
|
-
createdAt: message.createdAt ?? new Date(),
|
|
1129
|
+
createdAt: coerceCreatedAt(message.createdAt) ?? new Date(),
|
|
847
1130
|
}
|
|
848
1131
|
}
|
|
849
1132
|
}
|