@tanstack/ai 0.59.0 → 0.63.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 (123) hide show
  1. package/README.md +1 -0
  2. package/dist/esm/activities/chat/adapter.d.ts +9 -0
  3. package/dist/esm/activities/chat/adapter.js +1 -0
  4. package/dist/esm/activities/chat/adapter.js.map +1 -1
  5. package/dist/esm/activities/chat/agents/define-agent.d.ts +17 -5
  6. package/dist/esm/activities/chat/agents/define-agent.js.map +1 -1
  7. package/dist/esm/activities/chat/agents/spawn.d.ts +2 -0
  8. package/dist/esm/activities/chat/agents/spawn.js +8 -5
  9. package/dist/esm/activities/chat/agents/spawn.js.map +1 -1
  10. package/dist/esm/activities/chat/index.js +289 -78
  11. package/dist/esm/activities/chat/index.js.map +1 -1
  12. package/dist/esm/activities/chat/messages.js +35 -19
  13. package/dist/esm/activities/chat/messages.js.map +1 -1
  14. package/dist/esm/activities/chat/middleware/types.d.ts +1 -0
  15. package/dist/esm/activities/chat/middleware/types.js.map +1 -1
  16. package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
  17. package/dist/esm/activities/chat/stream/message-updaters.js +11 -3
  18. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  19. package/dist/esm/activities/chat/stream/processor.d.ts +11 -10
  20. package/dist/esm/activities/chat/stream/processor.js +51 -28
  21. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  22. package/dist/esm/activities/chat/tools/tool-calls.d.ts +16 -2
  23. package/dist/esm/activities/chat/tools/tool-calls.js +57 -16
  24. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  25. package/dist/esm/activities/chat/tools/tool-definition.d.ts +4 -0
  26. package/dist/esm/activities/chat/tools/tool-definition.js +4 -0
  27. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  28. package/dist/esm/activities/embed/adapter.d.ts +7 -0
  29. package/dist/esm/activities/embed/adapter.js +1 -0
  30. package/dist/esm/activities/embed/adapter.js.map +1 -1
  31. package/dist/esm/activities/embed/index.js +2 -0
  32. package/dist/esm/activities/embed/index.js.map +1 -1
  33. package/dist/esm/activities/evaluate/adapter.d.ts +4 -0
  34. package/dist/esm/activities/evaluate/adapter.js.map +1 -1
  35. package/dist/esm/activities/evaluate/index.d.ts +4 -0
  36. package/dist/esm/activities/evaluate/index.js +3 -1
  37. package/dist/esm/activities/evaluate/index.js.map +1 -1
  38. package/dist/esm/activities/files/adapter.d.ts +97 -0
  39. package/dist/esm/activities/files/adapter.js +45 -0
  40. package/dist/esm/activities/files/adapter.js.map +1 -0
  41. package/dist/esm/activities/files/index.d.ts +66 -0
  42. package/dist/esm/activities/files/index.js +78 -0
  43. package/dist/esm/activities/files/index.js.map +1 -0
  44. package/dist/esm/activities/generateImage/adapter.d.ts +8 -0
  45. package/dist/esm/activities/generateImage/adapter.js +1 -0
  46. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  47. package/dist/esm/activities/generateImage/index.js +2 -0
  48. package/dist/esm/activities/generateImage/index.js.map +1 -1
  49. package/dist/esm/activities/generateVideo/adapter.d.ts +8 -0
  50. package/dist/esm/activities/generateVideo/adapter.js +1 -0
  51. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  52. package/dist/esm/activities/generateVideo/index.js +3 -0
  53. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  54. package/dist/esm/activities/generateWorld/adapter.d.ts +4 -2
  55. package/dist/esm/activities/generateWorld/adapter.js.map +1 -1
  56. package/dist/esm/activities/generateWorld/index.d.ts +4 -3
  57. package/dist/esm/activities/generateWorld/index.js +5 -4
  58. package/dist/esm/activities/generateWorld/index.js.map +1 -1
  59. package/dist/esm/activities/index.d.ts +6 -3
  60. package/dist/esm/activities/index.js +13 -11
  61. package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -0
  62. package/dist/esm/activities/summarize/chat-stream-summarize.js +8 -8
  63. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  64. package/dist/esm/client.d.ts +3 -1
  65. package/dist/esm/client.js +2 -1
  66. package/dist/esm/client.js.map +1 -1
  67. package/dist/esm/index.d.ts +4 -3
  68. package/dist/esm/index.js +5 -3
  69. package/dist/esm/interrupt-resume.js +29 -4
  70. package/dist/esm/interrupt-resume.js.map +1 -1
  71. package/dist/esm/middlewares/otel.d.ts +5 -2
  72. package/dist/esm/middlewares/otel.js +114 -0
  73. package/dist/esm/middlewares/otel.js.map +1 -1
  74. package/dist/esm/types.d.ts +114 -14
  75. package/dist/esm/utilities/ag-ui-wire.js +39 -16
  76. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  77. package/dist/esm/utilities/content-source.d.ts +60 -0
  78. package/dist/esm/utilities/content-source.js +85 -0
  79. package/dist/esm/utilities/content-source.js.map +1 -0
  80. package/dist/esm/utilities/provider-executed.d.ts +7 -0
  81. package/dist/esm/utilities/provider-executed.js +10 -1
  82. package/dist/esm/utilities/provider-executed.js.map +1 -1
  83. package/dist/esm/utilities/tool-result.d.ts +14 -3
  84. package/dist/esm/utilities/tool-result.js +26 -3
  85. package/dist/esm/utilities/tool-result.js.map +1 -1
  86. package/package.json +4 -4
  87. package/skills/ai-core/adapter-configuration/SKILL.md +62 -0
  88. package/skills/ai-core/chat-experience/SKILL.md +134 -0
  89. package/skills/ai-core/media-generation/SKILL.md +8 -0
  90. package/skills/ai-core/tool-calling/SKILL.md +103 -0
  91. package/src/activities/chat/adapter.ts +10 -0
  92. package/src/activities/chat/agents/define-agent.ts +20 -3
  93. package/src/activities/chat/agents/spawn.ts +20 -12
  94. package/src/activities/chat/index.ts +458 -99
  95. package/src/activities/chat/messages.ts +52 -6
  96. package/src/activities/chat/middleware/types.ts +1 -0
  97. package/src/activities/chat/stream/message-updaters.ts +27 -2
  98. package/src/activities/chat/stream/processor.ts +81 -49
  99. package/src/activities/chat/tools/tool-calls.ts +104 -9
  100. package/src/activities/chat/tools/tool-definition.ts +8 -0
  101. package/src/activities/embed/adapter.ts +7 -0
  102. package/src/activities/embed/index.ts +5 -0
  103. package/src/activities/evaluate/adapter.ts +4 -0
  104. package/src/activities/evaluate/index.ts +6 -0
  105. package/src/activities/files/adapter.ts +120 -0
  106. package/src/activities/files/index.ts +113 -0
  107. package/src/activities/generateImage/adapter.ts +8 -0
  108. package/src/activities/generateImage/index.ts +4 -0
  109. package/src/activities/generateVideo/adapter.ts +8 -0
  110. package/src/activities/generateVideo/index.ts +7 -0
  111. package/src/activities/generateWorld/adapter.ts +4 -2
  112. package/src/activities/generateWorld/index.ts +7 -6
  113. package/src/activities/index.ts +25 -1
  114. package/src/activities/summarize/chat-stream-summarize.ts +22 -12
  115. package/src/client.ts +8 -0
  116. package/src/index.ts +17 -0
  117. package/src/interrupt-resume.ts +55 -4
  118. package/src/middlewares/otel.ts +161 -3
  119. package/src/types.ts +114 -14
  120. package/src/utilities/ag-ui-wire.ts +72 -17
  121. package/src/utilities/content-source.ts +138 -0
  122. package/src/utilities/provider-executed.ts +13 -0
  123. package/src/utilities/tool-result.ts +45 -3
