@tanstack/ai 0.58.0 → 0.61.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/adapter.d.ts +9 -0
- package/dist/esm/activities/chat/adapter.js +1 -0
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/agents/define-agent.d.ts +81 -0
- package/dist/esm/activities/chat/agents/define-agent.js +34 -0
- package/dist/esm/activities/chat/agents/define-agent.js.map +1 -0
- package/dist/esm/activities/chat/agents/route.d.ts +53 -0
- package/dist/esm/activities/chat/agents/route.js +59 -0
- package/dist/esm/activities/chat/agents/route.js.map +1 -0
- package/dist/esm/activities/chat/agents/spawn.d.ts +124 -0
- package/dist/esm/activities/chat/agents/spawn.js +490 -0
- package/dist/esm/activities/chat/agents/spawn.js.map +1 -0
- package/dist/esm/activities/chat/agents/turn.d.ts +36 -0
- package/dist/esm/activities/chat/agents/turn.js +78 -0
- package/dist/esm/activities/chat/agents/turn.js.map +1 -0
- package/dist/esm/activities/chat/index.d.ts +13 -3
- package/dist/esm/activities/chat/index.js +462 -26
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.d.ts +7 -1
- package/dist/esm/activities/chat/messages.js +99 -19
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/run-store.d.ts +43 -7
- package/dist/esm/activities/chat/middleware/run-store.js +8 -1
- package/dist/esm/activities/chat/middleware/run-store.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +47 -1
- package/dist/esm/activities/chat/middleware/types.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.js +9 -2
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +46 -1
- package/dist/esm/activities/chat/stream/processor.js +294 -18
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +17 -3
- package/dist/esm/activities/chat/tools/tool-calls.js +56 -5
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/embed/adapter.d.ts +7 -0
- package/dist/esm/activities/embed/adapter.js +1 -0
- package/dist/esm/activities/embed/adapter.js.map +1 -1
- package/dist/esm/activities/embed/index.js +2 -0
- package/dist/esm/activities/embed/index.js.map +1 -1
- package/dist/esm/activities/files/adapter.d.ts +97 -0
- package/dist/esm/activities/files/adapter.js +45 -0
- package/dist/esm/activities/files/adapter.js.map +1 -0
- package/dist/esm/activities/files/index.d.ts +66 -0
- package/dist/esm/activities/files/index.js +78 -0
- package/dist/esm/activities/files/index.js.map +1 -0
- package/dist/esm/activities/generateAudio/index.js +1 -1
- package/dist/esm/activities/generateImage/adapter.d.ts +8 -0
- package/dist/esm/activities/generateImage/adapter.js +1 -0
- package/dist/esm/activities/generateImage/adapter.js.map +1 -1
- package/dist/esm/activities/generateImage/index.js +3 -1
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/activities/generateLiveVideo/index.js +1 -1
- package/dist/esm/activities/generateSpeech/index.js +1 -1
- package/dist/esm/activities/generateTranscription/index.js +1 -1
- package/dist/esm/activities/generateVideo/adapter.d.ts +8 -0
- package/dist/esm/activities/generateVideo/adapter.js +1 -0
- package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.js +3 -0
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/generateVoice/index.js +1 -1
- package/dist/esm/activities/generateWorld/adapter.d.ts +4 -2
- package/dist/esm/activities/generateWorld/adapter.js.map +1 -1
- package/dist/esm/activities/generateWorld/index.d.ts +4 -3
- package/dist/esm/activities/generateWorld/index.js +6 -5
- package/dist/esm/activities/generateWorld/index.js.map +1 -1
- package/dist/esm/activities/index.d.ts +9 -3
- package/dist/esm/activities/index.js +17 -13
- package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -0
- package/dist/esm/activities/summarize/chat-stream-summarize.js +8 -8
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/activities/summarize/index.js +1 -1
- package/dist/esm/client.d.ts +7 -36
- package/dist/esm/client.js +5 -37
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +8 -2
- package/dist/esm/index.js +9 -4
- package/dist/esm/middlewares/content-guard.js.map +1 -1
- package/dist/esm/types.d.ts +179 -98
- package/dist/esm/utilities/adapter-yield-chunk.d.ts +5 -1
- package/dist/esm/utilities/ag-ui-usage.d.ts +9 -9
- package/dist/esm/utilities/ag-ui-usage.js +66 -3
- package/dist/esm/utilities/ag-ui-usage.js.map +1 -1
- package/dist/esm/utilities/ag-ui-wire.js +90 -13
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/content-source.d.ts +60 -0
- package/dist/esm/utilities/content-source.js +85 -0
- package/dist/esm/utilities/content-source.js.map +1 -0
- package/dist/esm/utilities/normalize-stream-chunk.js +7 -2
- package/dist/esm/utilities/normalize-stream-chunk.js.map +1 -1
- package/dist/esm/utilities/provider-executed.d.ts +7 -0
- package/dist/esm/utilities/provider-executed.js +10 -1
- package/dist/esm/utilities/provider-executed.js.map +1 -1
- package/dist/esm/utilities/spec-event-keys.js +13 -8
- package/dist/esm/utilities/spec-event-keys.js.map +1 -1
- package/dist/esm/utilities/subagent-wire.d.ts +36 -0
- package/dist/esm/utilities/subagent-wire.js +131 -0
- package/dist/esm/utilities/subagent-wire.js.map +1 -0
- package/dist/esm/utilities/tool-result.d.ts +12 -2
- package/dist/esm/utilities/tool-result.js +23 -3
- package/dist/esm/utilities/tool-result.js.map +1 -1
- package/package.json +3 -3
- package/skills/ai-core/adapter-configuration/SKILL.md +62 -0
- package/skills/ai-core/adapter-configuration/references/grok-adapter.md +1 -1
- package/skills/ai-core/chat-experience/SKILL.md +14 -0
- package/skills/ai-core/media-generation/SKILL.md +10 -2
- package/skills/ai-core/middleware/SKILL.md +7 -4
- package/src/activities/chat/adapter.ts +10 -0
- package/src/activities/chat/agents/define-agent.ts +121 -0
- package/src/activities/chat/agents/route.ts +115 -0
- package/src/activities/chat/agents/spawn.ts +806 -0
- package/src/activities/chat/agents/turn.ts +151 -0
- package/src/activities/chat/index.ts +734 -30
- package/src/activities/chat/messages.ts +137 -16
- package/src/activities/chat/middleware/run-store.ts +56 -7
- package/src/activities/chat/middleware/types.ts +47 -0
- package/src/activities/chat/stream/message-updaters.ts +24 -2
- package/src/activities/chat/stream/processor.ts +452 -30
- package/src/activities/chat/tools/tool-calls.ts +83 -11
- package/src/activities/embed/adapter.ts +7 -0
- package/src/activities/embed/index.ts +5 -0
- package/src/activities/files/adapter.ts +120 -0
- package/src/activities/files/index.ts +113 -0
- package/src/activities/generateImage/adapter.ts +8 -0
- package/src/activities/generateImage/index.ts +4 -0
- package/src/activities/generateVideo/adapter.ts +8 -0
- package/src/activities/generateVideo/index.ts +7 -0
- package/src/activities/generateWorld/adapter.ts +4 -2
- package/src/activities/generateWorld/index.ts +7 -6
- package/src/activities/index.ts +40 -1
- package/src/activities/summarize/chat-stream-summarize.ts +22 -12
- package/src/client.ts +29 -35
- package/src/index.ts +39 -0
- package/src/middlewares/content-guard.ts +7 -5
- package/src/types.ts +226 -103
- package/src/utilities/adapter-yield-chunk.ts +10 -2
- package/src/utilities/ag-ui-usage.test.ts +38 -0
- package/src/utilities/ag-ui-usage.ts +98 -11
- package/src/utilities/ag-ui-wire.ts +134 -16
- package/src/utilities/content-source.ts +138 -0
- package/src/utilities/normalize-stream-chunk.ts +10 -2
- package/src/utilities/provider-executed.ts +13 -0
- package/src/utilities/spec-event-keys.ts +34 -7
- package/src/utilities/subagent-wire.ts +184 -0
- package/src/utilities/tool-result.ts +38 -2
|
@@ -1,15 +1,8 @@
|
|
|
1
|
+
import type { TokenUsage as SpecTokenUsage } from '@ag-ui/core'
|
|
1
2
|
import type { TokenUsage } from '../types'
|
|
2
3
|
|
|
3
4
|
/** AG-UI spec `usage[]` item (provider/model labels + token counts only). */
|
|
4
|
-
export
|
|
5
|
-
provider?: string
|
|
6
|
-
model?: string
|
|
7
|
-
inputTokens?: number
|
|
8
|
-
outputTokens?: number
|
|
9
|
-
totalTokens?: number
|
|
10
|
-
reasoningTokens?: number
|
|
11
|
-
cachedInputTokens?: number
|
|
12
|
-
}
|
|
5
|
+
export type { TokenUsage as SpecTokenUsage } from '@ag-ui/core'
|
|
13
6
|
|
|
14
7
|
export interface ToSpecTokenUsageOptions {
|
|
15
8
|
provider?: string
|
|
@@ -68,11 +61,16 @@ export function toSpecTokenUsage(
|
|
|
68
61
|
if (cachedInputTokens !== undefined) {
|
|
69
62
|
spec.cachedInputTokens = cachedInputTokens
|
|
70
63
|
}
|
|
64
|
+
if (promptTokensDetails?.cacheWriteTokens !== undefined) {
|
|
65
|
+
spec.cacheWriteInputTokens = promptTokensDetails.cacheWriteTokens
|
|
66
|
+
}
|
|
71
67
|
const reasoningTokens = completionTokensDetails?.reasoningTokens
|
|
72
68
|
if (reasoningTokens !== undefined) {
|
|
73
69
|
spec.reasoningTokens = reasoningTokens
|
|
74
70
|
}
|
|
75
71
|
|
|
72
|
+
// cacheWriteTokens stays in the leftover too, so clients built before
|
|
73
|
+
// cacheWriteInputTokens existed still rebuild it.
|
|
76
74
|
const leftoverPrompt = promptTokensDetails
|
|
77
75
|
? definedDetails(withoutKey(promptTokensDetails, 'cachedTokens'))
|
|
78
76
|
: undefined
|
|
@@ -94,6 +92,80 @@ export function toSpecTokenUsage(
|
|
|
94
92
|
}
|
|
95
93
|
}
|
|
96
94
|
|
|
95
|
+
function sumNumbers<T extends object>(
|
|
96
|
+
current: T | undefined,
|
|
97
|
+
next: T | undefined,
|
|
98
|
+
): T | undefined {
|
|
99
|
+
if (!current) return next
|
|
100
|
+
if (!next) return current
|
|
101
|
+
const result = { ...current }
|
|
102
|
+
for (const key of Object.keys(next) as Array<keyof T>) {
|
|
103
|
+
const value = next[key]
|
|
104
|
+
if (typeof value !== 'number') continue
|
|
105
|
+
const previous = current[key]
|
|
106
|
+
result[key] = ((typeof previous === 'number' ? previous : 0) +
|
|
107
|
+
value) as T[keyof T]
|
|
108
|
+
}
|
|
109
|
+
return result
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function sumOptional(current?: number, next?: number): number | undefined {
|
|
113
|
+
if (current === undefined) return next
|
|
114
|
+
if (next === undefined) return current
|
|
115
|
+
return current + next
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Add two usage totals, as for a parent run and its children. Numbers add
|
|
120
|
+
* up. `billed` adds up only in the same unit. `providerUsageDetails` is
|
|
121
|
+
* opaque, so the latest one stays. `@tanstack/ai-persistence` sums per-run
|
|
122
|
+
* usage with the same rules.
|
|
123
|
+
*/
|
|
124
|
+
export function addTokenUsage(
|
|
125
|
+
current: TokenUsage,
|
|
126
|
+
next: TokenUsage,
|
|
127
|
+
): TokenUsage {
|
|
128
|
+
const promptTokensDetails = sumNumbers(
|
|
129
|
+
current.promptTokensDetails,
|
|
130
|
+
next.promptTokensDetails,
|
|
131
|
+
)
|
|
132
|
+
const completionTokensDetails = sumNumbers(
|
|
133
|
+
current.completionTokensDetails,
|
|
134
|
+
next.completionTokensDetails,
|
|
135
|
+
)
|
|
136
|
+
const costDetails = sumNumbers(current.costDetails, next.costDetails)
|
|
137
|
+
const cost = sumOptional(current.cost, next.cost)
|
|
138
|
+
const durationSeconds = sumOptional(
|
|
139
|
+
current.durationSeconds,
|
|
140
|
+
next.durationSeconds,
|
|
141
|
+
)
|
|
142
|
+
const unitsBilled = sumOptional(current.unitsBilled, next.unitsBilled)
|
|
143
|
+
const billed =
|
|
144
|
+
current.billed && next.billed && current.billed.unit === next.billed.unit
|
|
145
|
+
? {
|
|
146
|
+
quantity: current.billed.quantity + next.billed.quantity,
|
|
147
|
+
unit: current.billed.unit,
|
|
148
|
+
}
|
|
149
|
+
: (next.billed ?? current.billed)
|
|
150
|
+
const providerUsageDetails =
|
|
151
|
+
next.providerUsageDetails ?? current.providerUsageDetails
|
|
152
|
+
return {
|
|
153
|
+
...current,
|
|
154
|
+
...next,
|
|
155
|
+
promptTokens: current.promptTokens + next.promptTokens,
|
|
156
|
+
completionTokens: current.completionTokens + next.completionTokens,
|
|
157
|
+
totalTokens: current.totalTokens + next.totalTokens,
|
|
158
|
+
...(promptTokensDetails && { promptTokensDetails }),
|
|
159
|
+
...(completionTokensDetails && { completionTokensDetails }),
|
|
160
|
+
...(cost !== undefined && { cost }),
|
|
161
|
+
...(costDetails && { costDetails }),
|
|
162
|
+
...(durationSeconds !== undefined && { durationSeconds }),
|
|
163
|
+
...(unitsBilled !== undefined && { unitsBilled }),
|
|
164
|
+
...(billed && { billed }),
|
|
165
|
+
...(providerUsageDetails && { providerUsageDetails }),
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
97
169
|
export function rebuildTokenUsage(
|
|
98
170
|
usage: unknown,
|
|
99
171
|
leftover?: TokenUsageLeftover,
|
|
@@ -111,8 +183,20 @@ export function fromSpecTokenUsage(
|
|
|
111
183
|
usage: ReadonlyArray<SpecTokenUsage> | undefined,
|
|
112
184
|
leftover?: TokenUsageLeftover,
|
|
113
185
|
): TokenUsage | undefined {
|
|
114
|
-
const spec = usage?.
|
|
115
|
-
|
|
186
|
+
const spec = usage?.reduce<SpecTokenUsage>((total, entry) => {
|
|
187
|
+
for (const key of [
|
|
188
|
+
'inputTokens',
|
|
189
|
+
'outputTokens',
|
|
190
|
+
'totalTokens',
|
|
191
|
+
'cachedInputTokens',
|
|
192
|
+
'cacheWriteInputTokens',
|
|
193
|
+
'reasoningTokens',
|
|
194
|
+
] as const) {
|
|
195
|
+
if (entry[key] !== undefined) total[key] = (total[key] ?? 0) + entry[key]
|
|
196
|
+
}
|
|
197
|
+
return total
|
|
198
|
+
}, {})
|
|
199
|
+
if ((usage === undefined || usage.length === 0) && leftover == null) {
|
|
116
200
|
return undefined
|
|
117
201
|
}
|
|
118
202
|
|
|
@@ -126,6 +210,9 @@ export function fromSpecTokenUsage(
|
|
|
126
210
|
...(spec?.cachedInputTokens !== undefined
|
|
127
211
|
? { cachedTokens: spec.cachedInputTokens }
|
|
128
212
|
: {}),
|
|
213
|
+
...(spec?.cacheWriteInputTokens !== undefined
|
|
214
|
+
? { cacheWriteTokens: spec.cacheWriteInputTokens }
|
|
215
|
+
: {}),
|
|
129
216
|
...leftoverPromptDetails,
|
|
130
217
|
})
|
|
131
218
|
const completionTokensDetails = definedDetails({
|
|
@@ -12,13 +12,17 @@ import type {
|
|
|
12
12
|
MessagePart,
|
|
13
13
|
ModelMessage,
|
|
14
14
|
StructuredOutputPart,
|
|
15
|
+
SubagentPart,
|
|
15
16
|
TanStackMessageMetadata,
|
|
16
17
|
UIMessage,
|
|
17
18
|
UIResourcePart,
|
|
18
19
|
} from '../types'
|
|
19
20
|
import type { MetadataRecord } from './merge-metadata'
|
|
20
21
|
import { tanstackMetadata } from './merge-metadata'
|
|
22
|
+
import { isProviderExecutedToolCall } from './provider-executed'
|
|
21
23
|
import { normalizeToolResult } from './tool-result'
|
|
24
|
+
import { wireSubagentInfo, wireSubagentRunId } from './subagent-wire'
|
|
25
|
+
import type { SubagentWireInfo } from './subagent-wire'
|
|
22
26
|
import {
|
|
23
27
|
coerceCreatedAt,
|
|
24
28
|
modelMessageToUIMessage,
|
|
@@ -168,9 +172,45 @@ export function uiMessagesToWire(
|
|
|
168
172
|
continue
|
|
169
173
|
}
|
|
170
174
|
|
|
171
|
-
// assistant:
|
|
175
|
+
// assistant: reasoning fan-outs, then anchor, then tool fan-outs.
|
|
176
|
+
//
|
|
177
|
+
// Provider-executed tools (Anthropic web_search / web_fetch) run inside one
|
|
178
|
+
// provider response, and the provider signs every thinking block against
|
|
179
|
+
// the blocks before it. Emitting all reasoning first and a single anchor
|
|
180
|
+
// with the joined text and every tool call turns
|
|
181
|
+
// "thinking, tool, thinking, text, tool" into
|
|
182
|
+
// "thinking, thinking, text, tool, tool", and the provider rejects the next
|
|
183
|
+
// turn ("thinking blocks in the latest assistant message cannot be
|
|
184
|
+
// modified"). Split the message into ordered segments at each thinking
|
|
185
|
+
// part that follows a provider-executed tool call, the same rule
|
|
186
|
+
// buildAssistantMessages applies, so the wire keeps the signed order.
|
|
187
|
+
// Segments after the first get derived anchor ids; strict AG-UI consumers
|
|
188
|
+
// still see plain anchors.
|
|
189
|
+
type Segment = {
|
|
190
|
+
thinking: Array<Extract<MessagePart, { type: 'thinking' }>>
|
|
191
|
+
parts: Array<MessagePart>
|
|
192
|
+
}
|
|
193
|
+
let current: Segment = { thinking: [], parts: [] }
|
|
194
|
+
const segments: Array<Segment> = [current]
|
|
172
195
|
for (const part of parts) {
|
|
173
196
|
if (part.type === 'thinking') {
|
|
197
|
+
if (
|
|
198
|
+
current.parts.some(
|
|
199
|
+
(p) => p.type === 'tool-call' && isProviderExecutedToolCall(p),
|
|
200
|
+
)
|
|
201
|
+
) {
|
|
202
|
+
current = { thinking: [part], parts: [] }
|
|
203
|
+
segments.push(current)
|
|
204
|
+
} else {
|
|
205
|
+
current.thinking.push(part)
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
current.parts.push(part)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
segments.forEach((segment, index) => {
|
|
213
|
+
for (const part of segment.thinking) {
|
|
174
214
|
const reasoning: WireReasoningMessage = {
|
|
175
215
|
role: 'reasoning',
|
|
176
216
|
id: uniqueWireId(deriveReasoningId(uiMessage.id, part), usedWireIds),
|
|
@@ -181,22 +221,29 @@ export function uiMessagesToWire(
|
|
|
181
221
|
}
|
|
182
222
|
wire.push(reasoning)
|
|
183
223
|
}
|
|
184
|
-
}
|
|
185
224
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
225
|
+
const text = collectText(segment.parts)
|
|
226
|
+
const toolCalls = collectToolCalls(segment.parts)
|
|
227
|
+
const anchorMessage: UIMessage =
|
|
228
|
+
index === 0
|
|
229
|
+
? uiMessage
|
|
230
|
+
: {
|
|
231
|
+
...uiMessage,
|
|
232
|
+
id: uniqueWireId(`${uiMessage.id}-segment-${index}`, usedWireIds),
|
|
233
|
+
}
|
|
234
|
+
wire.push(
|
|
235
|
+
toAnchor(
|
|
236
|
+
anchorMessage,
|
|
237
|
+
'assistant',
|
|
238
|
+
{
|
|
239
|
+
...(text !== '' && { content: text }),
|
|
240
|
+
...(toolCalls && { toolCalls }),
|
|
241
|
+
},
|
|
242
|
+
segment.parts,
|
|
243
|
+
includeSnapshotStructuredOutput,
|
|
244
|
+
),
|
|
245
|
+
)
|
|
246
|
+
})
|
|
200
247
|
|
|
201
248
|
const explicitToolResults = new Set(
|
|
202
249
|
parts.flatMap((part) =>
|
|
@@ -263,6 +310,10 @@ export function uiMessagesToWire(
|
|
|
263
310
|
})
|
|
264
311
|
}
|
|
265
312
|
}
|
|
313
|
+
|
|
314
|
+
for (const part of parts) {
|
|
315
|
+
if (part.type === 'subagent') wire.push(...subagentToWire(part, options))
|
|
316
|
+
}
|
|
266
317
|
}
|
|
267
318
|
|
|
268
319
|
return wire
|
|
@@ -332,6 +383,8 @@ function messageMetadata(
|
|
|
332
383
|
const tanstack: TanStackMessageMetadata = {}
|
|
333
384
|
if (previousTanstack?.model !== undefined)
|
|
334
385
|
tanstack.model = previousTanstack.model
|
|
386
|
+
if (previousTanstack?.runId !== undefined)
|
|
387
|
+
tanstack.runId = previousTanstack.runId
|
|
335
388
|
if (previousTanstack?.signature !== undefined)
|
|
336
389
|
tanstack.signature = previousTanstack.signature
|
|
337
390
|
const createdAt = coerceCreatedAt(msg.createdAt)
|
|
@@ -424,6 +477,71 @@ function collectText(parts: ReadonlyArray<MessagePart>): string {
|
|
|
424
477
|
return out.join('')
|
|
425
478
|
}
|
|
426
479
|
|
|
480
|
+
/**
|
|
481
|
+
* A child's messages, tagged with its AG-UI `subagentRunId`. The card data
|
|
482
|
+
* rides in `metadata.tanstack.subagent`, so the other side can rebuild the
|
|
483
|
+
* card and continue a suspended child.
|
|
484
|
+
*/
|
|
485
|
+
function subagentToWire(
|
|
486
|
+
part: SubagentPart,
|
|
487
|
+
options?: { includeSnapshotStructuredOutput: boolean },
|
|
488
|
+
): Array<WireMessage> {
|
|
489
|
+
const { subagent } = part
|
|
490
|
+
const info: SubagentWireInfo = {
|
|
491
|
+
name: subagent.name,
|
|
492
|
+
status: subagent.status,
|
|
493
|
+
...(subagent.description !== undefined && {
|
|
494
|
+
description: subagent.description,
|
|
495
|
+
}),
|
|
496
|
+
...(subagent.error !== undefined && { error: subagent.error }),
|
|
497
|
+
...(subagent.interruptIds !== undefined && {
|
|
498
|
+
interruptIds: subagent.interruptIds,
|
|
499
|
+
}),
|
|
500
|
+
...(subagent.parentSubagentRunId !== undefined && {
|
|
501
|
+
parentSubagentRunId: subagent.parentSubagentRunId,
|
|
502
|
+
}),
|
|
503
|
+
...(subagent.parentToolCallId !== undefined && {
|
|
504
|
+
parentToolCallId: subagent.parentToolCallId,
|
|
505
|
+
}),
|
|
506
|
+
...(subagent.metadata !== undefined && { metadata: subagent.metadata }),
|
|
507
|
+
}
|
|
508
|
+
const child = uiMessagesToWire(subagent.messages, options)
|
|
509
|
+
const own = child.some((message) => wireSubagentRunId(message) === undefined)
|
|
510
|
+
const messages: Array<WireMessage> = own
|
|
511
|
+
? child
|
|
512
|
+
: [{ id: `subagent:${subagent.id}`, role: 'assistant' }, ...child]
|
|
513
|
+
return messages.map((message, index) => {
|
|
514
|
+
const nestedId = wireSubagentRunId(message)
|
|
515
|
+
if (nestedId !== undefined) {
|
|
516
|
+
const nested = wireSubagentInfo(message)
|
|
517
|
+
if (!nested || nested.parentSubagentRunId !== undefined) return message
|
|
518
|
+
return withSubagentInfo(message, nestedId, {
|
|
519
|
+
...nested,
|
|
520
|
+
parentSubagentRunId: subagent.id,
|
|
521
|
+
})
|
|
522
|
+
}
|
|
523
|
+
return withSubagentInfo(
|
|
524
|
+
message,
|
|
525
|
+
subagent.id,
|
|
526
|
+
!own && index === 0 ? { ...info, placeholder: true } : info,
|
|
527
|
+
)
|
|
528
|
+
})
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function withSubagentInfo(
|
|
532
|
+
message: WireMessage,
|
|
533
|
+
subagentRunId: string,
|
|
534
|
+
info: SubagentWireInfo,
|
|
535
|
+
): WireMessage {
|
|
536
|
+
const metadata = isRecord(message.metadata) ? message.metadata : {}
|
|
537
|
+
const tanstack = isRecord(metadata.tanstack) ? metadata.tanstack : {}
|
|
538
|
+
return {
|
|
539
|
+
...message,
|
|
540
|
+
subagentRunId,
|
|
541
|
+
metadata: { ...metadata, tanstack: { ...tanstack, subagent: info } },
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
427
545
|
function collectUserContent(
|
|
428
546
|
parts: ReadonlyArray<MessagePart>,
|
|
429
547
|
): string | Array<InputContent> {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { ContentPartFileSource, ContentPartSource } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Narrow a {@link ContentPartSource} to the provider-file-reference arm.
|
|
5
|
+
*
|
|
6
|
+
* Issuer adapters use this to route a file source to their native wire field;
|
|
7
|
+
* everyone else is protected by the core preflight (see
|
|
8
|
+
* {@link assertMessagesFileSourceSupport}) plus a defensive throw at their own
|
|
9
|
+
* mapping site.
|
|
10
|
+
*/
|
|
11
|
+
export function isFileSource(
|
|
12
|
+
source: ContentPartSource,
|
|
13
|
+
): source is ContentPartFileSource {
|
|
14
|
+
return source.type === 'file'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the handle `providerName` should send for a file source.
|
|
19
|
+
*
|
|
20
|
+
* A file source carries one opaque handle (`value`) and, optionally, the
|
|
21
|
+
* provider that issued it. An adapter always knows which provider it talks
|
|
22
|
+
* to, so a source that names no provider is taken as-is.
|
|
23
|
+
*
|
|
24
|
+
* @throws when the source names a different issuing provider. A handle only
|
|
25
|
+
* resolves at the provider that minted it.
|
|
26
|
+
*/
|
|
27
|
+
export function fileReferenceFor(
|
|
28
|
+
source: ContentPartFileSource,
|
|
29
|
+
providerName: string,
|
|
30
|
+
): string {
|
|
31
|
+
if (source.provider !== undefined && source.provider !== providerName) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`${providerName}: file source was issued by ${source.provider}. ` +
|
|
34
|
+
`A provider file handle only works with the provider that issued ` +
|
|
35
|
+
`it. Upload the file with ${providerName}Files(), or pass a data or ` +
|
|
36
|
+
`url source instead.`,
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
return source.value
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build the standard error a non-issuer adapter throws when it encounters a
|
|
44
|
+
* `{ type: 'file' }` source it can't consume — either because the provider has
|
|
45
|
+
* no file-reference input surface, or because the endpoint requires raw bytes
|
|
46
|
+
* (image edits, Veo) rather than a reference.
|
|
47
|
+
*
|
|
48
|
+
* @param detail Optional context appended to the message (e.g. a modality or
|
|
49
|
+
* endpoint name, or a pointer to the adapter that does support references).
|
|
50
|
+
* When provided it replaces the generic remediation tail, so a site-specific
|
|
51
|
+
* hint ("pass inline bytes") is never contradicted by generic advice.
|
|
52
|
+
*/
|
|
53
|
+
export function unsupportedFileSourceError(
|
|
54
|
+
providerName: string,
|
|
55
|
+
detail?: string,
|
|
56
|
+
): Error {
|
|
57
|
+
return new Error(
|
|
58
|
+
`${providerName} does not support provider file-handle sources ` +
|
|
59
|
+
`({ type: 'file' })` +
|
|
60
|
+
(detail
|
|
61
|
+
? ` ${detail}.`
|
|
62
|
+
: `. Pass a data or url source, or upload via the provider's files ` +
|
|
63
|
+
`adapter where supported.`),
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The slice of an adapter the file-source preflight reads. Adapters that can
|
|
69
|
+
* consume `{ type: 'file' }` sources declare `supportsFileSources: true`;
|
|
70
|
+
* everything else — including adapters written before this arm existed —
|
|
71
|
+
* fails closed at the activity layer instead of falling through to a
|
|
72
|
+
* URL/data branch and silently mis-mapping the reference.
|
|
73
|
+
*/
|
|
74
|
+
export interface FileSourceCapable {
|
|
75
|
+
name: string
|
|
76
|
+
supportsFileSources?: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** True when a content-part-like value carries a `{ type: 'file' }` source. */
|
|
80
|
+
function partHasFileSource(part: unknown): boolean {
|
|
81
|
+
if (typeof part !== 'object' || part === null) return false
|
|
82
|
+
const source = (part as { source?: unknown }).source
|
|
83
|
+
return (
|
|
84
|
+
typeof source === 'object' &&
|
|
85
|
+
source !== null &&
|
|
86
|
+
(source as { type?: unknown }).type === 'file'
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* True when `value` is a content part with a file source, or an array
|
|
92
|
+
* (possibly nested — fused embedding items) that contains one.
|
|
93
|
+
*/
|
|
94
|
+
function inputHasFileSource(value: unknown): boolean {
|
|
95
|
+
if (Array.isArray(value)) return value.some(inputHasFileSource)
|
|
96
|
+
return partHasFileSource(value)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Fail-closed preflight for media prompts and embedding inputs
|
|
101
|
+
* (`generateImage` / `generateVideo` / `embed`): throws when the input
|
|
102
|
+
* carries a `{ type: 'file' }` source and the adapter hasn't declared
|
|
103
|
+
* `supportsFileSources`. Runs in the activity dispatcher — the same layer
|
|
104
|
+
* that validates modality — so an adapter that predates the file arm can
|
|
105
|
+
* never receive one. Walks a single part, an array of parts, and nested
|
|
106
|
+
* arrays (fused embedding items).
|
|
107
|
+
*/
|
|
108
|
+
export function assertPromptFileSourceSupport(
|
|
109
|
+
adapter: FileSourceCapable,
|
|
110
|
+
prompt: unknown,
|
|
111
|
+
): void {
|
|
112
|
+
if (adapter.supportsFileSources === true) return
|
|
113
|
+
if (inputHasFileSource(prompt)) {
|
|
114
|
+
throw unsupportedFileSourceError(adapter.name)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Fail-closed preflight for chat messages: throws when any message content
|
|
120
|
+
* part carries a `{ type: 'file' }` source and the adapter hasn't declared
|
|
121
|
+
* `supportsFileSources`. See {@link assertPromptFileSourceSupport}.
|
|
122
|
+
*/
|
|
123
|
+
export function assertMessagesFileSourceSupport(
|
|
124
|
+
adapter: FileSourceCapable,
|
|
125
|
+
messages: ReadonlyArray<unknown>,
|
|
126
|
+
): void {
|
|
127
|
+
if (adapter.supportsFileSources === true) return
|
|
128
|
+
for (const message of messages) {
|
|
129
|
+
if (typeof message !== 'object' || message === null) continue
|
|
130
|
+
const content = (message as { content?: unknown }).content
|
|
131
|
+
if (!Array.isArray(content)) continue
|
|
132
|
+
for (const part of content) {
|
|
133
|
+
if (partHasFileSource(part)) {
|
|
134
|
+
throw unsupportedFileSourceError(adapter.name)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -11,6 +11,14 @@ function stringField(value: unknown): string | undefined {
|
|
|
11
11
|
return typeof value === 'string' && value !== '' ? value : undefined
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function chunkFields(chunk: object) {
|
|
15
|
+
const fields: Record<string, unknown> = {}
|
|
16
|
+
for (const key of Object.keys(chunk)) {
|
|
17
|
+
fields[key] = Reflect.get(chunk, key)
|
|
18
|
+
}
|
|
19
|
+
return fields
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
function encryptedValueExtras(chunk: AdapterYieldChunk): Array<StreamChunk> {
|
|
15
23
|
const extras: Array<StreamChunk> = []
|
|
16
24
|
const timestamp =
|
|
@@ -19,7 +27,7 @@ function encryptedValueExtras(chunk: AdapterYieldChunk): Array<StreamChunk> {
|
|
|
19
27
|
: undefined
|
|
20
28
|
|
|
21
29
|
if (typeof chunk.signature === 'string' && chunk.signature !== '') {
|
|
22
|
-
const source = chunk
|
|
30
|
+
const source = chunkFields(chunk)
|
|
23
31
|
const toolCallId = stringField(source.toolCallId)
|
|
24
32
|
const entityId =
|
|
25
33
|
stringField(chunk.stepId) ??
|
|
@@ -62,7 +70,7 @@ export function normalizeStreamChunk(
|
|
|
62
70
|
chunk: AdapterYieldChunk,
|
|
63
71
|
): Array<StreamChunk> {
|
|
64
72
|
const specKeys = specKeysFor(chunk.type)
|
|
65
|
-
const source = chunk
|
|
73
|
+
const source = chunkFields(chunk)
|
|
66
74
|
const specChunk: Record<string, unknown> & {
|
|
67
75
|
metadata?: MetadataRecord | null
|
|
68
76
|
} = {}
|
|
@@ -30,3 +30,16 @@ export function isProviderExecutedToolCall(
|
|
|
30
30
|
): boolean {
|
|
31
31
|
return getProviderExecutedMetadata(toolCall) !== null
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* True when `id` is a `${parentId}-segment-${n}` message. The wire and the
|
|
36
|
+
* run loop split one provider turn into segments at thinking that follows a
|
|
37
|
+
* provider-executed tool call, to keep signed thinking order. Readers fold a
|
|
38
|
+
* segment back into its parent so the UI still shows one message.
|
|
39
|
+
*/
|
|
40
|
+
export function isAssistantSegmentOf(
|
|
41
|
+
id: string | undefined,
|
|
42
|
+
parentId: string,
|
|
43
|
+
): boolean {
|
|
44
|
+
return id?.startsWith(`${parentId}-segment-`) === true
|
|
45
|
+
}
|
|
@@ -30,11 +30,6 @@ const SPEC_KEYS: ReadonlyMap<string, ReadonlySet<string>> = new Map([
|
|
|
30
30
|
EventType.TOOL_CALL_RESULT,
|
|
31
31
|
keys('messageId', 'toolCallId', 'content', 'role'),
|
|
32
32
|
],
|
|
33
|
-
[EventType.THINKING_START, keys('title')],
|
|
34
|
-
[EventType.THINKING_END, SHARED],
|
|
35
|
-
[EventType.THINKING_TEXT_MESSAGE_START, SHARED],
|
|
36
|
-
[EventType.THINKING_TEXT_MESSAGE_CONTENT, keys('delta')],
|
|
37
|
-
[EventType.THINKING_TEXT_MESSAGE_END, SHARED],
|
|
38
33
|
[EventType.STATE_SNAPSHOT, keys('snapshot')],
|
|
39
34
|
[EventType.STATE_DELTA, keys('delta')],
|
|
40
35
|
[EventType.MESSAGES_SNAPSHOT, keys('messages')],
|
|
@@ -45,7 +40,10 @@ const SPEC_KEYS: ReadonlyMap<string, ReadonlySet<string>> = new Map([
|
|
|
45
40
|
[EventType.ACTIVITY_DELTA, keys('messageId', 'activityType', 'patch')],
|
|
46
41
|
[EventType.RAW, keys('event', 'source')],
|
|
47
42
|
[EventType.CUSTOM, keys('name', 'value')],
|
|
48
|
-
[
|
|
43
|
+
[
|
|
44
|
+
EventType.RUN_STARTED,
|
|
45
|
+
keys('threadId', 'runId', 'protocolVersion', 'parentRunId', 'input'),
|
|
46
|
+
],
|
|
49
47
|
[
|
|
50
48
|
EventType.RUN_FINISHED,
|
|
51
49
|
keys('threadId', 'runId', 'result', 'outcome', 'usage'),
|
|
@@ -63,10 +61,39 @@ const SPEC_KEYS: ReadonlyMap<string, ReadonlySet<string>> = new Map([
|
|
|
63
61
|
EventType.REASONING_ENCRYPTED_VALUE,
|
|
64
62
|
keys('subtype', 'entityId', 'encryptedValue'),
|
|
65
63
|
],
|
|
64
|
+
[
|
|
65
|
+
EventType.SUBAGENT_STARTED,
|
|
66
|
+
keys(
|
|
67
|
+
'subagentRunId',
|
|
68
|
+
'name',
|
|
69
|
+
'description',
|
|
70
|
+
'parentSubagentRunId',
|
|
71
|
+
'parentToolCallId',
|
|
72
|
+
'parentMessageId',
|
|
73
|
+
),
|
|
74
|
+
],
|
|
75
|
+
[EventType.SUBAGENT_FINISHED, keys('subagentRunId', 'result', 'outcome')],
|
|
76
|
+
[EventType.SUBAGENT_ERROR, keys('subagentRunId', 'message', 'code')],
|
|
66
77
|
])
|
|
67
78
|
|
|
79
|
+
const RUN_SCOPED = new Set<string>([
|
|
80
|
+
EventType.RUN_STARTED,
|
|
81
|
+
EventType.RUN_FINISHED,
|
|
82
|
+
EventType.RUN_ERROR,
|
|
83
|
+
EventType.MESSAGES_SNAPSHOT,
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
const ATTRIBUTABLE_KEYS = new Map(
|
|
87
|
+
[...SPEC_KEYS]
|
|
88
|
+
.filter(([type]) => !RUN_SCOPED.has(type))
|
|
89
|
+
.map(
|
|
90
|
+
([type, fields]) =>
|
|
91
|
+
[type, new Set([...fields, 'subagentRunId'])] as const,
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
|
|
68
95
|
export function specKeysFor(type: string): ReadonlySet<string> {
|
|
69
|
-
return SPEC_KEYS.get(type) ?? SHARED
|
|
96
|
+
return ATTRIBUTABLE_KEYS.get(type) ?? SPEC_KEYS.get(type) ?? SHARED
|
|
70
97
|
}
|
|
71
98
|
|
|
72
99
|
export function isSpecTopLevelKey(type: string, key: string): boolean {
|