@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.
- package/README.md +1 -0
- package/dist/esm/activities/chat/adapter.d.ts +9 -0
- package/dist/esm/activities/chat/adapter.js +1 -0
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/agents/define-agent.d.ts +17 -5
- package/dist/esm/activities/chat/agents/define-agent.js.map +1 -1
- package/dist/esm/activities/chat/agents/spawn.d.ts +2 -0
- package/dist/esm/activities/chat/agents/spawn.js +8 -5
- package/dist/esm/activities/chat/agents/spawn.js.map +1 -1
- package/dist/esm/activities/chat/index.js +289 -78
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +35 -19
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +1 -0
- package/dist/esm/activities/chat/middleware/types.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
- package/dist/esm/activities/chat/stream/message-updaters.js +11 -3
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +11 -10
- package/dist/esm/activities/chat/stream/processor.js +51 -28
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +16 -2
- package/dist/esm/activities/chat/tools/tool-calls.js +57 -16
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-definition.d.ts +4 -0
- package/dist/esm/activities/chat/tools/tool-definition.js +4 -0
- package/dist/esm/activities/chat/tools/tool-definition.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/evaluate/adapter.d.ts +4 -0
- package/dist/esm/activities/evaluate/adapter.js.map +1 -1
- package/dist/esm/activities/evaluate/index.d.ts +4 -0
- package/dist/esm/activities/evaluate/index.js +3 -1
- package/dist/esm/activities/evaluate/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 +3 -1
- package/dist/esm/client.js +2 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +4 -3
- package/dist/esm/index.js +5 -3
- package/dist/esm/interrupt-resume.js +29 -4
- package/dist/esm/interrupt-resume.js.map +1 -1
- package/dist/esm/middlewares/otel.d.ts +5 -2
- package/dist/esm/middlewares/otel.js +114 -0
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/types.d.ts +114 -14
- package/dist/esm/utilities/ag-ui-wire.js +39 -16
- 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 +14 -3
- package/dist/esm/utilities/tool-result.js +26 -3
- package/dist/esm/utilities/tool-result.js.map +1 -1
- package/package.json +4 -4
- package/skills/ai-core/adapter-configuration/SKILL.md +62 -0
- package/skills/ai-core/chat-experience/SKILL.md +134 -0
- package/skills/ai-core/media-generation/SKILL.md +8 -0
- package/skills/ai-core/tool-calling/SKILL.md +103 -0
- package/src/activities/chat/adapter.ts +10 -0
- package/src/activities/chat/agents/define-agent.ts +20 -3
- package/src/activities/chat/agents/spawn.ts +20 -12
- package/src/activities/chat/index.ts +458 -99
- package/src/activities/chat/messages.ts +52 -6
- package/src/activities/chat/middleware/types.ts +1 -0
- package/src/activities/chat/stream/message-updaters.ts +27 -2
- package/src/activities/chat/stream/processor.ts +81 -49
- package/src/activities/chat/tools/tool-calls.ts +104 -9
- package/src/activities/chat/tools/tool-definition.ts +8 -0
- package/src/activities/embed/adapter.ts +7 -0
- package/src/activities/embed/index.ts +5 -0
- package/src/activities/evaluate/adapter.ts +4 -0
- package/src/activities/evaluate/index.ts +6 -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 +8 -0
- package/src/index.ts +17 -0
- package/src/interrupt-resume.ts +55 -4
- package/src/middlewares/otel.ts +161 -3
- package/src/types.ts +114 -14
- package/src/utilities/ag-ui-wire.ts +72 -17
- package/src/utilities/content-source.ts +138 -0
- package/src/utilities/provider-executed.ts +13 -0
- package/src/utilities/tool-result.ts +45 -3
|
@@ -74,6 +74,13 @@ export interface VideoAdapter<
|
|
|
74
74
|
readonly kind: 'video'
|
|
75
75
|
/** Adapter name identifier */
|
|
76
76
|
readonly name: string
|
|
77
|
+
/**
|
|
78
|
+
* Declares that this adapter can consume `{ type: 'file' }` content
|
|
79
|
+
* sources (provider Files API references). The activity dispatcher rejects
|
|
80
|
+
* file sources in preflight for adapters that don't declare this, so
|
|
81
|
+
* adapters written before the file arm existed fail closed.
|
|
82
|
+
*/
|
|
83
|
+
readonly supportsFileSources?: boolean
|
|
77
84
|
/** The model this adapter is configured for */
|
|
78
85
|
readonly model: TModel
|
|
79
86
|
|
|
@@ -161,6 +168,7 @@ export abstract class BaseVideoAdapter<
|
|
|
161
168
|
> {
|
|
162
169
|
readonly kind = 'video' as const
|
|
163
170
|
abstract readonly name: string
|
|
171
|
+
readonly supportsFileSources: boolean = false
|
|
164
172
|
readonly model: TModel
|
|
165
173
|
|
|
166
174
|
// Type-only property - never assigned at runtime
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { aiEventClient } from '@tanstack/ai-event-client'
|
|
13
13
|
import { toRunErrorPayload } from '../error-payload'
|
|
14
14
|
import { resolveDebugOption } from '../../logger/resolve'
|
|
15
|
+
import { assertPromptFileSourceSupport } from '../../utilities/content-source'
|
|
15
16
|
import {
|
|
16
17
|
applyGenerationResultTransforms,
|
|
17
18
|
createGenerationContext,
|
|
@@ -452,6 +453,9 @@ async function runCreateVideoJob<
|
|
|
452
453
|
timeout,
|
|
453
454
|
abortSignal: callerAbortSignal,
|
|
454
455
|
} = options
|
|
456
|
+
// Fail closed on `{ type: 'file' }` sources for adapters that haven't
|
|
457
|
+
// declared support (see assertPromptFileSourceSupport).
|
|
458
|
+
assertPromptFileSourceSupport(adapter, prompt)
|
|
455
459
|
const model = adapter.model
|
|
456
460
|
const requestId = createId('video')
|
|
457
461
|
const startTime = Date.now()
|
|
@@ -580,6 +584,9 @@ async function* runStreamingVideoGeneration<
|
|
|
580
584
|
timeout,
|
|
581
585
|
abortSignal: callerAbortSignal,
|
|
582
586
|
} = options
|
|
587
|
+
// Fail closed on `{ type: 'file' }` sources for adapters that haven't
|
|
588
|
+
// declared support (see assertPromptFileSourceSupport).
|
|
589
|
+
assertPromptFileSourceSupport(adapter, prompt)
|
|
583
590
|
const model = adapter.model
|
|
584
591
|
const runId = options.runId ?? createId('run')
|
|
585
592
|
const requestId = createId('video')
|
|
@@ -44,10 +44,12 @@ export interface WorldAdapter<
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
47
|
+
* Create a world from a prompt.
|
|
48
48
|
*
|
|
49
|
-
*
|
|
49
|
+
* Live session adapters mint a short-lived token and return it with the
|
|
50
50
|
* prompt so a browser can connect, set the prompt, and start streaming.
|
|
51
|
+
* Job adapters start generation and return a world URL (or an operation
|
|
52
|
+
* id while the job is still running).
|
|
51
53
|
*/
|
|
52
54
|
createWorld: (
|
|
53
55
|
options: WorldGenerationOptions<TProviderOptions>,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* World Activity (Experimental)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Live adapters mint a session token for a prompt-steerable world. Job
|
|
5
|
+
* adapters start generation and return a viewer URL, or an operation id
|
|
6
|
+
* while the job is still running.
|
|
7
7
|
*
|
|
8
8
|
* @experimental World generation is an experimental feature and may change.
|
|
9
9
|
*/
|
|
@@ -76,7 +76,7 @@ export interface WorldActivityOptions<
|
|
|
76
76
|
/** Provider-specific options for world generation */
|
|
77
77
|
modelOptions?: WorldProviderOptions<TAdapter>
|
|
78
78
|
/**
|
|
79
|
-
* Whether to wrap the
|
|
79
|
+
* Whether to wrap the result as StreamChunks for SSE transport.
|
|
80
80
|
* This is not the live video. When false or omitted, returns
|
|
81
81
|
* Promise<WorldGenerationResult>.
|
|
82
82
|
*
|
|
@@ -100,7 +100,7 @@ export interface WorldActivityOptions<
|
|
|
100
100
|
/** Stable run id for correlating this run when persisted. */
|
|
101
101
|
runId?: string
|
|
102
102
|
/**
|
|
103
|
-
* Maximum
|
|
103
|
+
* Maximum wait for the mint or job poll, in milliseconds.
|
|
104
104
|
* No SDK-wide default. Composed with {@link abortSignal}; the first abort wins.
|
|
105
105
|
*/
|
|
106
106
|
timeout?: number
|
|
@@ -135,7 +135,8 @@ function createId(prefix: string): string {
|
|
|
135
135
|
// ===========================
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
|
-
* World generation activity
|
|
138
|
+
* World generation activity. Live adapters mint a session token. Job
|
|
139
|
+
* adapters return a viewer URL or an in-progress operation id.
|
|
139
140
|
*
|
|
140
141
|
* @example Mint a session token on the server
|
|
141
142
|
* ```ts
|
package/src/activities/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import type { AnyRerankAdapter } from './rerank/adapter'
|
|
|
27
27
|
import type { AnyEvaluateAdapter } from './evaluate/adapter'
|
|
28
28
|
import type { AnyWorldAdapter } from './generateWorld/adapter'
|
|
29
29
|
import type { AnyLiveVideoAdapter } from './generateLiveVideo/adapter'
|
|
30
|
+
import type { AnyFilesAdapter } from './files/adapter'
|
|
30
31
|
|
|
31
32
|
// ===========================
|
|
32
33
|
// Chat Activity
|
|
@@ -330,11 +331,32 @@ export {
|
|
|
330
331
|
type AnyLiveVideoAdapter,
|
|
331
332
|
} from './generateLiveVideo/adapter'
|
|
332
333
|
|
|
334
|
+
// ===========================
|
|
335
|
+
// Files Activity
|
|
336
|
+
// ===========================
|
|
337
|
+
|
|
338
|
+
export {
|
|
339
|
+
kind as filesKind,
|
|
340
|
+
uploadFile,
|
|
341
|
+
getFile,
|
|
342
|
+
deleteFile,
|
|
343
|
+
fileSourceFromHandle,
|
|
344
|
+
} from './files/index'
|
|
345
|
+
|
|
346
|
+
export {
|
|
347
|
+
BaseFilesAdapter,
|
|
348
|
+
normalizeFileUploadInput,
|
|
349
|
+
type FilesAdapter,
|
|
350
|
+
type AnyFilesAdapter,
|
|
351
|
+
type FileHandle,
|
|
352
|
+
type FileUploadInput,
|
|
353
|
+
} from './files/adapter'
|
|
354
|
+
|
|
333
355
|
// ===========================
|
|
334
356
|
// Adapter Union Types
|
|
335
357
|
// ===========================
|
|
336
358
|
|
|
337
|
-
/** Union of all adapter types
|
|
359
|
+
/** Union of all adapter types across every activity kind */
|
|
338
360
|
export type AIAdapter =
|
|
339
361
|
| AnyTextAdapter
|
|
340
362
|
| AnySummarizeAdapter
|
|
@@ -349,6 +371,7 @@ export type AIAdapter =
|
|
|
349
371
|
| AnyEvaluateAdapter
|
|
350
372
|
| AnyWorldAdapter
|
|
351
373
|
| AnyLiveVideoAdapter
|
|
374
|
+
| AnyFilesAdapter
|
|
352
375
|
|
|
353
376
|
/** Union type of all adapter kinds */
|
|
354
377
|
export type AdapterKind =
|
|
@@ -365,3 +388,4 @@ export type AdapterKind =
|
|
|
365
388
|
| 'evaluate'
|
|
366
389
|
| 'world'
|
|
367
390
|
| 'liveVideo'
|
|
391
|
+
| 'files'
|
|
@@ -65,6 +65,8 @@ function throwRunError(
|
|
|
65
65
|
* `SummarizationOptions<TProviderOptions>` on the wrapper itself.
|
|
66
66
|
*/
|
|
67
67
|
export interface ChatStreamCapable {
|
|
68
|
+
/** Native token-limit option for adapters whose provider name is user-defined. */
|
|
69
|
+
readonly maxTokensKey?: string
|
|
68
70
|
chatStream: (options: TextOptions<any>) => AsyncIterable<AdapterYieldChunk>
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -151,8 +153,8 @@ function applyDefaultTemperature(
|
|
|
151
153
|
|
|
152
154
|
/**
|
|
153
155
|
* Resolve `maxLength` to the provider-native max-output-tokens key for the
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
+
* wrapped text adapter's explicit key, falling back to this wrapper's `name`,
|
|
157
|
+
* and merge it into a working copy of the caller's
|
|
156
158
|
* `modelOptions`. The caller always wins: if they already set any recognised
|
|
157
159
|
* token-limit key (flat or, for Ollama, nested `options.num_predict`), the
|
|
158
160
|
* default is left untouched. Unknown/unrecognised adapter names fall back to
|
|
@@ -172,10 +174,11 @@ function applyMaxLength(
|
|
|
172
174
|
adapterName: string,
|
|
173
175
|
maxLength: number,
|
|
174
176
|
modelOptions: Record<string, unknown>,
|
|
177
|
+
maxTokensKey?: string,
|
|
175
178
|
): Record<string, unknown> {
|
|
176
179
|
const merged: Record<string, unknown> = { ...modelOptions }
|
|
177
180
|
|
|
178
|
-
if (adapterName === 'ollama') {
|
|
181
|
+
if (adapterName === 'ollama' && maxTokensKey === undefined) {
|
|
179
182
|
// Honor a caller-set limit in either shape: a recognised flat key (e.g.
|
|
180
183
|
// left over from a migration) or the nested `options.num_predict`.
|
|
181
184
|
const callerSetFlatLimit = KNOWN_MAX_TOKENS_KEYS.some(
|
|
@@ -195,12 +198,12 @@ function applyMaxLength(
|
|
|
195
198
|
return merged
|
|
196
199
|
}
|
|
197
200
|
|
|
198
|
-
const key = MAX_TOKENS_KEY_BY_ADAPTER[adapterName]
|
|
201
|
+
const key = maxTokensKey ?? MAX_TOKENS_KEY_BY_ADAPTER[adapterName]
|
|
199
202
|
if (key === undefined) return merged
|
|
200
203
|
|
|
201
|
-
const callerSetLimit =
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
const callerSetLimit =
|
|
205
|
+
typeof merged[key] === 'number' ||
|
|
206
|
+
KNOWN_MAX_TOKENS_KEYS.some((k) => typeof merged[k] === 'number')
|
|
204
207
|
if (callerSetLimit) return merged
|
|
205
208
|
|
|
206
209
|
merged[key] = maxLength
|
|
@@ -372,17 +375,24 @@ export class ChatStreamSummarizeAdapter<
|
|
|
372
375
|
working = applyDefaultTemperature(this.name, 0.3, working)
|
|
373
376
|
// `maxLength` must reach the wire under the provider-native token key (it
|
|
374
377
|
// differs per provider, and no adapter reads a generic `maxTokens`).
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
// token limit.
|
|
378
|
+
// Prefer the wrapped adapter's explicit key, then this wrapper's name,
|
|
379
|
+
// never overriding a caller-supplied token limit.
|
|
378
380
|
if (options.maxLength !== undefined) {
|
|
379
|
-
if (
|
|
381
|
+
if (
|
|
382
|
+
this.textAdapter.maxTokensKey === undefined &&
|
|
383
|
+
!isKnownMaxTokensAdapter(this.name)
|
|
384
|
+
) {
|
|
380
385
|
options.logger.warn(
|
|
381
386
|
`summarize: maxLength=${options.maxLength} could not be mapped to a provider token key for adapter name "${this.name}" — it was dropped from modelOptions (the prompt still asks the model to stay under it). Construct ChatStreamSummarizeAdapter with a recognised provider name to forward the cap.`,
|
|
382
387
|
{ provider: this.name },
|
|
383
388
|
)
|
|
384
389
|
}
|
|
385
|
-
working = applyMaxLength(
|
|
390
|
+
working = applyMaxLength(
|
|
391
|
+
this.name,
|
|
392
|
+
options.maxLength,
|
|
393
|
+
working,
|
|
394
|
+
this.textAdapter.maxTokensKey,
|
|
395
|
+
)
|
|
386
396
|
}
|
|
387
397
|
const modelOptions = working as TProviderOptions
|
|
388
398
|
|
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,
|
|
@@ -364,6 +371,7 @@ export type {
|
|
|
364
371
|
ThinkingPart,
|
|
365
372
|
ToolCall,
|
|
366
373
|
ToolCallPart,
|
|
374
|
+
ToolResultOutcome,
|
|
367
375
|
ToolResultPart,
|
|
368
376
|
UIMessage,
|
|
369
377
|
UIResourcePart,
|
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
|
|
@@ -527,8 +535,17 @@ export type { WireMessage } from './utilities/ag-ui-wire'
|
|
|
527
535
|
export {
|
|
528
536
|
isContentPart,
|
|
529
537
|
isContentPartArray,
|
|
538
|
+
isToolResultOutcome,
|
|
530
539
|
normalizeToolResult,
|
|
531
540
|
} from './utilities/tool-result'
|
|
541
|
+
export {
|
|
542
|
+
assertMessagesFileSourceSupport,
|
|
543
|
+
assertPromptFileSourceSupport,
|
|
544
|
+
fileReferenceFor,
|
|
545
|
+
isFileSource,
|
|
546
|
+
unsupportedFileSourceError,
|
|
547
|
+
type FileSourceCapable,
|
|
548
|
+
} from './utilities/content-source'
|
|
532
549
|
|
|
533
550
|
export {
|
|
534
551
|
getProviderExecutedMetadata,
|
package/src/interrupt-resume.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
isStandardSchema,
|
|
15
15
|
validateWithStandardSchema,
|
|
16
16
|
} from './activities/chat/tools/schema-converter'
|
|
17
|
+
import { tanstackMetadata } from './utilities/merge-metadata'
|
|
17
18
|
import type {
|
|
18
19
|
InterruptBinding,
|
|
19
20
|
InterruptSubmissionError,
|
|
@@ -95,6 +96,33 @@ function stringField(
|
|
|
95
96
|
return typeof value[key] === 'string' ? value[key] : undefined
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
type ClientToolResumeResult =
|
|
100
|
+
| { state: 'output-available'; output: unknown }
|
|
101
|
+
| { state: 'output-error'; errorText: string }
|
|
102
|
+
|
|
103
|
+
function clientToolResult(
|
|
104
|
+
entry: RunAgentResumeItem,
|
|
105
|
+
): ClientToolResumeResult | null {
|
|
106
|
+
if (tanstackMetadata(entry)?.state === 'output-error') {
|
|
107
|
+
const result = objectValue(entry.payload)
|
|
108
|
+
if (
|
|
109
|
+
!result ||
|
|
110
|
+
Object.keys(result).length !== 1 ||
|
|
111
|
+
typeof result.error !== 'string'
|
|
112
|
+
) {
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
state: 'output-error',
|
|
117
|
+
errorText: result.error,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
state: 'output-available',
|
|
122
|
+
output: entry.payload,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
98
126
|
function normalizeIssuePath(
|
|
99
127
|
path: ReadonlyArray<unknown> | undefined,
|
|
100
128
|
): ReadonlyArray<string | number> | undefined {
|
|
@@ -528,7 +556,19 @@ export async function validateInterruptResumeBatch(
|
|
|
528
556
|
if (schemaDrifted) continue
|
|
529
557
|
|
|
530
558
|
if (binding.kind === 'client-tool-execution') {
|
|
531
|
-
|
|
559
|
+
const result = clientToolResult(entry)
|
|
560
|
+
if (!result) {
|
|
561
|
+
errors.push(
|
|
562
|
+
interruptItemError(
|
|
563
|
+
input,
|
|
564
|
+
record.interruptId,
|
|
565
|
+
'invalid-tool-output',
|
|
566
|
+
`Tool ${binding.toolName} result is invalid.`,
|
|
567
|
+
),
|
|
568
|
+
)
|
|
569
|
+
continue
|
|
570
|
+
}
|
|
571
|
+
if (result.state === 'output-available' && responseSchema !== undefined) {
|
|
532
572
|
await pushSchemaIssues({
|
|
533
573
|
request: input,
|
|
534
574
|
errors,
|
|
@@ -539,13 +579,16 @@ export async function validateInterruptResumeBatch(
|
|
|
539
579
|
label: `Tool ${binding.toolName} output is invalid`,
|
|
540
580
|
})
|
|
541
581
|
}
|
|
542
|
-
if (
|
|
582
|
+
if (
|
|
583
|
+
result.state === 'output-available' &&
|
|
584
|
+
tool.outputSchema !== undefined
|
|
585
|
+
) {
|
|
543
586
|
await pushSchemaIssues({
|
|
544
587
|
request: input,
|
|
545
588
|
errors,
|
|
546
589
|
interruptId: record.interruptId,
|
|
547
590
|
schema: tool.outputSchema,
|
|
548
|
-
value:
|
|
591
|
+
value: result.output,
|
|
549
592
|
code: 'invalid-tool-output',
|
|
550
593
|
label: `Tool ${binding.toolName} output is invalid`,
|
|
551
594
|
})
|
|
@@ -682,6 +725,7 @@ export async function validateInterruptResumeBatch(
|
|
|
682
725
|
const canonical = canonicalizeInterruptResolutions(input.resume ?? [])
|
|
683
726
|
const approvals = new Map<string, ToolApprovalResolution>()
|
|
684
727
|
const clientToolResults = new Map<string, unknown>()
|
|
728
|
+
const clientToolErrors = new Map<string, string>()
|
|
685
729
|
const genericInterrupts = new Map<
|
|
686
730
|
string,
|
|
687
731
|
| { interruptId: string; status: 'resolved'; payload: unknown }
|
|
@@ -738,7 +782,13 @@ export async function validateInterruptResumeBatch(
|
|
|
738
782
|
continue
|
|
739
783
|
}
|
|
740
784
|
if (binding.kind === 'client-tool-execution') {
|
|
741
|
-
|
|
785
|
+
const result = clientToolResult(entry)
|
|
786
|
+
if (!result) continue
|
|
787
|
+
if (result.state === 'output-error') {
|
|
788
|
+
clientToolErrors.set(binding.toolCallId, result.errorText)
|
|
789
|
+
} else {
|
|
790
|
+
clientToolResults.set(binding.toolCallId, result.output)
|
|
791
|
+
}
|
|
742
792
|
continue
|
|
743
793
|
}
|
|
744
794
|
const envelope = objectValue(entry.payload)
|
|
@@ -781,6 +831,7 @@ export async function validateInterruptResumeBatch(
|
|
|
781
831
|
resumeToolState: {
|
|
782
832
|
approvals,
|
|
783
833
|
clientToolResults,
|
|
834
|
+
clientToolErrors,
|
|
784
835
|
genericInterrupts,
|
|
785
836
|
deniedToolResults,
|
|
786
837
|
cancelledToolCallIds,
|
package/src/middlewares/otel.ts
CHANGED
|
@@ -108,7 +108,9 @@ export interface OtelMiddlewareOptions {
|
|
|
108
108
|
meter?: Meter
|
|
109
109
|
/**
|
|
110
110
|
* When `true`, prompt and completion content is attached to iteration spans
|
|
111
|
-
* as `gen_ai.*.message` / `gen_ai.choice` events.
|
|
111
|
+
* as `gen_ai.*.message` / `gen_ai.choice` events. Media spans get the
|
|
112
|
+
* prompt, input media and output (URLs or transcript text) as
|
|
113
|
+
* `gen_ai.input.messages` / `gen_ai.output.messages`. Defaults to `false` so
|
|
112
114
|
* that PII never lands on a span by accident.
|
|
113
115
|
*/
|
|
114
116
|
captureContent?: boolean
|
|
@@ -121,7 +123,8 @@ export interface OtelMiddlewareOptions {
|
|
|
121
123
|
redact?: (text: string) => string
|
|
122
124
|
/**
|
|
123
125
|
* Maximum characters kept in the per-iteration assistant text buffer used
|
|
124
|
-
* to emit `gen_ai.choice` events
|
|
126
|
+
* to emit `gen_ai.choice` events, and in each text part of a media span's
|
|
127
|
+
* input and output. Extra characters are truncated with a
|
|
125
128
|
* trailing `"…"` marker. Defaults to 100 000. Set to `0` to disable the
|
|
126
129
|
* cap. Exporters typically truncate long attribute values anyway.
|
|
127
130
|
*/
|
|
@@ -221,6 +224,111 @@ function serializeContent(content: unknown): string {
|
|
|
221
224
|
return parts.join(' ')
|
|
222
225
|
}
|
|
223
226
|
|
|
227
|
+
type InputPart =
|
|
228
|
+
| { type: 'text'; content: string }
|
|
229
|
+
| { type: 'uri'; modality: string; uri: string; mime_type?: string }
|
|
230
|
+
| { type: 'file'; modality: string; file_id: string; mime_type?: string }
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Structured form of `ContentPart[]` for `gen_ai.input.messages`, using the
|
|
234
|
+
* OTel GenAI semconv part shapes. URL and file-handle media keep their
|
|
235
|
+
* reference; inline bytes (and `data:` URLs) stay a `[type]` placeholder so
|
|
236
|
+
* they never blow attribute size limits. `redact` runs on text parts only.
|
|
237
|
+
*/
|
|
238
|
+
function serializeParts(
|
|
239
|
+
content: Array<unknown>,
|
|
240
|
+
redact: (text: string) => string,
|
|
241
|
+
): Array<InputPart> {
|
|
242
|
+
const parts: Array<InputPart> = []
|
|
243
|
+
for (const part of content) {
|
|
244
|
+
if (!part || typeof part !== 'object') continue
|
|
245
|
+
const p = part as {
|
|
246
|
+
type?: string
|
|
247
|
+
text?: string
|
|
248
|
+
content?: string
|
|
249
|
+
source?: { type?: string; value?: string; mimeType?: string }
|
|
250
|
+
}
|
|
251
|
+
if (p.type === 'text') {
|
|
252
|
+
parts.push({
|
|
253
|
+
type: 'text',
|
|
254
|
+
content: redact((p.text ?? p.content ?? '').toString()),
|
|
255
|
+
})
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
const modality = p.type ?? 'unknown'
|
|
259
|
+
const { type, value, mimeType } = p.source ?? {}
|
|
260
|
+
const mime = mimeType ? { mime_type: mimeType } : {}
|
|
261
|
+
if (type === 'url' && value && !value.startsWith('data:')) {
|
|
262
|
+
parts.push({ type: 'uri', modality, uri: value, ...mime })
|
|
263
|
+
} else if (type === 'file' && value) {
|
|
264
|
+
parts.push({ type: 'file', modality, file_id: value, ...mime })
|
|
265
|
+
} else {
|
|
266
|
+
parts.push({ type: 'text', content: `[${modality}]` })
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return parts
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** A media reference as a content part, so `serializeParts` can map it. */
|
|
273
|
+
function mediaPart(modality: string, url: unknown): unknown {
|
|
274
|
+
return {
|
|
275
|
+
type: modality,
|
|
276
|
+
source: typeof url === 'string' ? { type: 'url', value: url } : undefined,
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Content parts for a media call's inputs, read from `artifactInputs`: the
|
|
282
|
+
* prompt (a string or `MediaPrompt` parts), TTS `text`, and transcription
|
|
283
|
+
* `audio`. Audio is a reference only when it is an http(s) URL; a base64
|
|
284
|
+
* string, `File` or `Blob` becomes a placeholder.
|
|
285
|
+
*/
|
|
286
|
+
function mediaInputParts(inputs: unknown): Array<unknown> {
|
|
287
|
+
if (!inputs || typeof inputs !== 'object') return []
|
|
288
|
+
const { prompt, text, audio } = inputs as Record<string, unknown>
|
|
289
|
+
const parts: Array<unknown> = []
|
|
290
|
+
for (const value of [prompt, text]) {
|
|
291
|
+
if (typeof value === 'string') parts.push({ type: 'text', content: value })
|
|
292
|
+
else if (Array.isArray(value)) parts.push(...value)
|
|
293
|
+
}
|
|
294
|
+
if (audio !== undefined) {
|
|
295
|
+
const url =
|
|
296
|
+
typeof audio === 'string' && /^https?:\/\//.test(audio) ? audio : null
|
|
297
|
+
parts.push(mediaPart('audio', url))
|
|
298
|
+
}
|
|
299
|
+
return parts
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Content parts for a media call's result: generated image, audio or video
|
|
304
|
+
* URLs, and transcript text. Base64 output becomes a placeholder.
|
|
305
|
+
*/
|
|
306
|
+
function mediaOutputParts(
|
|
307
|
+
activity: GenerationActivity,
|
|
308
|
+
result: unknown,
|
|
309
|
+
): Array<unknown> {
|
|
310
|
+
if (!result || typeof result !== 'object') return []
|
|
311
|
+
const r = result as Record<string, unknown>
|
|
312
|
+
const parts: Array<unknown> = []
|
|
313
|
+
if (Array.isArray(r.images)) {
|
|
314
|
+
for (const image of r.images) {
|
|
315
|
+
parts.push(mediaPart('image', (image as { url?: unknown } | null)?.url))
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// `TTSResult.audio` is a base64 string; `AudioGenerationResult.audio` is a
|
|
319
|
+
// `{ url } | { b64Json }` source.
|
|
320
|
+
if (r.audio !== undefined) {
|
|
321
|
+
parts.push(mediaPart('audio', (r.audio as { url?: unknown } | null)?.url))
|
|
322
|
+
}
|
|
323
|
+
// Only video puts its asset on a top-level `url`. A world `url` is a viewer
|
|
324
|
+
// page, not media.
|
|
325
|
+
if (activity === 'video' && typeof r.url === 'string') {
|
|
326
|
+
parts.push(mediaPart('video', r.url))
|
|
327
|
+
}
|
|
328
|
+
if (typeof r.text === 'string') parts.push({ type: 'text', content: r.text })
|
|
329
|
+
return parts
|
|
330
|
+
}
|
|
331
|
+
|
|
224
332
|
function messageEventName(role: string): string {
|
|
225
333
|
switch (role) {
|
|
226
334
|
case 'user':
|
|
@@ -340,6 +448,29 @@ export function otelMiddleware(
|
|
|
340
448
|
})
|
|
341
449
|
}
|
|
342
450
|
|
|
451
|
+
// Media prompts and transcripts are single strings, so cap each text part
|
|
452
|
+
// with `maxContentLength` the same way the chat completion buffer is capped.
|
|
453
|
+
const redactMediaText = (text: string): string =>
|
|
454
|
+
redactContent(
|
|
455
|
+
maxContentLength > 0 && text.length > maxContentLength
|
|
456
|
+
? text.slice(0, maxContentLength) + '…'
|
|
457
|
+
: text,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
const setMediaMessages = (
|
|
461
|
+
span: Span,
|
|
462
|
+
direction: 'input' | 'output',
|
|
463
|
+
parts: Array<unknown>,
|
|
464
|
+
): void => {
|
|
465
|
+
const content = serializeParts(parts, redactMediaText)
|
|
466
|
+
if (content.length === 0) return
|
|
467
|
+
const json = JSON.stringify([
|
|
468
|
+
{ role: direction === 'input' ? 'user' : 'assistant', content },
|
|
469
|
+
])
|
|
470
|
+
span.setAttribute(`gen_ai.${direction}.messages`, json)
|
|
471
|
+
span.setAttribute(`langfuse.observation.${direction}`, json)
|
|
472
|
+
}
|
|
473
|
+
|
|
343
474
|
const startMediaSpan = (ctx: GenerationMiddlewareContext): void => {
|
|
344
475
|
safeCall('otel.onStart', () => {
|
|
345
476
|
const operationName = OPERATION_NAME[ctx.activity]
|
|
@@ -365,6 +496,22 @@ export function otelMiddleware(
|
|
|
365
496
|
)
|
|
366
497
|
if (enriched) span.setAttributes(enriched)
|
|
367
498
|
mediaSpans.set(ctx, span)
|
|
499
|
+
|
|
500
|
+
if (captureContent) {
|
|
501
|
+
setMediaMessages(span, 'input', mediaInputParts(ctx.artifactInputs))
|
|
502
|
+
// Observe the result through a transform: the terminal hooks never
|
|
503
|
+
// see it. Returns `undefined`, so the result is left unchanged.
|
|
504
|
+
ctx.resultTransforms.push((result) => {
|
|
505
|
+
safeCall('otel.captureOutput', () =>
|
|
506
|
+
setMediaMessages(
|
|
507
|
+
span,
|
|
508
|
+
'output',
|
|
509
|
+
mediaOutputParts(ctx.activity, result),
|
|
510
|
+
),
|
|
511
|
+
)
|
|
512
|
+
return undefined
|
|
513
|
+
})
|
|
514
|
+
}
|
|
368
515
|
})
|
|
369
516
|
}
|
|
370
517
|
|
|
@@ -581,7 +728,12 @@ export function otelMiddleware(
|
|
|
581
728
|
// Also emit the current GenAI-semconv attribute form
|
|
582
729
|
// (`gen_ai.input.messages`) — backends like PostHog read prompt
|
|
583
730
|
// content from this attribute, not from span events.
|
|
584
|
-
|
|
731
|
+
// Multimodal messages keep their parts structured so image / audio /
|
|
732
|
+
// video / document references survive into the trace (#1525).
|
|
733
|
+
const inputMessages: Array<{
|
|
734
|
+
role: string
|
|
735
|
+
content: string | Array<InputPart>
|
|
736
|
+
}> = []
|
|
585
737
|
for (const sys of systemPromptContents) {
|
|
586
738
|
inputMessages.push({
|
|
587
739
|
role: 'system',
|
|
@@ -589,6 +741,12 @@ export function otelMiddleware(
|
|
|
589
741
|
})
|
|
590
742
|
}
|
|
591
743
|
for (const m of config.messages) {
|
|
744
|
+
if (Array.isArray(m.content)) {
|
|
745
|
+
const parts = serializeParts(m.content, redactContent)
|
|
746
|
+
if (parts.length === 0) continue
|
|
747
|
+
inputMessages.push({ role: m.role, content: parts })
|
|
748
|
+
continue
|
|
749
|
+
}
|
|
592
750
|
const body = serializeContent(m.content)
|
|
593
751
|
if (body.length === 0) continue
|
|
594
752
|
inputMessages.push({
|