@tanstack/ai 0.59.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 (91) 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/index.js +134 -22
  5. package/dist/esm/activities/chat/index.js.map +1 -1
  6. package/dist/esm/activities/chat/messages.js +5 -1
  7. package/dist/esm/activities/chat/messages.js.map +1 -1
  8. package/dist/esm/activities/chat/stream/message-updaters.js +9 -2
  9. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  10. package/dist/esm/activities/chat/stream/processor.d.ts +0 -1
  11. package/dist/esm/activities/chat/stream/processor.js +15 -12
  12. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  13. package/dist/esm/activities/chat/tools/tool-calls.js +2 -0
  14. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  15. package/dist/esm/activities/embed/adapter.d.ts +7 -0
  16. package/dist/esm/activities/embed/adapter.js +1 -0
  17. package/dist/esm/activities/embed/adapter.js.map +1 -1
  18. package/dist/esm/activities/embed/index.js +2 -0
  19. package/dist/esm/activities/embed/index.js.map +1 -1
  20. package/dist/esm/activities/files/adapter.d.ts +97 -0
  21. package/dist/esm/activities/files/adapter.js +45 -0
  22. package/dist/esm/activities/files/adapter.js.map +1 -0
  23. package/dist/esm/activities/files/index.d.ts +66 -0
  24. package/dist/esm/activities/files/index.js +78 -0
  25. package/dist/esm/activities/files/index.js.map +1 -0
  26. package/dist/esm/activities/generateImage/adapter.d.ts +8 -0
  27. package/dist/esm/activities/generateImage/adapter.js +1 -0
  28. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  29. package/dist/esm/activities/generateImage/index.js +2 -0
  30. package/dist/esm/activities/generateImage/index.js.map +1 -1
  31. package/dist/esm/activities/generateVideo/adapter.d.ts +8 -0
  32. package/dist/esm/activities/generateVideo/adapter.js +1 -0
  33. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  34. package/dist/esm/activities/generateVideo/index.js +3 -0
  35. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  36. package/dist/esm/activities/generateWorld/adapter.d.ts +4 -2
  37. package/dist/esm/activities/generateWorld/adapter.js.map +1 -1
  38. package/dist/esm/activities/generateWorld/index.d.ts +4 -3
  39. package/dist/esm/activities/generateWorld/index.js +5 -4
  40. package/dist/esm/activities/generateWorld/index.js.map +1 -1
  41. package/dist/esm/activities/index.d.ts +6 -3
  42. package/dist/esm/activities/index.js +13 -11
  43. package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -0
  44. package/dist/esm/activities/summarize/chat-stream-summarize.js +8 -8
  45. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  46. package/dist/esm/client.d.ts +2 -0
  47. package/dist/esm/client.js +2 -1
  48. package/dist/esm/client.js.map +1 -1
  49. package/dist/esm/index.d.ts +3 -2
  50. package/dist/esm/index.js +4 -2
  51. package/dist/esm/types.d.ts +72 -13
  52. package/dist/esm/utilities/ag-ui-wire.js +34 -13
  53. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  54. package/dist/esm/utilities/content-source.d.ts +60 -0
  55. package/dist/esm/utilities/content-source.js +85 -0
  56. package/dist/esm/utilities/content-source.js.map +1 -0
  57. package/dist/esm/utilities/provider-executed.d.ts +7 -0
  58. package/dist/esm/utilities/provider-executed.js +10 -1
  59. package/dist/esm/utilities/provider-executed.js.map +1 -1
  60. package/dist/esm/utilities/tool-result.d.ts +12 -2
  61. package/dist/esm/utilities/tool-result.js +23 -3
  62. package/dist/esm/utilities/tool-result.js.map +1 -1
  63. package/package.json +2 -2
  64. package/skills/ai-core/adapter-configuration/SKILL.md +62 -0
  65. package/skills/ai-core/chat-experience/SKILL.md +14 -0
  66. package/skills/ai-core/media-generation/SKILL.md +8 -0
  67. package/src/activities/chat/adapter.ts +10 -0
  68. package/src/activities/chat/index.ts +226 -40
  69. package/src/activities/chat/messages.ts +12 -1
  70. package/src/activities/chat/stream/message-updaters.ts +24 -2
  71. package/src/activities/chat/stream/processor.ts +24 -22
  72. package/src/activities/chat/tools/tool-calls.ts +6 -0
  73. package/src/activities/embed/adapter.ts +7 -0
  74. package/src/activities/embed/index.ts +5 -0
  75. package/src/activities/files/adapter.ts +120 -0
  76. package/src/activities/files/index.ts +113 -0
  77. package/src/activities/generateImage/adapter.ts +8 -0
  78. package/src/activities/generateImage/index.ts +4 -0
  79. package/src/activities/generateVideo/adapter.ts +8 -0
  80. package/src/activities/generateVideo/index.ts +7 -0
  81. package/src/activities/generateWorld/adapter.ts +4 -2
  82. package/src/activities/generateWorld/index.ts +7 -6
  83. package/src/activities/index.ts +25 -1
  84. package/src/activities/summarize/chat-stream-summarize.ts +22 -12
  85. package/src/client.ts +7 -0
  86. package/src/index.ts +16 -0
  87. package/src/types.ts +76 -13
  88. package/src/utilities/ag-ui-wire.ts +60 -16
  89. package/src/utilities/content-source.ts +138 -0
  90. package/src/utilities/provider-executed.ts +13 -0
  91. package/src/utilities/tool-result.ts +38 -2
