@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.
- 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/index.js +134 -22
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +5 -1
- package/dist/esm/activities/chat/messages.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 +0 -1
- package/dist/esm/activities/chat/stream/processor.js +15 -12
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.js +2 -0
- 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/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 +2 -0
- package/dist/esm/activities/generateImage/index.js.map +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/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 +5 -4
- package/dist/esm/activities/generateWorld/index.js.map +1 -1
- package/dist/esm/activities/index.d.ts +6 -3
- package/dist/esm/activities/index.js +13 -11
- 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/client.d.ts +2 -0
- package/dist/esm/client.js +2 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.js +4 -2
- package/dist/esm/types.d.ts +72 -13
- package/dist/esm/utilities/ag-ui-wire.js +34 -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/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/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 +2 -2
- package/skills/ai-core/adapter-configuration/SKILL.md +62 -0
- package/skills/ai-core/chat-experience/SKILL.md +14 -0
- package/skills/ai-core/media-generation/SKILL.md +8 -0
- package/src/activities/chat/adapter.ts +10 -0
- package/src/activities/chat/index.ts +226 -40
- package/src/activities/chat/messages.ts +12 -1
- package/src/activities/chat/stream/message-updaters.ts +24 -2
- package/src/activities/chat/stream/processor.ts +24 -22
- package/src/activities/chat/tools/tool-calls.ts +6 -0
- 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 +25 -1
- package/src/activities/summarize/chat-stream-summarize.ts +22 -12
- package/src/client.ts +7 -0
- package/src/index.ts +16 -0
- package/src/types.ts +76 -13
- package/src/utilities/ag-ui-wire.ts +60 -16
- package/src/utilities/content-source.ts +138 -0
- package/src/utilities/provider-executed.ts +13 -0
- 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
|
|
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
|
|
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
|
|
2321
|
-
*
|
|
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.
|
|
2341
|
-
*
|
|
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
|
|
2351
|
-
token
|
|
2352
|
-
/**
|
|
2353
|
-
|
|
2354
|
-
|
|
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
|
-
/**
|
|
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:
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
14
|
-
*
|
|
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
|