@tanstack/ai 0.49.1 → 0.50.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.js +7 -7
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/client.js +1 -1
- package/dist/esm/index.js +2 -2
- 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/package.json +2 -2
- package/src/activities/chat/index.ts +3 -4
- package/src/activities/chat/messages.ts +344 -61
- package/src/activities/chat/stream/processor.ts +19 -10
- package/src/types.ts +18 -0
- package/src/utilities/ag-ui-wire.ts +278 -73
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import {
|
|
21
21
|
aguiSnapshotMessageToUIMessage,
|
|
22
|
+
coerceCreatedAt,
|
|
22
23
|
generateMessageId,
|
|
23
24
|
uiMessageToModelMessages,
|
|
24
25
|
} from '../messages.js'
|
|
@@ -835,11 +836,10 @@ export class StreamProcessor {
|
|
|
835
836
|
}
|
|
836
837
|
: incomingRecord
|
|
837
838
|
const metadata = mergeMetadata(message.metadata, toMerge)
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
const createdAtValid =
|
|
842
|
-
createdAt !== undefined && !Number.isNaN(createdAt.getTime())
|
|
839
|
+
const createdAt = coerceCreatedAt(
|
|
840
|
+
tanstackMetadata(incomingRecord)?.createdAt,
|
|
841
|
+
)
|
|
842
|
+
const createdAtValid = createdAt !== undefined
|
|
843
843
|
this.messages = this.messages.map((msg) =>
|
|
844
844
|
msg.id === messageId
|
|
845
845
|
? {
|
|
@@ -1050,8 +1050,10 @@ export class StreamProcessor {
|
|
|
1050
1050
|
msg.parts.every((part) => part.type === 'thinking')
|
|
1051
1051
|
const isToolResultOnly = (msg: UIMessage) =>
|
|
1052
1052
|
msg.role === 'assistant' &&
|
|
1053
|
-
msg.parts.
|
|
1054
|
-
msg.parts
|
|
1053
|
+
msg.parts.some((part) => part.type === 'tool-result') &&
|
|
1054
|
+
msg.parts.every(
|
|
1055
|
+
(part) => part.type === 'tool-result' || part.type === 'ui-resource',
|
|
1056
|
+
)
|
|
1055
1057
|
const flushPending = () => {
|
|
1056
1058
|
out.push(...pending)
|
|
1057
1059
|
pending = []
|
|
@@ -1109,9 +1111,16 @@ export class StreamProcessor {
|
|
|
1109
1111
|
|
|
1110
1112
|
const reconciled: Array<UIMessage> = []
|
|
1111
1113
|
for (const msg of snapshot) {
|
|
1114
|
+
const toolResultParts = msg.parts.filter(
|
|
1115
|
+
(part): part is ToolResultPart => part.type === 'tool-result',
|
|
1116
|
+
)
|
|
1112
1117
|
const toolResultPart =
|
|
1113
|
-
msg.role === 'assistant' &&
|
|
1114
|
-
|
|
1118
|
+
msg.role === 'assistant' &&
|
|
1119
|
+
toolResultParts.length === 1 &&
|
|
1120
|
+
msg.parts.every(
|
|
1121
|
+
(part) => part.type === 'tool-result' || part.type === 'ui-resource',
|
|
1122
|
+
)
|
|
1123
|
+
? toolResultParts[0]
|
|
1115
1124
|
: undefined
|
|
1116
1125
|
|
|
1117
1126
|
if (!toolResultPart) {
|
|
@@ -1170,7 +1179,7 @@ export class StreamProcessor {
|
|
|
1170
1179
|
)
|
|
1171
1180
|
}
|
|
1172
1181
|
}
|
|
1173
|
-
parts.push(
|
|
1182
|
+
parts.push(...msg.parts)
|
|
1174
1183
|
// Replace rather than push into `target.parts`: a snapshot message that
|
|
1175
1184
|
// arrived already carrying `parts` (TanStack server echoing UIMessages)
|
|
1176
1185
|
// shares its array with the incoming chunk, and mutating it in place
|
package/src/types.ts
CHANGED
|
@@ -372,6 +372,10 @@ export interface ModelMessage<
|
|
|
372
372
|
toolCalls?: Array<ToolCall>
|
|
373
373
|
toolCallId?: string
|
|
374
374
|
thinking?: Array<{ content: string; signature?: string }>
|
|
375
|
+
/** Error reported by an AG-UI tool message. */
|
|
376
|
+
error?: string
|
|
377
|
+
/** Optional AG-UI message metadata. TanStack-owned fields live under `tanstack`. */
|
|
378
|
+
metadata?: Record<string, any>
|
|
375
379
|
/**
|
|
376
380
|
* Completed structured output represented by this assistant message.
|
|
377
381
|
* `content` remains the provider-facing JSON text; this field preserves the
|
|
@@ -435,10 +439,14 @@ export interface ToolCallPart<TMetadata = unknown> {
|
|
|
435
439
|
|
|
436
440
|
export interface ToolResultPart {
|
|
437
441
|
type: 'tool-result'
|
|
442
|
+
id?: string
|
|
443
|
+
name?: string
|
|
438
444
|
toolCallId: string
|
|
439
445
|
content: string | Array<ContentPart>
|
|
440
446
|
state: ToolResultState
|
|
441
447
|
error?: string // Error message if state is "error"
|
|
448
|
+
metadata?: Record<string, unknown>
|
|
449
|
+
createdAt?: Date
|
|
442
450
|
}
|
|
443
451
|
|
|
444
452
|
export interface ThinkingPart {
|
|
@@ -524,9 +532,17 @@ export interface TanStackMessageMetadata {
|
|
|
524
532
|
signature?: string
|
|
525
533
|
/** Per-tool-call provider metadata keyed by tool call id (e.g. Gemini thoughtSignature). */
|
|
526
534
|
toolCallMetadata?: Record<string, unknown>
|
|
535
|
+
toolResult?: {
|
|
536
|
+
id?: string
|
|
537
|
+
createdAt?: string
|
|
538
|
+
content?: Array<ContentPart>
|
|
539
|
+
}
|
|
527
540
|
structuredOutput?: {
|
|
528
541
|
status?: 'streaming' | 'complete' | 'error'
|
|
542
|
+
partial?: unknown
|
|
543
|
+
data?: unknown
|
|
529
544
|
raw?: string
|
|
545
|
+
reasoning?: string
|
|
530
546
|
errorMessage?: string
|
|
531
547
|
}
|
|
532
548
|
uiResources?: Array<UIResourcePart>
|
|
@@ -562,6 +578,8 @@ export interface UIMessage<TData = unknown> {
|
|
|
562
578
|
role: 'system' | 'user' | 'assistant'
|
|
563
579
|
parts: Array<MessagePart<TData>>
|
|
564
580
|
createdAt?: Date
|
|
581
|
+
/** Optional AG-UI sender name. Converters preserve it across wire and persist. */
|
|
582
|
+
name?: string
|
|
565
583
|
/**
|
|
566
584
|
* Optional AG-UI metadata bag. TanStack writes the `tanstack` key.
|
|
567
585
|
* User keys stay at the top.
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AssistantMessage,
|
|
3
|
+
InputContent,
|
|
4
|
+
ReasoningMessage,
|
|
5
|
+
SystemMessage,
|
|
6
|
+
ToolCall,
|
|
7
|
+
ToolMessage,
|
|
8
|
+
UserMessage,
|
|
9
|
+
} from '@ag-ui/core'
|
|
1
10
|
import type {
|
|
2
11
|
ContentPart,
|
|
3
12
|
MessagePart,
|
|
13
|
+
ModelMessage,
|
|
4
14
|
StructuredOutputPart,
|
|
5
15
|
TanStackMessageMetadata,
|
|
6
16
|
UIMessage,
|
|
@@ -8,72 +18,125 @@ import type {
|
|
|
8
18
|
} from '../types'
|
|
9
19
|
import type { MetadataRecord } from './merge-metadata'
|
|
10
20
|
import { tanstackMetadata } from './merge-metadata'
|
|
21
|
+
import { normalizeToolResult } from './tool-result'
|
|
22
|
+
import {
|
|
23
|
+
coerceCreatedAt,
|
|
24
|
+
modelMessageToUIMessage,
|
|
25
|
+
} from '../activities/chat/messages'
|
|
11
26
|
|
|
12
|
-
type
|
|
13
|
-
type
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
type AGUIToolMessage = {
|
|
25
|
-
role: 'tool'
|
|
26
|
-
id: string
|
|
27
|
-
toolCallId: string
|
|
28
|
-
content: string
|
|
29
|
-
error?: string
|
|
30
|
-
}
|
|
27
|
+
type WithMetadata<T> = T & { metadata?: MetadataRecord }
|
|
28
|
+
type WireSystemMessage = WithMetadata<SystemMessage>
|
|
29
|
+
type WireUserMessage = WithMetadata<UserMessage>
|
|
30
|
+
type WireAssistantMessage = WithMetadata<AssistantMessage>
|
|
31
|
+
type WireToolMessage = WithMetadata<
|
|
32
|
+
ToolMessage & {
|
|
33
|
+
name?: string
|
|
34
|
+
}
|
|
35
|
+
>
|
|
36
|
+
type WireReasoningMessage = WithMetadata<ReasoningMessage>
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
id: string
|
|
35
|
-
content: string
|
|
36
|
-
encryptedValue?: string
|
|
37
|
-
metadata?: MetadataRecord
|
|
38
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
39
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
metadata
|
|
42
|
+
function rebuiltToolMetadata(
|
|
43
|
+
metadata: unknown,
|
|
44
|
+
createdAt: unknown,
|
|
45
|
+
id: string | undefined,
|
|
46
|
+
content: string | null | Array<ContentPart>,
|
|
47
|
+
anchorOwnsUiResources = false,
|
|
48
|
+
): MetadataRecord | undefined {
|
|
49
|
+
const source: MetadataRecord = isRecord(metadata) ? metadata : {}
|
|
50
|
+
const tanstack = isRecord(source.tanstack) ? { ...source.tanstack } : {}
|
|
51
|
+
if (anchorOwnsUiResources) delete tanstack.uiResources
|
|
52
|
+
const date = coerceCreatedAt(createdAt)
|
|
53
|
+
const toolResult: NonNullable<TanStackMessageMetadata['toolResult']> = {
|
|
54
|
+
...(id !== undefined ? { id } : {}),
|
|
55
|
+
...(date && { createdAt: date.toISOString() }),
|
|
56
|
+
...(Array.isArray(content) && { content }),
|
|
57
|
+
}
|
|
58
|
+
const result = {
|
|
59
|
+
...source,
|
|
60
|
+
tanstack: { ...tanstack, toolResult },
|
|
61
|
+
}
|
|
62
|
+
return Object.keys(result).length ? result : undefined
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
export type WireMessage =
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
66
|
+
| WireSystemMessage
|
|
67
|
+
| WireUserMessage
|
|
68
|
+
| WireAssistantMessage
|
|
69
|
+
| WireToolMessage
|
|
70
|
+
| WireReasoningMessage
|
|
54
71
|
|
|
55
72
|
/**
|
|
56
|
-
* Serialize TanStack `UIMessage`s into the AG-UI
|
|
57
|
-
* wire shape. Anchors are spec-only (`id`, `role`,
|
|
58
|
-
* `toolCalls`, `metadata`). Tool results and thinking parts
|
|
59
|
-
* messages are additionally emitted as fan-out
|
|
60
|
-
* `{role:'reasoning',...}` entries for strict AG-UI
|
|
73
|
+
* Serialize TanStack `UIMessage`s and `ModelMessage`s into the AG-UI
|
|
74
|
+
* `RunAgentInput.messages` wire shape. Anchors are spec-only (`id`, `role`,
|
|
75
|
+
* `name`, `content`, `toolCalls`, `metadata`). Tool results and thinking parts
|
|
76
|
+
* on assistant messages are additionally emitted as fan-out
|
|
77
|
+
* `{role:'tool',...}` and `{role:'reasoning',...}` entries for strict AG-UI
|
|
78
|
+
* server consumers. Set `includeSnapshotStructuredOutput` to retain complete
|
|
79
|
+
* structured-output metadata for UI snapshots.
|
|
61
80
|
*/
|
|
62
81
|
export function uiMessagesToWire(
|
|
63
|
-
messages: Array<UIMessage>,
|
|
82
|
+
messages: Array<UIMessage | ModelMessage>,
|
|
83
|
+
options?: { includeSnapshotStructuredOutput: boolean },
|
|
64
84
|
): Array<WireMessage> {
|
|
65
85
|
const wire: Array<WireMessage> = []
|
|
86
|
+
const usedWireIds = new Set<string>(
|
|
87
|
+
messages.flatMap((message) =>
|
|
88
|
+
'id' in message && message.id && message.role !== 'tool'
|
|
89
|
+
? [message.id]
|
|
90
|
+
: [],
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
const includeSnapshotStructuredOutput =
|
|
94
|
+
options?.includeSnapshotStructuredOutput ?? false
|
|
95
|
+
|
|
96
|
+
const assistantIds = new Set<string>()
|
|
97
|
+
for (const msg of messages) {
|
|
98
|
+
if (msg.role === 'assistant' && msg.id !== undefined) {
|
|
99
|
+
assistantIds.add(msg.id)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
66
102
|
|
|
67
103
|
for (const msg of messages) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
104
|
+
if (!('parts' in msg) && msg.role === 'tool' && msg.toolCallId) {
|
|
105
|
+
const id = uniqueToolWireId(
|
|
106
|
+
toolWireId(msg.id, msg.toolCallId, assistantIds),
|
|
107
|
+
usedWireIds,
|
|
108
|
+
)
|
|
109
|
+
const metadata = rebuiltToolMetadata(
|
|
110
|
+
msg.metadata,
|
|
111
|
+
msg.createdAt,
|
|
112
|
+
msg.id,
|
|
113
|
+
msg.content,
|
|
114
|
+
)
|
|
115
|
+
wire.push({
|
|
116
|
+
role: 'tool',
|
|
117
|
+
id,
|
|
118
|
+
...(msg.name !== undefined && { name: msg.name }),
|
|
119
|
+
toolCallId: msg.toolCallId,
|
|
120
|
+
content:
|
|
121
|
+
typeof msg.content === 'string'
|
|
122
|
+
? msg.content
|
|
123
|
+
: JSON.stringify(msg.content),
|
|
124
|
+
...(msg.error !== undefined && { error: msg.error }),
|
|
125
|
+
...(metadata !== undefined && { metadata }),
|
|
126
|
+
})
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const uiMessage: UIMessage =
|
|
131
|
+
'parts' in msg ? msg : modelMessageToUIMessage(msg, msg.id)
|
|
132
|
+
const parts: ReadonlyArray<MessagePart> = uiMessage.parts
|
|
133
|
+
usedWireIds.add(uiMessage.id)
|
|
72
134
|
|
|
73
135
|
if (msg.role === 'system') {
|
|
74
136
|
wire.push(
|
|
75
137
|
toAnchor(
|
|
76
|
-
|
|
138
|
+
uiMessage,
|
|
139
|
+
'system',
|
|
77
140
|
{
|
|
78
141
|
content:
|
|
79
142
|
parts.length > 0
|
|
@@ -81,6 +144,7 @@ export function uiMessagesToWire(
|
|
|
81
144
|
: ((msg as { content?: string }).content ?? ''),
|
|
82
145
|
},
|
|
83
146
|
parts,
|
|
147
|
+
includeSnapshotStructuredOutput,
|
|
84
148
|
),
|
|
85
149
|
)
|
|
86
150
|
continue
|
|
@@ -89,7 +153,8 @@ export function uiMessagesToWire(
|
|
|
89
153
|
if (msg.role === 'user') {
|
|
90
154
|
wire.push(
|
|
91
155
|
toAnchor(
|
|
92
|
-
|
|
156
|
+
uiMessage,
|
|
157
|
+
'user',
|
|
93
158
|
{
|
|
94
159
|
content:
|
|
95
160
|
parts.length > 0
|
|
@@ -97,6 +162,7 @@ export function uiMessagesToWire(
|
|
|
97
162
|
: ((msg as { content?: string }).content ?? ''),
|
|
98
163
|
},
|
|
99
164
|
parts,
|
|
165
|
+
includeSnapshotStructuredOutput,
|
|
100
166
|
),
|
|
101
167
|
)
|
|
102
168
|
continue
|
|
@@ -105,9 +171,9 @@ export function uiMessagesToWire(
|
|
|
105
171
|
// assistant: emit reasoning fan-outs first, then anchor, then tool fan-outs
|
|
106
172
|
for (const part of parts) {
|
|
107
173
|
if (part.type === 'thinking') {
|
|
108
|
-
const reasoning:
|
|
174
|
+
const reasoning: WireReasoningMessage = {
|
|
109
175
|
role: 'reasoning',
|
|
110
|
-
id: deriveReasoningId(
|
|
176
|
+
id: uniqueWireId(deriveReasoningId(uiMessage.id, part), usedWireIds),
|
|
111
177
|
content: part.content,
|
|
112
178
|
}
|
|
113
179
|
if (part.signature) {
|
|
@@ -121,26 +187,79 @@ export function uiMessagesToWire(
|
|
|
121
187
|
const toolCalls = collectToolCalls(parts)
|
|
122
188
|
wire.push(
|
|
123
189
|
toAnchor(
|
|
124
|
-
|
|
190
|
+
uiMessage,
|
|
191
|
+
'assistant',
|
|
125
192
|
{
|
|
126
193
|
...(text !== '' && { content: text }),
|
|
127
194
|
...(toolCalls && { toolCalls }),
|
|
128
195
|
},
|
|
129
196
|
parts,
|
|
197
|
+
includeSnapshotStructuredOutput,
|
|
130
198
|
),
|
|
131
199
|
)
|
|
132
200
|
|
|
201
|
+
const explicitToolResults = new Set(
|
|
202
|
+
parts.flatMap((part) =>
|
|
203
|
+
part.type === 'tool-result' ? [part.toolCallId] : [],
|
|
204
|
+
),
|
|
205
|
+
)
|
|
133
206
|
for (const part of parts) {
|
|
134
207
|
if (part.type === 'tool-result') {
|
|
208
|
+
const id = uniqueToolWireId(
|
|
209
|
+
part.id ?? deriveToolMessageId(part.toolCallId),
|
|
210
|
+
usedWireIds,
|
|
211
|
+
)
|
|
212
|
+
const metadata = rebuiltToolMetadata(
|
|
213
|
+
part.metadata,
|
|
214
|
+
part.createdAt,
|
|
215
|
+
part.id,
|
|
216
|
+
part.content,
|
|
217
|
+
true,
|
|
218
|
+
)
|
|
135
219
|
wire.push({
|
|
136
220
|
role: 'tool',
|
|
137
|
-
id
|
|
221
|
+
id,
|
|
138
222
|
toolCallId: part.toolCallId,
|
|
223
|
+
...(part.name !== undefined && { name: part.name }),
|
|
139
224
|
content:
|
|
140
225
|
typeof part.content === 'string'
|
|
141
226
|
? part.content
|
|
142
227
|
: JSON.stringify(part.content),
|
|
143
228
|
...(part.error !== undefined && { error: part.error }),
|
|
229
|
+
...(metadata !== undefined && { metadata }),
|
|
230
|
+
})
|
|
231
|
+
} else if (part.type === 'tool-call') {
|
|
232
|
+
const approved = part.approval?.approved
|
|
233
|
+
if (
|
|
234
|
+
explicitToolResults.has(part.id) ||
|
|
235
|
+
(part.output === undefined &&
|
|
236
|
+
(part.state !== 'approval-responded' || approved === undefined))
|
|
237
|
+
) {
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
const result =
|
|
241
|
+
part.output !== undefined
|
|
242
|
+
? normalizeToolResult(part.output)
|
|
243
|
+
: JSON.stringify({
|
|
244
|
+
approved,
|
|
245
|
+
...(approved && { pendingExecution: true }),
|
|
246
|
+
message: approved
|
|
247
|
+
? 'User approved this action'
|
|
248
|
+
: 'User denied this action',
|
|
249
|
+
})
|
|
250
|
+
const content =
|
|
251
|
+
typeof result === 'string' ? result : JSON.stringify(result)
|
|
252
|
+
wire.push({
|
|
253
|
+
role: 'tool',
|
|
254
|
+
id: uniqueToolWireId(deriveToolMessageId(part.id), usedWireIds),
|
|
255
|
+
toolCallId: part.id,
|
|
256
|
+
content,
|
|
257
|
+
metadata: rebuiltToolMetadata(
|
|
258
|
+
undefined,
|
|
259
|
+
undefined,
|
|
260
|
+
undefined,
|
|
261
|
+
result,
|
|
262
|
+
),
|
|
144
263
|
})
|
|
145
264
|
}
|
|
146
265
|
}
|
|
@@ -151,33 +270,78 @@ export function uiMessagesToWire(
|
|
|
151
270
|
|
|
152
271
|
function toAnchor(
|
|
153
272
|
msg: UIMessage,
|
|
273
|
+
role: 'system',
|
|
274
|
+
extras: { content: string },
|
|
275
|
+
parts: ReadonlyArray<MessagePart>,
|
|
276
|
+
includeSnapshotStructuredOutput: boolean,
|
|
277
|
+
): WireSystemMessage
|
|
278
|
+
function toAnchor(
|
|
279
|
+
msg: UIMessage,
|
|
280
|
+
role: 'user',
|
|
281
|
+
extras: { content: string | Array<InputContent> },
|
|
282
|
+
parts: ReadonlyArray<MessagePart>,
|
|
283
|
+
includeSnapshotStructuredOutput: boolean,
|
|
284
|
+
): WireUserMessage
|
|
285
|
+
function toAnchor(
|
|
286
|
+
msg: UIMessage,
|
|
287
|
+
role: 'assistant',
|
|
154
288
|
extras: {
|
|
155
|
-
content?: string
|
|
156
|
-
toolCalls?: Array<
|
|
289
|
+
content?: string
|
|
290
|
+
toolCalls?: Array<ToolCall>
|
|
157
291
|
},
|
|
158
292
|
parts: ReadonlyArray<MessagePart>,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
293
|
+
includeSnapshotStructuredOutput: boolean,
|
|
294
|
+
): WireAssistantMessage
|
|
295
|
+
function toAnchor(
|
|
296
|
+
msg: UIMessage,
|
|
297
|
+
role: UIMessage['role'],
|
|
298
|
+
extras: {
|
|
299
|
+
content?: string | Array<InputContent>
|
|
300
|
+
toolCalls?: Array<ToolCall>
|
|
301
|
+
},
|
|
302
|
+
parts: ReadonlyArray<MessagePart>,
|
|
303
|
+
includeSnapshotStructuredOutput: boolean,
|
|
304
|
+
): WireSystemMessage | WireUserMessage | WireAssistantMessage {
|
|
305
|
+
const metadata = messageMetadata(msg, parts, includeSnapshotStructuredOutput)
|
|
306
|
+
const base = {
|
|
163
307
|
id: msg.id,
|
|
164
|
-
|
|
165
|
-
...(name !== undefined && { name }),
|
|
166
|
-
...extras,
|
|
308
|
+
...(msg.name !== undefined && { name: msg.name }),
|
|
167
309
|
...(metadata !== undefined && { metadata }),
|
|
168
310
|
}
|
|
311
|
+
if (role === 'system') {
|
|
312
|
+
return { ...base, role, content: String(extras.content ?? '') }
|
|
313
|
+
}
|
|
314
|
+
if (role === 'user') {
|
|
315
|
+
return { ...base, role, content: extras.content ?? '' }
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
...base,
|
|
319
|
+
role,
|
|
320
|
+
...(typeof extras.content === 'string' && { content: extras.content }),
|
|
321
|
+
...(extras.toolCalls !== undefined && { toolCalls: extras.toolCalls }),
|
|
322
|
+
}
|
|
169
323
|
}
|
|
170
324
|
|
|
171
325
|
function messageMetadata(
|
|
172
326
|
msg: UIMessage,
|
|
173
327
|
parts: ReadonlyArray<MessagePart>,
|
|
328
|
+
includeSnapshotStructuredOutput: boolean,
|
|
174
329
|
): MetadataRecord | undefined {
|
|
175
330
|
const base: MetadataRecord = { ...(msg.metadata ?? {}) }
|
|
176
|
-
const
|
|
177
|
-
|
|
331
|
+
const previousTanstack = tanstackMetadata(msg)
|
|
332
|
+
const tanstack: TanStackMessageMetadata = {}
|
|
333
|
+
if (previousTanstack?.model !== undefined)
|
|
334
|
+
tanstack.model = previousTanstack.model
|
|
335
|
+
if (previousTanstack?.signature !== undefined)
|
|
336
|
+
tanstack.signature = previousTanstack.signature
|
|
337
|
+
const createdAt = coerceCreatedAt(msg.createdAt)
|
|
338
|
+
if (createdAt !== undefined) tanstack.createdAt = createdAt.toISOString()
|
|
178
339
|
|
|
179
|
-
const
|
|
180
|
-
|
|
340
|
+
const structuredOutput = serializedStructuredOutput(
|
|
341
|
+
parts,
|
|
342
|
+
includeSnapshotStructuredOutput,
|
|
343
|
+
)
|
|
344
|
+
if (structuredOutput) tanstack.structuredOutput = structuredOutput
|
|
181
345
|
|
|
182
346
|
const toolCallMetadata: Record<string, unknown> = {}
|
|
183
347
|
for (const part of parts) {
|
|
@@ -195,26 +359,41 @@ function messageMetadata(
|
|
|
195
359
|
if (uiResources.length > 0) tanstack.uiResources = uiResources
|
|
196
360
|
|
|
197
361
|
if (Object.keys(tanstack).length > 0) base.tanstack = tanstack
|
|
362
|
+
else delete base.tanstack
|
|
198
363
|
return Object.keys(base).length > 0 ? base : undefined
|
|
199
364
|
}
|
|
200
365
|
|
|
201
|
-
function
|
|
366
|
+
function serializedStructuredOutput(
|
|
202
367
|
parts: ReadonlyArray<MessagePart>,
|
|
368
|
+
includeSnapshotStructuredOutput: boolean,
|
|
203
369
|
): TanStackMessageMetadata['structuredOutput'] | undefined {
|
|
204
370
|
for (const p of parts) {
|
|
205
|
-
if (
|
|
206
|
-
|
|
371
|
+
if (
|
|
372
|
+
p.type === 'structured-output' &&
|
|
373
|
+
(includeSnapshotStructuredOutput || p.status !== 'complete')
|
|
374
|
+
) {
|
|
375
|
+
return structuredOutputMetadata(p, includeSnapshotStructuredOutput)
|
|
207
376
|
}
|
|
208
377
|
}
|
|
209
378
|
return undefined
|
|
210
379
|
}
|
|
211
380
|
|
|
212
|
-
function
|
|
381
|
+
function structuredOutputMetadata(
|
|
213
382
|
part: StructuredOutputPart,
|
|
383
|
+
includeSnapshotStructuredOutput: boolean,
|
|
214
384
|
): NonNullable<TanStackMessageMetadata['structuredOutput']> {
|
|
215
385
|
return {
|
|
216
386
|
status: part.status,
|
|
217
387
|
raw: part.raw,
|
|
388
|
+
...(includeSnapshotStructuredOutput && part.partial !== undefined
|
|
389
|
+
? { partial: part.partial }
|
|
390
|
+
: {}),
|
|
391
|
+
...(includeSnapshotStructuredOutput && part.data !== undefined
|
|
392
|
+
? { data: part.data }
|
|
393
|
+
: {}),
|
|
394
|
+
...(includeSnapshotStructuredOutput && part.reasoning
|
|
395
|
+
? { reasoning: part.reasoning }
|
|
396
|
+
: {}),
|
|
218
397
|
...(part.errorMessage !== undefined && { errorMessage: part.errorMessage }),
|
|
219
398
|
}
|
|
220
399
|
}
|
|
@@ -247,7 +426,7 @@ function collectText(parts: ReadonlyArray<MessagePart>): string {
|
|
|
247
426
|
|
|
248
427
|
function collectUserContent(
|
|
249
428
|
parts: ReadonlyArray<MessagePart>,
|
|
250
|
-
): string | Array<
|
|
429
|
+
): string | Array<InputContent> {
|
|
251
430
|
const hasMultimodal = parts.some(
|
|
252
431
|
(p) =>
|
|
253
432
|
p.type === 'image' ||
|
|
@@ -258,7 +437,7 @@ function collectUserContent(
|
|
|
258
437
|
if (!hasMultimodal) {
|
|
259
438
|
return collectText(parts)
|
|
260
439
|
}
|
|
261
|
-
const out: Array<
|
|
440
|
+
const out: Array<InputContent> = []
|
|
262
441
|
for (const p of parts) {
|
|
263
442
|
if (p.type === 'text') {
|
|
264
443
|
out.push({ type: 'text', text: p.content })
|
|
@@ -289,8 +468,8 @@ function thoughtSignatureFromMetadata(metadata: unknown): string | undefined {
|
|
|
289
468
|
|
|
290
469
|
function collectToolCalls(
|
|
291
470
|
parts: ReadonlyArray<MessagePart>,
|
|
292
|
-
): Array<
|
|
293
|
-
const calls: Array<
|
|
471
|
+
): Array<ToolCall> | undefined {
|
|
472
|
+
const calls: Array<ToolCall> = []
|
|
294
473
|
for (const p of parts) {
|
|
295
474
|
if (p.type === 'tool-call') {
|
|
296
475
|
const encryptedValue = thoughtSignatureFromMetadata(p.metadata)
|
|
@@ -313,6 +492,32 @@ function deriveToolMessageId(toolCallId: string): string {
|
|
|
313
492
|
return `tool-${toolCallId}`
|
|
314
493
|
}
|
|
315
494
|
|
|
495
|
+
function uniqueToolWireId(id: string, used: Set<string>): string {
|
|
496
|
+
return uniqueWireId(id, used)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function uniqueWireId(id: string, used: Set<string>): string {
|
|
500
|
+
if (!used.has(id)) {
|
|
501
|
+
used.add(id)
|
|
502
|
+
return id
|
|
503
|
+
}
|
|
504
|
+
let suffix = 2
|
|
505
|
+
while (used.has(`${id}-${suffix}`)) suffix++
|
|
506
|
+
const unique = `${id}-${suffix}`
|
|
507
|
+
used.add(unique)
|
|
508
|
+
return unique
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function toolWireId(
|
|
512
|
+
id: string | undefined,
|
|
513
|
+
toolCallId: string,
|
|
514
|
+
assistantIds: ReadonlySet<string>,
|
|
515
|
+
): string {
|
|
516
|
+
const derived = deriveToolMessageId(toolCallId)
|
|
517
|
+
if (id === undefined || assistantIds.has(id)) return derived
|
|
518
|
+
return id
|
|
519
|
+
}
|
|
520
|
+
|
|
316
521
|
function hashContent(s: string): string {
|
|
317
522
|
// Cheap deterministic id suffix; collisions are tolerable since
|
|
318
523
|
// reasoning ids only matter for AG-UI server consumers, not for our
|