package/src/client.ts CHANGED
@@ -329,6 +329,13 @@ export type { AdapterYieldChunk } from './utilities/adapter-yield-chunk'
329
329
  export { getChunkRunId, getChunkThreadId } from './utilities/chunk-ids'
330
330
  export type { WireMessage } from './utilities/ag-ui-wire'
331
331
 
332
+ // A browser client that received an uploaded handle from its server can build
333
+ // the `{ type: 'file' }` content source itself — `fileSourceFromHandle` is a
334
+ // pure object builder, so exporting it here keeps the documented client flow
335
+ // from pulling in the server entry.
336
+ export { fileSourceFromHandle } from './activities/files/index'
337
+ export type { FileHandle } from './activities/files/adapter'
338
+
332
339
  export type {
333
340
  AudioPart,
334
341
  ContentPart,
package/src/index.ts CHANGED
@@ -18,6 +18,10 @@ export {
18
18
  embed,
19
19
  generateWorld,
20
20
  generateLiveVideo,
21
+ uploadFile,
22
+ getFile,
23
+ deleteFile,
24
+ fileSourceFromHandle,
21
25
  } from './activities/index'
22
26
 
23
27
  // Create options functions - for pre-defining typed configurations
@@ -70,6 +74,10 @@ export type {
70
74
  WorldAdapter,
71
75
  AnyLiveVideoAdapter,
72
76
  LiveVideoAdapter,
77
+ FilesAdapter,
78
+ AnyFilesAdapter,
79
+ FileHandle,
80
+ FileUploadInput,
73
81
  } from './activities/index'
74
82
 
75
83
  // Rerank adapter base + types
@@ -529,6 +537,14 @@ export {
529
537
  isContentPartArray,
530
538
  normalizeToolResult,
531
539
  } from './utilities/tool-result'
540
+ export {
541
+ assertMessagesFileSourceSupport,
542
+ assertPromptFileSourceSupport,
543
+ fileReferenceFor,
544
+ isFileSource,
545
+ unsupportedFileSourceError,
546
+ type FileSourceCapable,
547
+ } from './utilities/content-source'
532
548
 
533
549
  export {
534
550
  getProviderExecutedMetadata,
package/src/types.ts CHANGED
@@ -239,8 +239,24 @@ export interface ContentPartUrlSource extends AGUIUrlSource {}
239
239
  /**
240
240
  * A provider-issued file handle (Files API). AG-UI `FileSource`: the handle
241
241
  * is opaque, do not fetch or parse it.
242
+ *
243
+ * The media is uploaded once via a `files` adapter (`openaiFiles()`,
244
+ * `anthropicFiles()`, `geminiFiles()`, `grokFiles()`, `falFiles()`) and
245
+ * referenced here by the returned handle instead of re-sending base64 or a
246
+ * public URL on each request. Only the provider that minted a handle can
247
+ * resolve it. Adapters that cannot consume file handles at all are rejected
248
+ * by the activity-layer preflight before mapping starts.
242
249
  */
243
- export interface ContentPartFileSource extends AGUIFileSource {}
250
+ export interface ContentPartFileSource<
251
+ TProvider extends string = string,
252
+ > extends AGUIFileSource {
253
+ /**
254
+ * The adapter name of the provider that issued the handle (`'openai'`,
255
+ * `'gemini'`, ...), the same id TanStack reports as the usage provider.
256
+ * When present, an adapter rejects a handle another provider issued.
257
+ */
258
+ provider?: TProvider
259
+ }
244
260
 
245
261
  /**
246
262
  * Where a media part's bytes come from: inline data, a URL, or a provider
@@ -2305,7 +2321,7 @@ export interface VideoUrlResult {
2305
2321
  // ============================================================================
2306
2322
 
2307
2323
  /**
2308
- * Options for world generation (live, prompt-steerable sessions).
2324
+ * Options for world generation (live session or finished job).
2309
2325
  *
2310
2326
  * @experimental World generation is an experimental feature and may change.
2311
2327
  */
@@ -2317,8 +2333,10 @@ export interface WorldGenerationOptions<
2317
2333
  /** Natural-language description of the world or scene */
2318
2334
  prompt: string
2319
2335
  /**
2320
- * Provider mint options. Reactor resolution/seed/audio are browser
2321
- * `sendCommand` fields, not token-mint fields.
2336
+ * Provider-specific options. Live adapters (Reactor) use mint fields here.
2337
+ * Job adapters (World Labs) use image/video inputs, `wait`, and poll.
2338
+ * Reactor resolution/seed/audio are browser `sendCommand` fields, not
2339
+ * token-mint fields.
2322
2340
  */
2323
2341
  modelOptions?: TProviderOptions
2324
2342
  /**
@@ -2335,28 +2353,73 @@ export interface WorldGenerationOptions<
2335
2353
  abortSignal?: AbortSignal
2336
2354
  }
2337
2355
 
2356
+ /**
2357
+ * Assets from a finished world job. Live session adapters omit this.
2358
+ * URLs are often signed CDN links. They can expire and may need a proxy
2359
+ * to fetch from a browser.
2360
+ *
2361
+ * @experimental World generation is an experimental feature and may change.
2362
+ */
2363
+ export interface WorldGenerationAssets {
2364
+ /** Auto-generated scene description */
2365
+ caption?: string
2366
+ /** Preview image URL */
2367
+ thumbnailUrl?: string
2368
+ splats?: {
2369
+ /** Quality-key map of splat URLs (`100k`, `500k`, `full_res`, …) */
2370
+ spzUrls?: Record<string, string>
2371
+ metricScaleFactor?: number
2372
+ groundPlaneOffset?: number
2373
+ }
2374
+ mesh?: {
2375
+ colliderMeshUrl?: string
2376
+ hqMeshUrl?: string
2377
+ fullResMeshUrl?: string
2378
+ }
2379
+ imagery?: {
2380
+ panoUrl?: string
2381
+ }
2382
+ }
2383
+
2338
2384
  /**
2339
2385
  * 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).
2386
+ * it to a browser.
2387
+ *
2388
+ * Live adapters (Reactor): `status: 'ready'` with `token` and token
2389
+ * `expiresAt`. The browser uses `token` + `model` to open the session.
2390
+ *
2391
+ * Job adapters (World Labs): `status: 'ready'` with viewer `url` and
2392
+ * `worldId`, or `status: 'waiting'` with `operationId` and no `url`.
2393
+ * `expiresAt` on a job is operation expiry, not a session token.
2342
2394
  *
2343
2395
  * @experimental World generation is an experimental feature and may change.
2344
2396
  */
2345
2397
  export interface WorldGenerationResult {
2346
2398
  /** Unique identifier for this generation */
2347
2399
  id: string
2348
- /** Model used for generation (provider connect slug) */
2400
+ /** Model used for generation (provider connect slug or model id) */
2349
2401
  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 */
2402
+ /** Short-lived session token for a live client connection */
2403
+ token?: string
2404
+ /**
2405
+ * Expiry as milliseconds since epoch. Live adapters: session token.
2406
+ * Job adapters: operation expiry when the provider sends it.
2407
+ */
2408
+ expiresAt?: number
2409
+ /** Prompt used to generate the world, or the prompt the client should send */
2355
2410
  prompt: string
2356
- /** Session status after the server half finishes */
2411
+ /** Status after the server half finishes */
2357
2412
  status: 'ready' | 'waiting'
2358
2413
  /** Provider session id, when the adapter created one */
2359
2414
  sessionId?: string
2415
+ /** Viewer URL for a finished world job (not an asset download URL) */
2416
+ url?: string
2417
+ /** Provider world id for a finished or in-progress job */
2418
+ worldId?: string
2419
+ /** Provider operation id for a long-running world job */
2420
+ operationId?: string
2421
+ /** Assets when a world job has finished and the provider returned them */
2422
+ assets?: WorldGenerationAssets
2360
2423
  /** Token usage / billing, when the adapter can report it */
2361
2424
  usage?: TokenUsage
2362
2425
  }
@@ -19,6 +19,7 @@ import type {
19
19
  } from '../types'
20
20
  import type { MetadataRecord } from './merge-metadata'
21
21
  import { tanstackMetadata } from './merge-metadata'
22
+ import { isProviderExecutedToolCall } from './provider-executed'
22
23
  import { normalizeToolResult } from './tool-result'
23
24
  import { wireSubagentInfo, wireSubagentRunId } from './subagent-wire'
24
25
  import type { SubagentWireInfo } from './subagent-wire'
@@ -171,9 +172,45 @@ export function uiMessagesToWire(
171
172
  continue
172
173
  }
173
174
 
174
- // 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]
175
195
  for (const part of parts) {
176
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) {
177
214
  const reasoning: WireReasoningMessage = {
178
215
  role: 'reasoning',
179
216
  id: uniqueWireId(deriveReasoningId(uiMessage.id, part), usedWireIds),
@@ -184,22 +221,29 @@ export function uiMessagesToWire(
184
221
  }
185
222
  wire.push(reasoning)
186
223
  }
187
- }
188
224
 
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
- )
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
+ })
203
247
 
