@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.
- package/README.md +2 -1
- package/dist/esm/activities/chat/adapter.d.ts +5 -4
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/index.d.ts +4 -7
- package/dist/esm/activities/chat/index.js +201 -243
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +115 -25
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +38 -17
- package/dist/esm/activities/chat/stream/processor.js +186 -101
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +3 -2
- package/dist/esm/activities/chat/tools/tool-calls.js +15 -10
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.js +6 -6
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/stream-generation-result.js +7 -8
- package/dist/esm/activities/stream-generation-result.js.map +1 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.js +56 -53
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/adapter-internals.d.ts +2 -0
- package/dist/esm/adapter-internals.js +3 -1
- package/dist/esm/client.d.ts +8 -1
- package/dist/esm/client.js +7 -2
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.js +6 -2
- package/dist/esm/middlewares/otel.js +11 -4
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/stream-to-response.js +8 -5
- package/dist/esm/stream-to-response.js.map +1 -1
- package/dist/esm/stream-to-websocket.js +4 -2
- package/dist/esm/stream-to-websocket.js.map +1 -1
- package/dist/esm/strip-to-spec-middleware.d.ts +10 -13
- package/dist/esm/strip-to-spec-middleware.js +24 -22
- package/dist/esm/strip-to-spec-middleware.js.map +1 -1
- package/dist/esm/types.d.ts +82 -305
- package/dist/esm/utilities/adapter-yield-chunk.d.ts +31 -0
- package/dist/esm/utilities/ag-ui-usage.d.ts +24 -0
- package/dist/esm/utilities/ag-ui-usage.js +66 -0
- package/dist/esm/utilities/ag-ui-usage.js.map +1 -0
- package/dist/esm/utilities/ag-ui-wire.d.ts +14 -7
- package/dist/esm/utilities/ag-ui-wire.js +71 -30
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/chat-params.d.ts +3 -3
- package/dist/esm/utilities/chat-params.js +10 -30
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/dist/esm/utilities/chunk-ids.d.ts +5 -0
- package/dist/esm/utilities/chunk-ids.js +25 -0
- package/dist/esm/utilities/chunk-ids.js.map +1 -0
- package/dist/esm/utilities/merge-metadata.d.ts +14 -0
- package/dist/esm/utilities/merge-metadata.js +43 -0
- package/dist/esm/utilities/merge-metadata.js.map +1 -0
- package/dist/esm/utilities/normalize-stream-chunk.d.ts +3 -0
- package/dist/esm/utilities/normalize-stream-chunk.js +100 -0
- package/dist/esm/utilities/normalize-stream-chunk.js.map +1 -0
- package/dist/esm/utilities/reasoning-encrypted-value.d.ts +8 -0
- package/dist/esm/utilities/reasoning-encrypted-value.js +16 -0
- package/dist/esm/utilities/reasoning-encrypted-value.js.map +1 -0
- package/dist/esm/utilities/restore-inbound-chunk.d.ts +15 -0
- package/dist/esm/utilities/restore-inbound-chunk.js +43 -0
- package/dist/esm/utilities/restore-inbound-chunk.js.map +1 -0
- package/dist/esm/utilities/spec-event-keys.d.ts +2 -0
- package/dist/esm/utilities/spec-event-keys.js +56 -0
- package/dist/esm/utilities/spec-event-keys.js.map +1 -0
- package/dist/esm/utilities/structured-output-events.d.ts +3 -3
- package/dist/esm/utilities/structured-output-events.js.map +1 -1
- package/package.json +2 -2
- package/src/activities/chat/adapter.ts +4 -4
- package/src/activities/chat/index.ts +398 -400
- package/src/activities/chat/messages.ts +174 -34
- package/src/activities/chat/stream/processor.ts +289 -218
- package/src/activities/chat/tools/tool-calls.ts +23 -22
- package/src/activities/generateVideo/index.ts +7 -6
- package/src/activities/stream-generation-result.ts +8 -12
- package/src/activities/summarize/chat-stream-summarize.ts +94 -70
- package/src/adapter-internals.ts +2 -0
- package/src/client.ts +13 -0
- package/src/index.ts +6 -0
- package/src/middlewares/otel.ts +16 -8
- package/src/stream-to-response.ts +11 -4
- package/src/stream-to-websocket.ts +3 -1
- package/src/strip-to-spec-middleware.ts +48 -24
- package/src/types.ts +109 -393
- package/src/utilities/adapter-yield-chunk.ts +30 -0
- package/src/utilities/ag-ui-usage.test.ts +194 -0
- package/src/utilities/ag-ui-usage.ts +148 -0
- package/src/utilities/ag-ui-wire.ts +149 -31
- package/src/utilities/chat-params.ts +22 -39
- package/src/utilities/chunk-ids.ts +24 -0
- package/src/utilities/merge-metadata.test.ts +117 -0
- package/src/utilities/merge-metadata.ts +59 -0
- package/src/utilities/normalize-stream-chunk.test.ts +423 -0
- package/src/utilities/normalize-stream-chunk.ts +186 -0
- package/src/utilities/reasoning-encrypted-value.ts +18 -0
- package/src/utilities/restore-inbound-chunk.test.ts +133 -0
- package/src/utilities/restore-inbound-chunk.ts +72 -0
- package/src/utilities/spec-event-keys.test.ts +34 -0
- package/src/utilities/spec-event-keys.ts +74 -0
- 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 {
|
|
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
|
-
|
|
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.
|
|
41
|
-
* `
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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:
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
}
|