package/src/types.ts CHANGED
@@ -100,6 +100,9 @@ export type ToolResultState =
100
100
  | 'complete' // Result is complete
101
101
  | 'error' // Error occurred
102
102
 
103
+ /** Why a tool result ended without executing successfully. */
104
+ export type ToolResultOutcome = 'cancelled' | 'denied'
105
+
103
106
  export type ToolOutputState = 'output-available' | 'output-error'
104
107
 
105
108
  /**
@@ -196,6 +199,13 @@ export interface ToolCall<TMetadata = unknown> extends Omit<
196
199
  metadata?: TMetadata
197
200
  }
198
201
 
202
+ /** One source link from a provider-executed web search. */
203
+ export interface ProviderExecutedToolSource {
204
+ url: string
205
+ title?: string
206
+ pageAge?: string
207
+ }
208
+
199
209
  /**
200
210
  * Convention for tool-call `metadata` that marks a call as **provider-executed**
201
211
  * — run by the provider's own infrastructure (e.g. Anthropic `web_search` /
@@ -209,10 +219,12 @@ export interface ToolCall<TMetadata = unknown> extends Omit<
209
219
  *
210
220
  * Provider-specific payloads live under a namespaced key (e.g. `anthropic`),
211
221
  * keeping this convention opaque to the framework core. The index signature
212
- * preserves those per-adapter fields.
222
+ * preserves those per-adapter fields. `sources` is the normalized list of
223
+ * links a web search used, shared across providers.
213
224
  */