204
248
  const explicitToolResults = new Set(
205
249
  parts.flatMap((part) =>
@@ -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
+ }
@@ -10,8 +10,9 @@ const CONTENT_PART_TYPES = new Set([
10
10
 
11
11
  /**
12
12
  * 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`.
13
+ * `content`. Every other part carries a source with a string `value`; a file
14
+ * source's `value` is a non-empty opaque handle, and its optional `provider`
15
+ * is a string.
15
16
  */
16
17
  export function isContentPart(value: unknown): value is ContentPart {
17
18
  if (typeof value !== 'object' || value === null) return false
@@ -26,6 +27,14 @@ export function isContentPart(value: unknown): value is ContentPart {
26
27
  if (typeof source !== 'object' || source === null) return false
27
28
  const src = source as Record<string, unknown>
28
29
  if (typeof src.value !== 'string') return false
30
+ // `file` sources carry an opaque handle in `value`; `provider`, when set,
31
+ // names the issuer.
32
+ if (src.type === 'file') {
33
+ return (
34
+ src.value.length > 0 &&
35
+ (src.provider === undefined || typeof src.provider === 'string')
36
+ )
37
+ }
29
38
  // `data` sources require a mimeType (matches ContentPartDataSource); `url`
30
39
  // sources don't. Requiring it here keeps the runtime guard consistent with
31
40
  // the type and avoids emitting `data:undefined;base64,...` downstream.
@@ -45,6 +54,33 @@ export function isContentPartArray(
45
54
  return Array.isArray(value) && value.length > 0 && value.every(isContentPart)
46
55
  }
47
56
 
57
+ /**
58
+ * Error text for a failed tool result: `output.error` when it is a string,
59
+ * else the output itself when it is a string, else a generic message.
60
+ * `StreamProcessor` and `chat()` history share it, so a reload shows the
61
+ * same text as the live stream.
62
+ */
63
+ export function toolResultErrorText(output: unknown): string {
64
+ if (
65
+ output &&
66
+ typeof output === 'object' &&
67
+ 'error' in output &&
68
+ typeof output.error === 'string'
69
+ ) {
70
+ return output.error
71
+ }
72
+ return typeof output === 'string' ? output : 'Tool execution failed'
73
+ }
74
+
75
+ /** Parse tool result content as JSON. Plain text stays a string. */
76
+ export function parseToolOutput(content: string): unknown {
77
+ try {
78
+ return JSON.parse(content)
79
+ } catch {
80
+ return content
81
+ }
82
+ }
83
+
48
84
  /**
49
85
  * Normalize a tool's return value for transport:
50
86
  * - string → unchanged