@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.
Files changed (144) hide show
  1. package/dist/esm/activities/chat/adapter.d.ts +9 -0
  2. package/dist/esm/activities/chat/adapter.js +1 -0
  3. package/dist/esm/activities/chat/adapter.js.map +1 -1
  4. package/dist/esm/activities/chat/agents/define-agent.d.ts +81 -0
  5. package/dist/esm/activities/chat/agents/define-agent.js +34 -0
  6. package/dist/esm/activities/chat/agents/define-agent.js.map +1 -0
  7. package/dist/esm/activities/chat/agents/route.d.ts +53 -0
  8. package/dist/esm/activities/chat/agents/route.js +59 -0
  9. package/dist/esm/activities/chat/agents/route.js.map +1 -0
  10. package/dist/esm/activities/chat/agents/spawn.d.ts +124 -0
  11. package/dist/esm/activities/chat/agents/spawn.js +490 -0
  12. package/dist/esm/activities/chat/agents/spawn.js.map +1 -0
  13. package/dist/esm/activities/chat/agents/turn.d.ts +36 -0
  14. package/dist/esm/activities/chat/agents/turn.js +78 -0
  15. package/dist/esm/activities/chat/agents/turn.js.map +1 -0
  16. package/dist/esm/activities/chat/index.d.ts +13 -3
  17. package/dist/esm/activities/chat/index.js +462 -26
  18. package/dist/esm/activities/chat/index.js.map +1 -1
  19. package/dist/esm/activities/chat/messages.d.ts +7 -1
  20. package/dist/esm/activities/chat/messages.js +99 -19
  21. package/dist/esm/activities/chat/messages.js.map +1 -1
  22. package/dist/esm/activities/chat/middleware/run-store.d.ts +43 -7
  23. package/dist/esm/activities/chat/middleware/run-store.js +8 -1
  24. package/dist/esm/activities/chat/middleware/run-store.js.map +1 -1
  25. package/dist/esm/activities/chat/middleware/types.d.ts +47 -1
  26. package/dist/esm/activities/chat/middleware/types.js.map +1 -1
  27. package/dist/esm/activities/chat/stream/message-updaters.js +9 -2
  28. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  29. package/dist/esm/activities/chat/stream/processor.d.ts +46 -1
  30. package/dist/esm/activities/chat/stream/processor.js +294 -18
  31. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  32. package/dist/esm/activities/chat/tools/tool-calls.d.ts +17 -3
  33. package/dist/esm/activities/chat/tools/tool-calls.js +56 -5
  34. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  35. package/dist/esm/activities/embed/adapter.d.ts +7 -0
  36. package/dist/esm/activities/embed/adapter.js +1 -0
  37. package/dist/esm/activities/embed/adapter.js.map +1 -1
  38. package/dist/esm/activities/embed/index.js +2 -0
  39. package/dist/esm/activities/embed/index.js.map +1 -1
  40. package/dist/esm/activities/files/adapter.d.ts +97 -0
  41. package/dist/esm/activities/files/adapter.js +45 -0
  42. package/dist/esm/activities/files/adapter.js.map +1 -0
  43. package/dist/esm/activities/files/index.d.ts +66 -0
  44. package/dist/esm/activities/files/index.js +78 -0
  45. package/dist/esm/activities/files/index.js.map +1 -0
  46. package/dist/esm/activities/generateAudio/index.js +1 -1
  47. package/dist/esm/activities/generateImage/adapter.d.ts +8 -0
  48. package/dist/esm/activities/generateImage/adapter.js +1 -0
  49. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  50. package/dist/esm/activities/generateImage/index.js +3 -1
  51. package/dist/esm/activities/generateImage/index.js.map +1 -1
  52. package/dist/esm/activities/generateLiveVideo/index.js +1 -1
  53. package/dist/esm/activities/generateSpeech/index.js +1 -1
  54. package/dist/esm/activities/generateTranscription/index.js +1 -1
  55. package/dist/esm/activities/generateVideo/adapter.d.ts +8 -0
  56. package/dist/esm/activities/generateVideo/adapter.js +1 -0
  57. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  58. package/dist/esm/activities/generateVideo/index.js +3 -0
  59. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  60. package/dist/esm/activities/generateVoice/index.js +1 -1
  61. package/dist/esm/activities/generateWorld/adapter.d.ts +4 -2
  62. package/dist/esm/activities/generateWorld/adapter.js.map +1 -1
  63. package/dist/esm/activities/generateWorld/index.d.ts +4 -3
  64. package/dist/esm/activities/generateWorld/index.js +6 -5
  65. package/dist/esm/activities/generateWorld/index.js.map +1 -1
  66. package/dist/esm/activities/index.d.ts +9 -3
  67. package/dist/esm/activities/index.js +17 -13
  68. package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -0
  69. package/dist/esm/activities/summarize/chat-stream-summarize.js +8 -8
  70. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  71. package/dist/esm/activities/summarize/index.js +1 -1
  72. package/dist/esm/client.d.ts +7 -36
  73. package/dist/esm/client.js +5 -37
  74. package/dist/esm/client.js.map +1 -1
  75. package/dist/esm/index.d.ts +8 -2
  76. package/dist/esm/index.js +9 -4
  77. package/dist/esm/middlewares/content-guard.js.map +1 -1
  78. package/dist/esm/types.d.ts +179 -98
  79. package/dist/esm/utilities/adapter-yield-chunk.d.ts +5 -1
  80. package/dist/esm/utilities/ag-ui-usage.d.ts +9 -9
  81. package/dist/esm/utilities/ag-ui-usage.js +66 -3
  82. package/dist/esm/utilities/ag-ui-usage.js.map +1 -1
  83. package/dist/esm/utilities/ag-ui-wire.js +90 -13
  84. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  85. package/dist/esm/utilities/content-source.d.ts +60 -0
  86. package/dist/esm/utilities/content-source.js +85 -0
  87. package/dist/esm/utilities/content-source.js.map +1 -0
  88. package/dist/esm/utilities/normalize-stream-chunk.js +7 -2
  89. package/dist/esm/utilities/normalize-stream-chunk.js.map +1 -1
  90. package/dist/esm/utilities/provider-executed.d.ts +7 -0
  91. package/dist/esm/utilities/provider-executed.js +10 -1
  92. package/dist/esm/utilities/provider-executed.js.map +1 -1
  93. package/dist/esm/utilities/spec-event-keys.js +13 -8
  94. package/dist/esm/utilities/spec-event-keys.js.map +1 -1
  95. package/dist/esm/utilities/subagent-wire.d.ts +36 -0
  96. package/dist/esm/utilities/subagent-wire.js +131 -0
  97. package/dist/esm/utilities/subagent-wire.js.map +1 -0
  98. package/dist/esm/utilities/tool-result.d.ts +12 -2
  99. package/dist/esm/utilities/tool-result.js +23 -3
  100. package/dist/esm/utilities/tool-result.js.map +1 -1
  101. package/package.json +3 -3
  102. package/skills/ai-core/adapter-configuration/SKILL.md +62 -0
  103. package/skills/ai-core/adapter-configuration/references/grok-adapter.md +1 -1
  104. package/skills/ai-core/chat-experience/SKILL.md +14 -0
  105. package/skills/ai-core/media-generation/SKILL.md +10 -2
  106. package/skills/ai-core/middleware/SKILL.md +7 -4
  107. package/src/activities/chat/adapter.ts +10 -0
  108. package/src/activities/chat/agents/define-agent.ts +121 -0
  109. package/src/activities/chat/agents/route.ts +115 -0
  110. package/src/activities/chat/agents/spawn.ts +806 -0
  111. package/src/activities/chat/agents/turn.ts +151 -0
  112. package/src/activities/chat/index.ts +734 -30
  113. package/src/activities/chat/messages.ts +137 -16
  114. package/src/activities/chat/middleware/run-store.ts +56 -7
  115. package/src/activities/chat/middleware/types.ts +47 -0
  116. package/src/activities/chat/stream/message-updaters.ts +24 -2
  117. package/src/activities/chat/stream/processor.ts +452 -30
  118. package/src/activities/chat/tools/tool-calls.ts +83 -11
  119. package/src/activities/embed/adapter.ts +7 -0
  120. package/src/activities/embed/index.ts +5 -0
  121. package/src/activities/files/adapter.ts +120 -0
  122. package/src/activities/files/index.ts +113 -0
  123. package/src/activities/generateImage/adapter.ts +8 -0
  124. package/src/activities/generateImage/index.ts +4 -0
  125. package/src/activities/generateVideo/adapter.ts +8 -0
  126. package/src/activities/generateVideo/index.ts +7 -0
  127. package/src/activities/generateWorld/adapter.ts +4 -2
  128. package/src/activities/generateWorld/index.ts +7 -6
  129. package/src/activities/index.ts +40 -1
  130. package/src/activities/summarize/chat-stream-summarize.ts +22 -12
  131. package/src/client.ts +29 -35
  132. package/src/index.ts +39 -0
  133. package/src/middlewares/content-guard.ts +7 -5
  134. package/src/types.ts +226 -103
  135. package/src/utilities/adapter-yield-chunk.ts +10 -2
  136. package/src/utilities/ag-ui-usage.test.ts +38 -0
  137. package/src/utilities/ag-ui-usage.ts +98 -11
  138. package/src/utilities/ag-ui-wire.ts +134 -16
  139. package/src/utilities/content-source.ts +138 -0
  140. package/src/utilities/normalize-stream-chunk.ts +10 -2
  141. package/src/utilities/provider-executed.ts +13 -0
  142. package/src/utilities/spec-event-keys.ts +34 -7
  143. package/src/utilities/subagent-wire.ts +184 -0
  144. 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 interface SpecTokenUsage {
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?.[0]
115
- if (spec == null && leftover == null) {
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: emit reasoning fan-outs first, then anchor, then tool fan-outs
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
- const text = collectText(parts)
187
- const toolCalls = collectToolCalls(parts)
188
- wire.push(
189
- toAnchor(
190
- uiMessage,
191
- 'assistant',
192
- {
193
- ...(text !== '' && { content: text }),
194
- ...(toolCalls && { toolCalls }),
195
- },
196
- parts,
197
- includeSnapshotStructuredOutput,
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 as Record<string, unknown>
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 as Record<string, unknown>
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
- [EventType.RUN_STARTED, keys('threadId', 'runId', 'parentRunId', 'input')],
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 {