@tanstack/ai 0.47.3 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +2 -1
  2. package/dist/esm/activities/chat/adapter.d.ts +5 -4
  3. package/dist/esm/activities/chat/adapter.js.map +1 -1
  4. package/dist/esm/activities/chat/index.d.ts +4 -7
  5. package/dist/esm/activities/chat/index.js +201 -243
  6. package/dist/esm/activities/chat/index.js.map +1 -1
  7. package/dist/esm/activities/chat/messages.js +115 -25
  8. package/dist/esm/activities/chat/messages.js.map +1 -1
  9. package/dist/esm/activities/chat/stream/processor.d.ts +38 -17
  10. package/dist/esm/activities/chat/stream/processor.js +186 -101
  11. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  12. package/dist/esm/activities/chat/tools/tool-calls.d.ts +3 -2
  13. package/dist/esm/activities/chat/tools/tool-calls.js +15 -10
  14. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  15. package/dist/esm/activities/generateVideo/index.js +6 -6
  16. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  17. package/dist/esm/activities/stream-generation-result.js +7 -8
  18. package/dist/esm/activities/stream-generation-result.js.map +1 -1
  19. package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -1
  20. package/dist/esm/activities/summarize/chat-stream-summarize.js +56 -53
  21. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  22. package/dist/esm/adapter-internals.d.ts +2 -0
  23. package/dist/esm/adapter-internals.js +3 -1
  24. package/dist/esm/client.d.ts +8 -1
  25. package/dist/esm/client.js +7 -2
  26. package/dist/esm/client.js.map +1 -1
  27. package/dist/esm/index.d.ts +6 -0
  28. package/dist/esm/index.js +6 -2
  29. package/dist/esm/middlewares/otel.js +11 -4
  30. package/dist/esm/middlewares/otel.js.map +1 -1
  31. package/dist/esm/stream-to-response.js +8 -5
  32. package/dist/esm/stream-to-response.js.map +1 -1
  33. package/dist/esm/stream-to-websocket.js +4 -2
  34. package/dist/esm/stream-to-websocket.js.map +1 -1
  35. package/dist/esm/strip-to-spec-middleware.d.ts +10 -13
  36. package/dist/esm/strip-to-spec-middleware.js +24 -22
  37. package/dist/esm/strip-to-spec-middleware.js.map +1 -1
  38. package/dist/esm/types.d.ts +82 -305
  39. package/dist/esm/utilities/adapter-yield-chunk.d.ts +31 -0
  40. package/dist/esm/utilities/ag-ui-usage.d.ts +24 -0
  41. package/dist/esm/utilities/ag-ui-usage.js +66 -0
  42. package/dist/esm/utilities/ag-ui-usage.js.map +1 -0
  43. package/dist/esm/utilities/ag-ui-wire.d.ts +14 -7
  44. package/dist/esm/utilities/ag-ui-wire.js +71 -30
  45. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  46. package/dist/esm/utilities/chat-params.d.ts +3 -3
  47. package/dist/esm/utilities/chat-params.js +10 -30
  48. package/dist/esm/utilities/chat-params.js.map +1 -1
  49. package/dist/esm/utilities/chunk-ids.d.ts +5 -0
  50. package/dist/esm/utilities/chunk-ids.js +25 -0
  51. package/dist/esm/utilities/chunk-ids.js.map +1 -0
  52. package/dist/esm/utilities/merge-metadata.d.ts +14 -0
  53. package/dist/esm/utilities/merge-metadata.js +43 -0
  54. package/dist/esm/utilities/merge-metadata.js.map +1 -0
  55. package/dist/esm/utilities/normalize-stream-chunk.d.ts +3 -0
  56. package/dist/esm/utilities/normalize-stream-chunk.js +100 -0
  57. package/dist/esm/utilities/normalize-stream-chunk.js.map +1 -0
  58. package/dist/esm/utilities/reasoning-encrypted-value.d.ts +8 -0
  59. package/dist/esm/utilities/reasoning-encrypted-value.js +16 -0
  60. package/dist/esm/utilities/reasoning-encrypted-value.js.map +1 -0
  61. package/dist/esm/utilities/restore-inbound-chunk.d.ts +15 -0
  62. package/dist/esm/utilities/restore-inbound-chunk.js +43 -0
  63. package/dist/esm/utilities/restore-inbound-chunk.js.map +1 -0
  64. package/dist/esm/utilities/spec-event-keys.d.ts +2 -0
  65. package/dist/esm/utilities/spec-event-keys.js +56 -0
  66. package/dist/esm/utilities/spec-event-keys.js.map +1 -0
  67. package/dist/esm/utilities/structured-output-events.d.ts +3 -3
  68. package/dist/esm/utilities/structured-output-events.js.map +1 -1
  69. package/package.json +2 -2
  70. package/src/activities/chat/adapter.ts +4 -4
  71. package/src/activities/chat/index.ts +398 -400
  72. package/src/activities/chat/messages.ts +174 -34
  73. package/src/activities/chat/stream/processor.ts +289 -218
  74. package/src/activities/chat/tools/tool-calls.ts +23 -22
  75. package/src/activities/generateVideo/index.ts +7 -6
  76. package/src/activities/stream-generation-result.ts +8 -12
  77. package/src/activities/summarize/chat-stream-summarize.ts +94 -70
  78. package/src/adapter-internals.ts +2 -0
  79. package/src/client.ts +13 -0
  80. package/src/index.ts +6 -0
  81. package/src/middlewares/otel.ts +16 -8
  82. package/src/stream-to-response.ts +11 -4
  83. package/src/stream-to-websocket.ts +3 -1
  84. package/src/strip-to-spec-middleware.ts +48 -24
  85. package/src/types.ts +109 -393
  86. package/src/utilities/adapter-yield-chunk.ts +30 -0
  87. package/src/utilities/ag-ui-usage.test.ts +194 -0
  88. package/src/utilities/ag-ui-usage.ts +148 -0
  89. package/src/utilities/ag-ui-wire.ts +149 -31
  90. package/src/utilities/chat-params.ts +22 -39
  91. package/src/utilities/chunk-ids.ts +24 -0
  92. package/src/utilities/merge-metadata.test.ts +117 -0
  93. package/src/utilities/merge-metadata.ts +59 -0
  94. package/src/utilities/normalize-stream-chunk.test.ts +423 -0
  95. package/src/utilities/normalize-stream-chunk.ts +186 -0
  96. package/src/utilities/reasoning-encrypted-value.ts +18 -0
  97. package/src/utilities/restore-inbound-chunk.test.ts +133 -0
  98. package/src/utilities/restore-inbound-chunk.ts +72 -0
  99. package/src/utilities/spec-event-keys.test.ts +34 -0
  100. package/src/utilities/spec-event-keys.ts +74 -0
  101. package/src/utilities/structured-output-events.ts +3 -3