214
225
  export interface ProviderExecutedToolMetadata {
215
226
  providerExecuted?: boolean
227
+ sources?: Array<ProviderExecutedToolSource>
216
228
  [key: string]: unknown
217
229
  }
218
230
 
@@ -239,8 +251,24 @@ export interface ContentPartUrlSource extends AGUIUrlSource {}
239
251
  /**
240
252
  * A provider-issued file handle (Files API). AG-UI `FileSource`: the handle
241
253
  * is opaque, do not fetch or parse it.
254
+ *
255
+ * The media is uploaded once via a `files` adapter (`openaiFiles()`,
256
+ * `anthropicFiles()`, `geminiFiles()`, `grokFiles()`, `falFiles()`) and
257
+ * referenced here by the returned handle instead of re-sending base64 or a
258
+ * public URL on each request. Only the provider that minted a handle can
259
+ * resolve it. Adapters that cannot consume file handles at all are rejected
260
+ * by the activity-layer preflight before mapping starts.
242
261
  */
243
- export interface ContentPartFileSource extends AGUIFileSource {}
262
+ export interface ContentPartFileSource<
263
+ TProvider extends string = string,
264
+ > extends AGUIFileSource {
265
+ /**
266
+ * The adapter name of the provider that issued the handle (`'openai'`,
267
+ * `'gemini'`, ...), the same id TanStack reports as the usage provider.
268
+ * When present, an adapter rejects a handle another provider issued.
269
+ */
270
+ provider?: TProvider
271
+ }
244
272
 
245
273
  /**
246
274
  * Where a media part's bytes come from: inline data, a URL, or a provider
@@ -426,6 +454,8 @@ export interface ToolResultPart {
426
454
  toolCallId: string
427
455
  content: string | Array<ContentPart>
428
456
  state: ToolResultState
457
+ /** Set when the user or middleware cancelled or denied the tool call; state remains `error`. */
458
+ outcome?: ToolResultOutcome
429
459
  error?: string // Error message if state is "error"
430
460
  metadata?: Record<string, unknown>
431
461
  createdAt?: Date