@@ -0,0 +1,30 @@
1
+ import type { InterruptSubmissionError } from '../interrupts'
2
+ import type { ContentPart, StreamChunk, ToolOutputState } from '../types'
3
+
4
+ /**
5
+ * Adapter / engine yield before normalize. Public StreamChunk is spec-only.
6
+ * This type still allows the old extra fields.
7
+ */
8
+ export type AdapterYieldChunk = StreamChunk & {
9
+ model?: string
10
+ finishReason?: 'stop' | 'length' | 'content_filter' | 'tool_calls' | null
11
+ // `any` so TokenUsage adapter yields stay assignable after public usage[]
12
+ usage?: any
13
+ content?: string
14
+ args?: string
15
+ toolName?: string
16
+ toolCallName?: string
17
+ index?: number
18
+ input?: unknown
19
+ output?: unknown
20
+ result?: string | Array<ContentPart>
21
+ state?: ToolOutputState | Record<string, unknown>
22
+ stepId?: string
23
+ stepType?: string
24
+ delta?: string | ReadonlyArray<unknown>
25
+ signature?: string
26
+ error?: { message: string; code?: string }
27
+ 'tanstack:interruptErrors'?: ReadonlyArray<InterruptSubmissionError>
28
+ threadId?: string
29
+ runId?: string
30
+ }
@@ -0,0 +1,194 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ fromSpecTokenUsage,
4
+ rebuildTokenUsage,
5
+ toSpecTokenUsage,
6
+ } from './ag-ui-usage'
7
+ import type { TokenUsage } from '../types'
8
+
9
+ const rich: TokenUsage = {
10
+ promptTokens: 100,
11
+ completionTokens: 50,
12
+ totalTokens: 150,
13
+ promptTokensDetails: { cachedTokens: 10 },
14
+ completionTokensDetails: { reasoningTokens: 7 },
15
+ billed: { quantity: 3, unit: 'units' },
16
+ cost: 0.02,
17
+ costDetails: { upstreamInputCost: 0.01, upstreamOutputCost: 0.01 },
18
+ providerUsageDetails: { foo: 1 },
19
+ durationSeconds: 2,
20
+ unitsBilled: 3,
21
+ }
22
+
23
+ describe('toSpecTokenUsage', () => {
24
+ it('maps core fields to spec usage[0] and keeps leftovers under tanstack.usage', () => {
25
+ const { usage, leftover } = toSpecTokenUsage(rich, {
26
+ provider: 'openai',
27
+ model: 'gpt-5.5',
28
+ })
29
+ expect(usage).toEqual([
30
+ {
31
+ provider: 'openai',
32
+ model: 'gpt-5.5',
33
+ inputTokens: 100,
34
+ outputTokens: 50,
35
+ totalTokens: 150,
36
+ cachedInputTokens: 10,
37
+ reasoningTokens: 7,
38
+ },
39
+ ])
40
+ expect(leftover).toEqual({
41
+ billed: { quantity: 3, unit: 'units' },
42
+ cost: 0.02,
43
+ costDetails: { upstreamInputCost: 0.01, upstreamOutputCost: 0.01 },
44
+ providerUsageDetails: { foo: 1 },
45
+ durationSeconds: 2,
46
+ unitsBilled: 3,
47
+ })
48
+ })
49
+
50
+ it('returns undefined leftover when only mapped token fields exist', () => {
51
+ const { leftover } = toSpecTokenUsage({
52
+ promptTokens: 1,
53
+ completionTokens: 2,
54
+ totalTokens: 3,
55
+ promptTokensDetails: { cachedTokens: 10 },
56
+ completionTokensDetails: { reasoningTokens: 7 },
57
+ })
58
+ expect(leftover).toBeUndefined()
59
+ })
60
+
61
+ it('keeps unmapped detail keys in leftover', () => {
62
+ const { usage, leftover } = toSpecTokenUsage({
63
+ promptTokens: 1,
64
+ completionTokens: 2,
65
+ totalTokens: 3,
66
+ promptTokensDetails: { cachedTokens: 10, audioTokens: 4 },
67
+ completionTokensDetails: { reasoningTokens: 7, audioTokens: 1 },
68
+ })
69
+ expect(usage[0]?.cachedInputTokens).toBe(10)
70
+ expect(usage[0]?.reasoningTokens).toBe(7)
71
+ expect(leftover).toEqual({
72
+ promptTokensDetails: { audioTokens: 4 },
73
+ completionTokensDetails: { audioTokens: 1 },
74
+ })
75
+ })
76
+ })
77
+
78
+ describe('fromSpecTokenUsage', () => {
79
+ it('rebuilds TokenUsage from spec array plus leftover', () => {
80
+ const { usage, leftover } = toSpecTokenUsage(rich, {
81
+ provider: 'openai',
82
+ model: 'gpt-5.5',
83
+ })
84
+ expect(fromSpecTokenUsage(usage, leftover)).toEqual(rich)
85
+ })
86
+
87
+ it('rebuilds from spec array alone', () => {
88
+ expect(
89
+ fromSpecTokenUsage([{ inputTokens: 4, outputTokens: 5, totalTokens: 9 }]),
90
+ ).toEqual({
91
+ promptTokens: 4,
92
+ completionTokens: 5,
93
+ totalTokens: 9,
94
+ })
95
+ })
96
+
97
+ it('rebuilds spec-only cachedInputTokens and reasoningTokens into details', () => {
98
+ expect(
99
+ fromSpecTokenUsage([
100
+ {
101
+ inputTokens: 1,
102
+ outputTokens: 2,
103
+ totalTokens: 3,
104
+ cachedInputTokens: 10,
105
+ reasoningTokens: 7,
106
+ },
107
+ ]),
108
+ ).toEqual({
109
+ promptTokens: 1,
110
+ completionTokens: 2,
111
+ totalTokens: 3,
112
+ promptTokensDetails: { cachedTokens: 10 },
113
+ completionTokensDetails: { reasoningTokens: 7 },
114
+ })
115
+ })
116
+
117
+ it('keeps spec cachedTokens and leftover extra promptTokensDetails keys', () => {
118
+ expect(
119
+ fromSpecTokenUsage(
120
+ [
121
+ {
122
+ inputTokens: 1,
123
+ outputTokens: 2,
124
+ totalTokens: 3,
125
+ cachedInputTokens: 10,
126
+ },
127
+ ],
128
+ { promptTokensDetails: { audioTokens: 5 } },
129
+ ),
130
+ ).toEqual({
131
+ promptTokens: 1,
132
+ completionTokens: 2,
133
+ totalTokens: 3,
134
+ promptTokensDetails: { cachedTokens: 10, audioTokens: 5 },
135
+ })
136
+ })
137
+
138
+ it('keeps spec reasoningTokens and leftover extra completionTokensDetails keys', () => {
139
+ expect(
140
+ fromSpecTokenUsage(
141
+ [
142
+ {
143
+ inputTokens: 1,
144
+ outputTokens: 2,
145
+ totalTokens: 3,
146
+ reasoningTokens: 7,
147
+ },
148
+ ],
149
+ { completionTokensDetails: { audioTokens: 4 } },
150
+ ),
151
+ ).toEqual({
152
+ promptTokens: 1,
153
+ completionTokens: 2,
154
+ totalTokens: 3,
155
+ completionTokensDetails: { reasoningTokens: 7, audioTokens: 4 },
156
+ })
157
+ })
158
+
159
+ it('returns undefined for missing or empty spec usage without leftover', () => {
160
+ expect(fromSpecTokenUsage(undefined)).toBeUndefined()
161
+ expect(fromSpecTokenUsage([])).toBeUndefined()
162
+ })
163
+ })
164
+
165
+ describe('rebuildTokenUsage', () => {
166
+ it('returns TokenUsage objects unchanged', () => {
167
+ expect(
168
+ rebuildTokenUsage({
169
+ promptTokens: 1,
170
+ completionTokens: 2,
171
+ totalTokens: 3,
172
+ cost: 0.01,
173
+ }),
174
+ ).toEqual({
175
+ promptTokens: 1,
176
+ completionTokens: 2,
177
+ totalTokens: 3,
178
+ cost: 0.01,
179
+ })
180
+ })
181
+
182
+ it('rebuilds spec usage[] plus leftover into TokenUsage', () => {
183
+ expect(
184
+ rebuildTokenUsage([{ inputTokens: 4, outputTokens: 5, totalTokens: 9 }], {
185
+ cost: 0.02,
186
+ }),
187
+ ).toEqual({
188
+ promptTokens: 4,
189
+ completionTokens: 5,
190
+ totalTokens: 9,
191
+ cost: 0.02,
192
+ })
193
+ })
194
+ })
@@ -0,0 +1,148 @@
1
+ import type { TokenUsage } from '../types'
2
+
3
+ /** AG-UI spec `usage[]` item (provider/model labels + token counts only). */
4
+ export interface SpecTokenUsage {
5
+ provider?: string
6
+ model?: string
7
+ inputTokens?: number
8
+ outputTokens?: number
9
+ totalTokens?: number
10
+ reasoningTokens?: number
11
+ cachedInputTokens?: number
12
+ }
13
+
14
+ export interface ToSpecTokenUsageOptions {
15
+ provider?: string
16
+ model?: string
17
+ }
18
+
19
+ /** TokenUsage fields that have no AG-UI `usage[]` equivalent. */
20
+ export type TokenUsageLeftover = Omit<
21
+ TokenUsage,
22
+ 'promptTokens' | 'completionTokens' | 'totalTokens'
23
+ >
24
+
25
+ function definedDetails<T extends object>(value: T): T | undefined {
26
+ return Object.keys(value).length > 0 ? value : undefined
27
+ }
28
+
29
+ function withoutKey<T extends object, K extends keyof T>(
30
+ value: T,
31
+ key: K,
32
+ ): Omit<T, K> {
33
+ const next = { ...value }
34
+ delete next[key]
35
+ return next
36
+ }
37
+
38
+ export function isTanstackUsage(usage: unknown): usage is TokenUsage {
39
+ return (
40
+ typeof usage === 'object' &&
41
+ usage != null &&
42
+ !Array.isArray(usage) &&
43
+ 'promptTokens' in usage
44
+ )
45
+ }
46
+
47
+ export function toSpecTokenUsage(
48
+ usage: TokenUsage,
49
+ options?: ToSpecTokenUsageOptions,
50
+ ): { usage: Array<SpecTokenUsage>; leftover?: TokenUsageLeftover } {
51
+ const {
52
+ promptTokens,
53
+ completionTokens,
54
+ totalTokens,
55
+ promptTokensDetails,
56
+ completionTokensDetails,
57
+ ...rest
58
+ } = usage
59
+
60
+ const spec: SpecTokenUsage = {
61
+ ...(options?.provider !== undefined ? { provider: options.provider } : {}),
62
+ ...(options?.model !== undefined ? { model: options.model } : {}),
63
+ inputTokens: promptTokens,
64
+ outputTokens: completionTokens,
65
+ totalTokens,
66
+ }
67
+ const cachedInputTokens = promptTokensDetails?.cachedTokens
68
+ if (cachedInputTokens !== undefined) {
69
+ spec.cachedInputTokens = cachedInputTokens
70
+ }
71
+ const reasoningTokens = completionTokensDetails?.reasoningTokens
72
+ if (reasoningTokens !== undefined) {
73
+ spec.reasoningTokens = reasoningTokens
74
+ }
75
+
76
+ const leftoverPrompt = promptTokensDetails
77
+ ? definedDetails(withoutKey(promptTokensDetails, 'cachedTokens'))
78
+ : undefined
79
+ const leftoverCompletion = completionTokensDetails
80
+ ? definedDetails(withoutKey(completionTokensDetails, 'reasoningTokens'))
81
+ : undefined
82
+
83
+ return {
84
+ usage: [spec],
85
+ leftover: definedDetails({
86
+ ...rest,
87
+ ...(leftoverPrompt !== undefined
88
+ ? { promptTokensDetails: leftoverPrompt }
89
+ : {}),
90
+ ...(leftoverCompletion !== undefined
91
+ ? { completionTokensDetails: leftoverCompletion }
92
+ : {}),
93
+ }),
94
+ }
95
+ }
96
+
97
+ export function rebuildTokenUsage(
98
+ usage: unknown,
99
+ leftover?: TokenUsageLeftover,
100
+ ): TokenUsage | undefined {
101
+ if (isTanstackUsage(usage)) {
102
+ return usage
103
+ }
104
+ if (Array.isArray(usage)) {
105
+ return fromSpecTokenUsage(usage, leftover)
106
+ }
107
+ return fromSpecTokenUsage(undefined, leftover)
108
+ }
109
+
110
+ export function fromSpecTokenUsage(
111
+ usage: ReadonlyArray<SpecTokenUsage> | undefined,
112
+ leftover?: TokenUsageLeftover,
113
+ ): TokenUsage | undefined {
114
+ const spec = usage?.[0]
115
+ if (spec == null && leftover == null) {
116
+ return undefined
117
+ }
118
+
119
+ const {
120
+ promptTokensDetails: leftoverPromptDetails,
121
+ completionTokensDetails: leftoverCompletionDetails,
122
+ ...leftoverRest
123
+ } = leftover ?? {}
124
+
125
+ const promptTokensDetails = definedDetails({
126
+ ...(spec?.cachedInputTokens !== undefined
127
+ ? { cachedTokens: spec.cachedInputTokens }
128
+ : {}),
129
+ ...leftoverPromptDetails,
130
+ })
131
+ const completionTokensDetails = definedDetails({
132
+ ...(spec?.reasoningTokens !== undefined
133
+ ? { reasoningTokens: spec.reasoningTokens }
134
+ : {}),
135
+ ...leftoverCompletionDetails,
136
+ })
137
+
138
+ return {
139
+ promptTokens: spec?.inputTokens ?? 0,
140
+ completionTokens: spec?.outputTokens ?? 0,
141
+ totalTokens: spec?.totalTokens ?? 0,
142
+ ...leftoverRest,
143
+ ...(promptTokensDetails !== undefined ? { promptTokensDetails } : {}),
144
+ ...(completionTokensDetails !== undefined
145
+ ? { completionTokensDetails }
146
+ : {}),
147
+ }
148
+ }
@@ -1,4 +1,13 @@
1
- import type { ContentPart, MessagePart, UIMessage } from '../types'
1
+ import type {
2
+ ContentPart,
3
+ MessagePart,
4
+ StructuredOutputPart,
5
+ TanStackMessageMetadata,
6
+ UIMessage,
7
+ UIResourcePart,
8
+ } from '../types'
9
+ import type { MetadataRecord } from './merge-metadata'
10
+ import { tanstackMetadata } from './merge-metadata'
2
11
 
3
12
  type AGUITextInputContent = { type: 'text'; text: string }
4
13
  type AGUIInputContent =
@@ -9,6 +18,7 @@ type AGUIToolCallMirror = {
9
18
  id: string
10
19
  type: 'function'
11
20
  function: { name: string; arguments: string }
21
+ encryptedValue?: string
12
22
  }
13
23
 
14
24
  type AGUIToolMessage = {
@@ -23,11 +33,18 @@ type AGUIReasoningMessage = {
23
33
  role: 'reasoning'
24
34
  id: string
25
35
  content: string
36
+ encryptedValue?: string
37
+ metadata?: MetadataRecord
26
38
  }
27
39
 
28
- type WireAnchorMessage = UIMessage & {
40
+ /** Spec AG-UI message. No `parts`, no `createdAt` Date. */
41
+ type WireAnchorMessage = {
42
+ id: string
43
+ role: UIMessage['role']
44
+ name?: string
29
45
  content?: string | Array<AGUIInputContent>
30
46
  toolCalls?: Array<AGUIToolCallMirror>
47
+ metadata?: MetadataRecord
31
48
  }
32
49
 
33
50
  export type WireMessage =
@@ -37,12 +54,10 @@ export type WireMessage =
37
54
 
38
55
  /**
39
56
  * Serialize TanStack `UIMessage`s into the AG-UI `RunAgentInput.messages`
40
- * wire shape. Each anchor (system/user/assistant) carries the canonical
41
- * `parts` array verbatim plus AG-UI mirror fields (`content`, `toolCalls`)
42
- * so AG-UI Zod parsing succeeds. Tool results and thinking parts on
43
- * assistant messages are additionally emitted as fan-out
44
- * `{role:'tool',...}` and `{role:'reasoning',...}` entries for strict
45
- * AG-UI server consumers.
57
+ * wire shape. Anchors are spec-only (`id`, `role`, `name`, `content`,
58
+ * `toolCalls`, `metadata`). Tool results and thinking parts on assistant
59
+ * messages are additionally emitted as fan-out `{role:'tool',...}` and
60
+ * `{role:'reasoning',...}` entries for strict AG-UI server consumers.
46
61
  */
47
62
  export function uiMessagesToWire(
48
63
  messages: Array<UIMessage>,
@@ -50,52 +65,70 @@ export function uiMessagesToWire(
50
65
  const wire: Array<WireMessage> = []
51
66
 
52
67
  for (const msg of messages) {
53
- // Defensive: if parts is missing (ModelMessage-shaped input), pass through as-is.
54
- // UIMessage always has parts; ModelMessage uses content directly.
68
+ // Defensive: ModelMessage-shaped input has no `parts`; fall back to `content`.
55
69
  const parts: ReadonlyArray<MessagePart> =
56
70
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- runtime input may be ModelMessage-shaped (no `parts`); cast forces the optional-chain fallback below to remain in scope
57
71
  (msg.parts as ReadonlyArray<MessagePart> | undefined) ?? []
58
72
 
59
73
  if (msg.role === 'system') {
60
- wire.push({
61
- ...msg,
62
- content:
63
- parts.length > 0
64
- ? collectText(parts)
65
- : ((msg as { content?: string }).content ?? ''),
66
- })
74
+ wire.push(
75
+ toAnchor(
76
+ msg,
77
+ {
78
+ content:
79
+ parts.length > 0
80
+ ? collectText(parts)
81
+ : ((msg as { content?: string }).content ?? ''),
82
+ },
83
+ parts,
84
+ ),
85
+ )
67
86
  continue
68
87
  }
69
88
 
70
89
  if (msg.role === 'user') {
71
- wire.push({
72
- ...msg,
73
- content:
74
- parts.length > 0
75
- ? collectUserContent(parts)
76
- : ((msg as { content?: string }).content ?? ''),
77
- })
90
+ wire.push(
91
+ toAnchor(
92
+ msg,
93
+ {
94
+ content:
95
+ parts.length > 0
96
+ ? collectUserContent(parts)
97
+ : ((msg as { content?: string }).content ?? ''),
98
+ },
99
+ parts,
100
+ ),
101
+ )
78
102
  continue
79
103
  }
80
104
 
81
105
  // assistant: emit reasoning fan-outs first, then anchor, then tool fan-outs
82
106
  for (const part of parts) {
83
107
  if (part.type === 'thinking') {
84
- wire.push({
108
+ const reasoning: AGUIReasoningMessage = {
85
109
  role: 'reasoning',
86
110
  id: deriveReasoningId(msg.id, part),
87
111
  content: part.content,
88
- })
112
+ }
113
+ if (part.signature) {
114
+ reasoning.encryptedValue = part.signature
115
+ }
116
+ wire.push(reasoning)
89
117
  }
90
118
  }
91
119
 
92
120
  const text = collectText(parts)
93
121
  const toolCalls = collectToolCalls(parts)
94
- wire.push({
95
- ...msg,
96
- ...(text !== '' && { content: text }),
97
- ...(toolCalls && { toolCalls }),
98
- })
122
+ wire.push(
123
+ toAnchor(
124
+ msg,
125
+ {
126
+ ...(text !== '' && { content: text }),
127
+ ...(toolCalls && { toolCalls }),
128
+ },
129
+ parts,
130
+ ),
131
+ )
99
132
 
100
133
  for (const part of parts) {
101
134
  if (part.type === 'tool-result') {
@@ -116,6 +149,76 @@ export function uiMessagesToWire(
116
149
  return wire
117
150
  }
118
151
 
152
+ function toAnchor(
153
+ msg: UIMessage,
154
+ extras: {
155
+ content?: string | Array<AGUIInputContent>
156
+ toolCalls?: Array<AGUIToolCallMirror>
157
+ },
158
+ parts: ReadonlyArray<MessagePart>,
159
+ ): WireAnchorMessage {
160
+ const metadata = messageMetadata(msg, parts)
161
+ const name = (msg as { name?: string }).name
162
+ return {
163
+ id: msg.id,
164
+ role: msg.role,
165
+ ...(name !== undefined && { name }),
166
+ ...extras,
167
+ ...(metadata !== undefined && { metadata }),
168
+ }
169
+ }
170
+
171
+ function messageMetadata(
172
+ msg: UIMessage,
173
+ parts: ReadonlyArray<MessagePart>,
174
+ ): MetadataRecord | undefined {
175
+ const base: MetadataRecord = { ...(msg.metadata ?? {}) }
176
+ const tanstack: MetadataRecord = { ...(tanstackMetadata(msg) ?? {}) }
177
+ if (msg.createdAt) tanstack.createdAt = msg.createdAt.toISOString()
178
+
179
+ const leftover = unfinishedStructuredOutput(parts)
180
+ if (leftover) tanstack.structuredOutput = leftover
181
+
182
+ const toolCallMetadata: Record<string, unknown> = {}
183
+ for (const part of parts) {
184
+ if (part.type === 'tool-call' && part.metadata !== undefined) {
185
+ toolCallMetadata[part.id] = part.metadata
186
+ }
187
+ }
188
+ if (Object.keys(toolCallMetadata).length > 0) {
189
+ tanstack.toolCallMetadata = toolCallMetadata
190
+ }
191
+
192
+ const uiResources = parts.filter(
193
+ (p): p is UIResourcePart => p.type === 'ui-resource',
194
+ )
195
+ if (uiResources.length > 0) tanstack.uiResources = uiResources
196
+
197
+ if (Object.keys(tanstack).length > 0) base.tanstack = tanstack
198
+ return Object.keys(base).length > 0 ? base : undefined
199
+ }
200
+
201
+ function unfinishedStructuredOutput(
202
+ parts: ReadonlyArray<MessagePart>,
203
+ ): TanStackMessageMetadata['structuredOutput'] | undefined {
204
+ for (const p of parts) {
205
+ if (p.type === 'structured-output' && p.status !== 'complete') {
206
+ return structuredOutputLeftover(p)
207
+ }
208
+ }
209
+ return undefined
210
+ }
211
+
212
+ function structuredOutputLeftover(
213
+ part: StructuredOutputPart,
214
+ ): NonNullable<TanStackMessageMetadata['structuredOutput']> {
215
+ return {
216
+ status: part.status,
217
+ raw: part.raw,
218
+ ...(part.errorMessage !== undefined && { errorMessage: part.errorMessage }),
219
+ }
220
+ }
221
+
119
222
  function collectText(parts: ReadonlyArray<MessagePart>): string {
120
223
  // The streamed JSON of a completed structured-output part is the source of
121
224
  // truth for multi-turn coherence — emitting it back as assistant content
@@ -171,16 +274,31 @@ function collectUserContent(
171
274
  return out
172
275
  }
173
276
 
277
+ function thoughtSignatureFromMetadata(metadata: unknown): string | undefined {
278
+ if (
279
+ metadata == null ||
280
+ typeof metadata !== 'object' ||
281
+ Array.isArray(metadata)
282
+ ) {
283
+ return undefined
284
+ }
285
+ if (!('thoughtSignature' in metadata)) return undefined
286
+ const value = metadata.thoughtSignature
287
+ return typeof value === 'string' && value !== '' ? value : undefined
288
+ }
289
+
174
290
  function collectToolCalls(
175
291
  parts: ReadonlyArray<MessagePart>,
176
292
  ): Array<AGUIToolCallMirror> | undefined {
177
293
  const calls: Array<AGUIToolCallMirror> = []
178
294
  for (const p of parts) {
179
295
  if (p.type === 'tool-call') {
296
+ const encryptedValue = thoughtSignatureFromMetadata(p.metadata)
180
297
  calls.push({
181
298
  id: p.id,
182
299
  type: 'function',
183
300
  function: { name: p.name, arguments: p.arguments },
301
+ ...(encryptedValue !== undefined ? { encryptedValue } : {}),
184
302
  })
185
303
  }
186
304
  }