@@ -544,6 +574,12 @@ export interface TanStackMessageMetadata {
544
574
  model?: string
545
575
  /** Parent chat run that produced this assistant message. */
546
576
  runId?: string
577
+ /**
578
+ * The chat run that produced this assistant message. `withPersistence` sets
579
+ * `id`. `reconstructChat` with `includeRuns: true` adds the finished run's
580
+ * timings, in epoch ms.
581
+ */
582
+ run?: { id: string; startedAt?: number; finishedAt?: number }
547
583
  /** Card data on a child wire message. See `uiMessagesToWire`. */
548
584
  subagent?: SubagentWireInfo
549
585
  /** Thinking signature for a `role: 'reasoning'` fan-out message. */
@@ -555,6 +591,8 @@ export interface TanStackMessageMetadata {
555
591
  createdAt?: string
556
592
  content?: Array<ContentPart>
557
593
  }
594
+ /** Outcome of a cancelled or denied tool result; when present, the UI state is `error`. */
595
+ toolResultOutcome?: ToolResultOutcome
558
596
  structuredOutput?: {
559
597
  status?: 'streaming' | 'complete' | 'error'
560
598
  partial?: unknown
@@ -663,6 +701,15 @@ export interface EmitCustomEventOptions {
663
701
  batch?: boolean
664
702
  }
665
703
 
704
+ /**
705
+ * The user's answer to an `mcp_input` interrupt.
706
+ * `resolved` carries the `payload` from `resolveInterrupt`.
707
+ * `cancelled` means the user called `cancel()`.
708
+ */
709
+ export type ToolInputResponse =
710
+ | { status: 'resolved'; payload: unknown }
711
+ | { status: 'cancelled' }
712
+
666
713
  /**
667
714
  * Context passed to tool execute functions, providing capabilities like
668
715
  * emitting custom events during execution.
@@ -677,6 +724,12 @@ export type ToolExecutionContext<TContext = unknown> =
677
724
  * e.g. MCP `callTool` — should forward this to cancel in-flight work.
678
725
  */
679
726
  abortSignal?: AbortSignal
727
+ /**
728
+ * The answer to the input request that this tool call raised in the
729
+ * previous run. It is set only when the run resumes an `mcp_input`
730
+ * interrupt for this tool call.
731
+ */
732
+ inputResponse?: ToolInputResponse
680
733
  /**
681
734
  * Emit a custom event during tool execution.
682
735
  * Events are streamed to the client in real-time as AG-UI CUSTOM events.
@@ -2305,7 +2358,7 @@ export interface VideoUrlResult {
2305
2358
  // ============================================================================
2306
2359
 
2307
2360
  /**
2308
- * Options for world generation (live, prompt-steerable sessions).
2361
+ * Options for world generation (live session or finished job).
2309
2362
  *
2310
2363
  * @experimental World generation is an experimental feature and may change.
2311
2364
  */
@@ -2317,8 +2370,10 @@ export interface WorldGenerationOptions<
2317
2370
  /** Natural-language description of the world or scene */
2318
2371
  prompt: string
2319
2372
  /**
2320
- * Provider mint options. Reactor resolution/seed/audio are browser
2321
- * `sendCommand` fields, not token-mint fields.
2373
+ * Provider-specific options. Live adapters (Reactor) use mint fields here.
2374
+ * Job adapters (World Labs) use image/video inputs, `wait`, and poll.
2375
+ * Reactor resolution/seed/audio are browser `sendCommand` fields, not
2376
+ * token-mint fields.
2322
2377
  */
2323
2378
  modelOptions?: TProviderOptions
2324
2379
  /**
@@ -2335,28 +2390,73 @@ export interface WorldGenerationOptions<
2335
2390
  abortSignal?: AbortSignal
2336
2391
  }
2337
2392
 
2393
+ /**
2394
+ * Assets from a finished world job. Live session adapters omit this.
2395
+ * URLs are often signed CDN links. They can expire and may need a proxy
2396
+ * to fetch from a browser.
2397
+ *
2398
+ * @experimental World generation is an experimental feature and may change.
2399
+ */
2400
+ export interface WorldGenerationAssets {
2401
+ /** Auto-generated scene description */
2402
+ caption?: string
2403
+ /** Preview image URL */
2404
+ thumbnailUrl?: string
2405
+ splats?: {
2406
+ /** Quality-key map of splat URLs (`100k`, `500k`, `full_res`, …) */
2407
+ spzUrls?: Record<string, string>
2408
+ metricScaleFactor?: number
2409
+ groundPlaneOffset?: number
2410
+ }
2411
+ mesh?: {
2412
+ colliderMeshUrl?: string
2413
+ hqMeshUrl?: string
2414
+ fullResMeshUrl?: string
2415
+ }
2416
+ imagery?: {
2417
+ panoUrl?: string
2418
+ }
2419
+ }
2420
+
2338
2421
  /**
2339
2422
  * Result of world generation. JSON-serializable so a server route can return
2340
- * it to a browser. The browser uses `token` + `model` to open the live
2341
- * session (set the prompt, start streaming, steer mid-run).
2423
+ * it to a browser.
2424
+ *
2425
+ * Live adapters (Reactor): `status: 'ready'` with `token` and token
2426
+ * `expiresAt`. The browser uses `token` + `model` to open the session.
2427
+ *
2428
+ * Job adapters (World Labs): `status: 'ready'` with viewer `url` and
2429
+ * `worldId`, or `status: 'waiting'` with `operationId` and no `url`.
2430
+ * `expiresAt` on a job is operation expiry, not a session token.
2342
2431
  *
2343
2432
  * @experimental World generation is an experimental feature and may change.
2344
2433
  */
2345
2434
  export interface WorldGenerationResult {
2346
2435
  /** Unique identifier for this generation */
2347
2436
  id: string
2348
- /** Model used for generation (provider connect slug) */
2437
+ /** Model used for generation (provider connect slug or model id) */
2349
2438
  model: string
2350
- /** Short-lived session token for the client connection */
2351
- token: string
2352
- /** Token expiry as milliseconds since epoch */
2353
- expiresAt: number
2354
- /** Prompt the client should send when it starts the session */
2439
+ /** Short-lived session token for a live client connection */
2440
+ token?: string
2441
+ /**
2442
+ * Expiry as milliseconds since epoch. Live adapters: session token.
2443
+ * Job adapters: operation expiry when the provider sends it.
2444
+ */
2445
+ expiresAt?: number
2446
+ /** Prompt used to generate the world, or the prompt the client should send */
2355
2447
  prompt: string
2356
- /** Session status after the server half finishes */
2448
+ /** Status after the server half finishes */
2357
2449
  status: 'ready' | 'waiting'
2358
2450
  /** Provider session id, when the adapter created one */
2359
2451
  sessionId?: string
2452
+ /** Viewer URL for a finished world job (not an asset download URL) */
2453
+ url?: string
2454
+ /** Provider world id for a finished or in-progress job */
2455
+ worldId?: string
2456
+ /** Provider operation id for a long-running world job */
2457
+ operationId?: string
2458
+ /** Assets when a world job has finished and the provider returned them */
2459
+ assets?: WorldGenerationAssets
2360
2460
  /** Token usage / billing, when the adapter can report it */
2361
2461
  usage?: TokenUsage
2362
2462
  }
@@ -14,11 +14,13 @@ import type {
14
14
  StructuredOutputPart,
15
15
  SubagentPart,
16
16
  TanStackMessageMetadata,
17
+ ToolResultOutcome,
17
18
  UIMessage,
18
19
  UIResourcePart,
19
20
  } from '../types'
20
21
  import type { MetadataRecord } from './merge-metadata'
21
22
  import { tanstackMetadata } from './merge-metadata'
23
+ import { isProviderExecutedToolCall } from './provider-executed'
22
24
  import { normalizeToolResult } from './tool-result'
23
25
  import { wireSubagentInfo, wireSubagentRunId } from './subagent-wire'
24
26
  import type { SubagentWireInfo } from './subagent-wire'
@@ -48,6 +50,7 @@ function rebuiltToolMetadata(
48
50
  id: string | undefined,
49
51
  content: string | null | Array<ContentPart>,
50
52
  anchorOwnsUiResources = false,
53
+ outcome?: ToolResultOutcome,
51
54
  ): MetadataRecord | undefined {
52
55
  const source: MetadataRecord = isRecord(metadata) ? metadata : {}
53
56
  const tanstack = isRecord(source.tanstack) ? { ...source.tanstack } : {}
@@ -60,7 +63,11 @@ function rebuiltToolMetadata(
60
63
  }
61
64
  const result = {
62
65
  ...source,
63
- tanstack: { ...tanstack, toolResult },
66
+ tanstack: {
67
+ ...tanstack,
68
+ ...(outcome !== undefined && { toolResultOutcome: outcome }),
69
+ toolResult,
70
+ },
64
71
  }
65
72
  return Object.keys(result).length ? result : undefined
66
73
  }
@@ -171,9 +178,45 @@ export function uiMessagesToWire(
171
178
  continue
172
179
  }
173
180
 
174
- // assistant: emit reasoning fan-outs first, then anchor, then tool fan-outs
181
+ // assistant: reasoning fan-outs, then anchor, then tool fan-outs.
182
+ //
183
+ // Provider-executed tools (Anthropic web_search / web_fetch) run inside one
184
+ // provider response, and the provider signs every thinking block against
185
+ // the blocks before it. Emitting all reasoning first and a single anchor
186
+ // with the joined text and every tool call turns
187
+ // "thinking, tool, thinking, text, tool" into
188
+ // "thinking, thinking, text, tool, tool", and the provider rejects the next
189
+ // turn ("thinking blocks in the latest assistant message cannot be
190
+ // modified"). Split the message into ordered segments at each thinking
191
+ // part that follows a provider-executed tool call, the same rule
192
+ // buildAssistantMessages applies, so the wire keeps the signed order.
193
+ // Segments after the first get derived anchor ids; strict AG-UI consumers
194
+ // still see plain anchors.
195
+ type Segment = {
196
+ thinking: Array<Extract<MessagePart, { type: 'thinking' }>>
197
+ parts: Array<MessagePart>
198
+ }
199
+ let current: Segment = { thinking: [], parts: [] }
200
+ const segments: Array<Segment> = [current]
175
201
  for (const part of parts) {
176
202
  if (part.type === 'thinking') {
203
+ if (
204
+ current.parts.some(
205
+ (p) => p.type === 'tool-call' && isProviderExecutedToolCall(p),
206
+ )
207
+ ) {
208
+ current = { thinking: [part], parts: [] }
209
+ segments.push(current)
210
+ } else {
211
+ current.thinking.push(part)
212
+ }
213
+ } else {
214
+ current.parts.push(part)
215
+ }
216
+ }
217
+
218
+ segments.forEach((segment, index) => {
219
+ for (const part of segment.thinking) {
177
220
  const reasoning: WireReasoningMessage = {
178
221
  role: 'reasoning',
179
222
  id: uniqueWireId(deriveReasoningId(uiMessage.id, part), usedWireIds),
@@ -184,22 +227,29 @@ export function uiMessagesToWire(
184
227
  }
185
228
  wire.push(reasoning)
186
229
  }
187
- }
188
230
 
189
- const text = collectText(parts)
190
- const toolCalls = collectToolCalls(parts)
191
- wire.push(
192
- toAnchor(
193
- uiMessage,
194
- 'assistant',
195
- {
196
- ...(text !== '' && { content: text }),
197
- ...(toolCalls && { toolCalls }),
198
- },
199
- parts,
200
- includeSnapshotStructuredOutput,
201
- ),
202
- )
231
+ const text = collectText(segment.parts)
232
+ const toolCalls = collectToolCalls(segment.parts)
233
+ const anchorMessage: UIMessage =
234
+ index === 0
235
+ ? uiMessage
236
+ : {
237
+ ...uiMessage,
238
+ id: uniqueWireId(`${uiMessage.id}-segment-${index}`, usedWireIds),
239
+ }
240
+ wire.push(
241
+ toAnchor(
242
+ anchorMessage,
243
+ 'assistant',
244
+ {
245
+ ...(text !== '' && { content: text }),
246
+ ...(toolCalls && { toolCalls }),
247
+ },
248
+ segment.parts,
249
+ includeSnapshotStructuredOutput,
250
+ ),
251
+ )
252
+ })
203
253
 
204
254
  const explicitToolResults = new Set(
205
255
  parts.flatMap((part) =>
@@ -218,6 +268,7 @@ export function uiMessagesToWire(
218
268
  part.id,
219
269
  part.content,
220
270
  true,
271
+ part.outcome,
221
272
  )
222
273
  wire.push({
223
274
  role: 'tool',
@@ -262,6 +313,8 @@ export function uiMessagesToWire(
262
313
  undefined,
263
314
  undefined,
264
315
  result,
316
+ false,
317
+ part.approval?.approved === false ? 'denied' : undefined,
265
318
  ),
266
319
  })
267
320
  }
@@ -341,6 +394,8 @@ function messageMetadata(
341
394
  tanstack.model = previousTanstack.model
342
395
  if (previousTanstack?.runId !== undefined)
343
396
  tanstack.runId = previousTanstack.runId
397
+ if (previousTanstack?.run?.id !== undefined)
398
+ tanstack.run = { id: previousTanstack.run.id }
344
399
  if (previousTanstack?.signature !== undefined)
345
400
  tanstack.signature = previousTanstack.signature
346
401
  const createdAt = coerceCreatedAt(msg.createdAt)
@@ -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
+ }
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import type { ContentPart } from '../types'
1
+ import type { ContentPart, ToolResultOutcome } from '../types'
2
2
 
3
3
  const CONTENT_PART_TYPES = new Set([
4
4
  'text',
@@ -8,10 +8,17 @@ const CONTENT_PART_TYPES = new Set([
8
8
  'document',
9
9
  ])
10
10
 
11
+ export function isToolResultOutcome(
12
+ value: unknown,
13
+ ): value is ToolResultOutcome {
14
+ return value === 'cancelled' || value === 'denied'
15
+ }
16
+
11
17
  /**
12
18
  * Structural check for a single `ContentPart`. A text part must carry a string
13
- * `content`; every other modality must carry a `source` with `type` of
14
- * `'url' | 'data'` and a string `value`.
19
+ * `content`. Every other part carries a source with a string `value`; a file
20
+ * source's `value` is a non-empty opaque handle, and its optional `provider`
21
+ * is a string.
15
22
  */
16
23
  export function isContentPart(value: unknown): value is ContentPart {
17
24
  if (typeof value !== 'object' || value === null) return false
@@ -26,6 +33,14 @@ export function isContentPart(value: unknown): value is ContentPart {
26
33
  if (typeof source !== 'object' || source === null) return false
27
34
  const src = source as Record<string, unknown>
28
35
  if (typeof src.value !== 'string') return false
36
+ // `file` sources carry an opaque handle in `value`; `provider`, when set,
37
+ // names the issuer.
38
+ if (src.type === 'file') {
39
+ return (
40
+ src.value.length > 0 &&
41
+ (src.provider === undefined || typeof src.provider === 'string')
42
+ )
43
+ }
29
44
  // `data` sources require a mimeType (matches ContentPartDataSource); `url`
30
45
  // sources don't. Requiring it here keeps the runtime guard consistent with
31
46
  // the type and avoids emitting `data:undefined;base64,...` downstream.
@@ -45,6 +60,33 @@ export function isContentPartArray(
45
60
  return Array.isArray(value) && value.length > 0 && value.every(isContentPart)
46
61
  }
47
62
 
63
+ /**
64
+ * Error text for a failed tool result: `output.error` when it is a string,
65
+ * else the output itself when it is a string, else a generic message.
66
+ * `StreamProcessor` and `chat()` history share it, so a reload shows the
67
+ * same text as the live stream.
68
+ */
69
+ export function toolResultErrorText(output: unknown): string {
70
+ if (
71
+ output &&
72
+ typeof output === 'object' &&
73
+ 'error' in output &&
74
+ typeof output.error === 'string'
75
+ ) {
76
+ return output.error
77
+ }
78
+ return typeof output === 'string' ? output : 'Tool execution failed'
79
+ }
80
+
81
+ /** Parse tool result content as JSON. Plain text stays a string. */
82
+ export function parseToolOutput(content: string): unknown {
83
+ try {
84
+ return JSON.parse(content)
85
+ } catch {
86
+ return content
87
+ }
88
+ }
89
+
48
90
  /**
49
91
  * Normalize a tool's return value for transport:
50
92
  * - string → unchanged