@tanstack/ai 0.61.0 → 0.64.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/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 +10 -33
- package/dist/esm/activities/chat/agents/spawn.js.map +1 -1
- package/dist/esm/activities/chat/index.d.ts +2 -0
- package/dist/esm/activities/chat/index.js +166 -60
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +43 -22
- 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 +6 -2
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +11 -9
- package/dist/esm/activities/chat/stream/processor.js +38 -18
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.d.ts +8 -0
- package/dist/esm/activities/chat/tools/schema-converter.js +6 -5
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +20 -3
- package/dist/esm/activities/chat/tools/tool-calls.js +126 -36
- 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/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/generateSpeech/index.d.ts +1 -1
- package/dist/esm/activities/generateSpeech/index.js +1 -1
- package/dist/esm/activities/generateSpeech/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/adapter.d.ts +14 -6
- package/dist/esm/activities/generateVideo/adapter.js +6 -3
- package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.d.ts +5 -4
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/snap.d.ts +12 -3
- package/dist/esm/activities/generateVideo/snap.js +47 -8
- package/dist/esm/activities/generateVideo/snap.js.map +1 -1
- package/dist/esm/activities/generateVoice/index.d.ts +1 -1
- package/dist/esm/activities/generateVoice/index.js +1 -1
- package/dist/esm/activities/generateVoice/index.js.map +1 -1
- package/dist/esm/activities/index.d.ts +2 -2
- package/dist/esm/activities/index.js +2 -2
- package/dist/esm/adapter-internals.d.ts +1 -0
- package/dist/esm/adapter-internals.js +2 -1
- package/dist/esm/client.d.ts +1 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +2 -2
- 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 +70 -4
- package/dist/esm/utilities/ag-ui-wire.js +8 -4
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/merge-streams.d.ts +6 -0
- package/dist/esm/utilities/merge-streams.js +37 -0
- package/dist/esm/utilities/merge-streams.js.map +1 -0
- package/dist/esm/utilities/reasoning-encrypted-value.d.ts +8 -0
- package/dist/esm/utilities/reasoning-encrypted-value.js +11 -1
- package/dist/esm/utilities/reasoning-encrypted-value.js.map +1 -1
- package/dist/esm/utilities/tool-result.d.ts +2 -1
- package/dist/esm/utilities/tool-result.js +4 -1
- package/dist/esm/utilities/tool-result.js.map +1 -1
- package/package.json +3 -3
- package/skills/ai-core/chat-experience/SKILL.md +120 -0
- package/skills/ai-core/media-generation/SKILL.md +3 -3
- package/skills/ai-core/tool-calling/SKILL.md +103 -0
- package/src/activities/chat/agents/define-agent.ts +20 -3
- package/src/activities/chat/agents/spawn.ts +22 -43
- package/src/activities/chat/index.ts +248 -65
- package/src/activities/chat/messages.ts +54 -7
- package/src/activities/chat/middleware/types.ts +1 -0
- package/src/activities/chat/stream/message-updaters.ts +8 -0
- package/src/activities/chat/stream/processor.ts +57 -27
- package/src/activities/chat/tools/schema-converter.ts +17 -5
- package/src/activities/chat/tools/tool-calls.ts +215 -68
- package/src/activities/chat/tools/tool-definition.ts +8 -0
- package/src/activities/evaluate/adapter.ts +4 -0
- package/src/activities/evaluate/index.ts +6 -0
- package/src/activities/generateSpeech/index.ts +1 -1
- package/src/activities/generateVideo/adapter.ts +21 -7
- package/src/activities/generateVideo/index.ts +5 -4
- package/src/activities/generateVideo/snap.ts +64 -6
- package/src/activities/generateVoice/index.ts +1 -1
- package/src/activities/index.ts +2 -1
- package/src/adapter-internals.ts +1 -0
- package/src/client.ts +1 -0
- package/src/index.ts +1 -0
- package/src/interrupt-resume.ts +55 -4
- package/src/middlewares/otel.ts +161 -3
- package/src/types.ts +66 -5
- package/src/utilities/ag-ui-wire.ts +17 -2
- package/src/utilities/merge-streams.ts +34 -0
- package/src/utilities/reasoning-encrypted-value.ts +12 -0
- package/src/utilities/tool-result.ts +7 -1
|
@@ -1,8 +1,38 @@
|
|
|
1
1
|
import type { DurationOptions } from './adapter'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* `"6"`, `"6s"`, and `"6.5s"` are seconds. Anything else (`"auto"`) is a
|
|
5
|
+
* keyword the model must list exactly.
|
|
6
|
+
*/
|
|
7
|
+
const DURATION_TEMPLATE = /^(\d+(?:\.\d+)?)s?$/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Seconds from a caller-supplied template, or `null` when `input` is a
|
|
11
|
+
* keyword (`"auto"`) rather than a length.
|
|
12
|
+
*/
|
|
13
|
+
function templateToSeconds(input: string): number | null {
|
|
14
|
+
const match = DURATION_TEMPLATE.exec(input)
|
|
15
|
+
const digits = match?.[1]
|
|
16
|
+
if (digits === undefined) return null
|
|
17
|
+
const seconds = Number(digits)
|
|
18
|
+
return Number.isFinite(seconds) ? seconds : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Seconds from a duration a caller wrote: `6`, `"6"`, or `"6s"`.
|
|
23
|
+
* Returns `undefined` for keywords such as `"auto"` and for non-finite numbers.
|
|
24
|
+
*/
|
|
25
|
+
export function durationToSeconds(input: number | string): number | undefined {
|
|
26
|
+
if (typeof input === 'number') {
|
|
27
|
+
return Number.isFinite(input) ? input : undefined
|
|
28
|
+
}
|
|
29
|
+
const seconds = templateToSeconds(input)
|
|
30
|
+
return seconds === null ? undefined : seconds
|
|
31
|
+
}
|
|
32
|
+
|
|
3
33
|
/**
|
|
4
34
|
* Extract a numeric seconds value from a `DurationOptions` entry. Returns
|
|
5
|
-
* `null` for entries that don't parse as a number
|
|
35
|
+
* `null` for entries that don't parse as a number, for example `'auto'`.
|
|
6
36
|
*
|
|
7
37
|
* Handles the keyword-with-unit form FAL uses for Luma/Veo (`'8s'`, `'9s'`)
|
|
8
38
|
* by stripping a trailing `s`. Pure-numeric strings (`'5'`, `'10'`) parse via
|
|
@@ -12,14 +42,16 @@ function entryToSeconds(entry: string | number): number | null {
|
|
|
12
42
|
if (typeof entry === 'number') {
|
|
13
43
|
return Number.isFinite(entry) ? entry : null
|
|
14
44
|
}
|
|
15
|
-
|
|
16
|
-
const parsed = Number(stripped)
|
|
17
|
-
return Number.isFinite(parsed) ? parsed : null
|
|
45
|
+
return templateToSeconds(entry)
|
|
18
46
|
}
|
|
19
47
|
|
|
20
48
|
/**
|
|
21
|
-
* Snap a
|
|
22
|
-
*
|
|
49
|
+
* Snap a caller duration to the closest valid option.
|
|
50
|
+
*
|
|
51
|
+
* `input` may be seconds (`7`), a numeric string (`"7"`), a template
|
|
52
|
+
* (`"6s"`), or a keyword the model lists (`"auto"`). A keyword that is not
|
|
53
|
+
* in the set returns `undefined`. Equal numeric distances keep the earlier
|
|
54
|
+
* option.
|
|
23
55
|
*
|
|
24
56
|
* - `none` → `undefined`
|
|
25
57
|
* - `discrete` → closest numeric-parseable entry; if none parse,
|
|
@@ -30,6 +62,32 @@ function entryToSeconds(entry: string | number): number | null {
|
|
|
30
62
|
* @experimental Video generation is an experimental feature and may change.
|
|
31
63
|
*/
|
|
32
64
|
export function snapToDurationOption<T extends string | number | undefined>(
|
|
65
|
+
input: number | string,
|
|
66
|
+
options: DurationOptions<T>,
|
|
67
|
+
): T | undefined {
|
|
68
|
+
// NaN is not a length. Infinity still clamps inside snapSeconds.
|
|
69
|
+
if (typeof input === 'number' && Number.isNaN(input)) return undefined
|
|
70
|
+
|
|
71
|
+
if (typeof input === 'string') {
|
|
72
|
+
const seconds = templateToSeconds(input)
|
|
73
|
+
if (seconds === null) return matchKeyword(input, options)
|
|
74
|
+
return snapSeconds(seconds, options)
|
|
75
|
+
}
|
|
76
|
+
return snapSeconds(input, options)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function matchKeyword<T extends string | number | undefined>(
|
|
80
|
+
keyword: string,
|
|
81
|
+
options: DurationOptions<T>,
|
|
82
|
+
): T | undefined {
|
|
83
|
+
if (options.kind !== 'discrete' && options.kind !== 'mixed') return undefined
|
|
84
|
+
for (const value of options.values) {
|
|
85
|
+
if (value === keyword) return value
|
|
86
|
+
}
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function snapSeconds<T extends string | number | undefined>(
|
|
33
91
|
seconds: number,
|
|
34
92
|
options: DurationOptions<T>,
|
|
35
93
|
): T | undefined {
|
|
@@ -166,7 +166,7 @@ function createId(prefix: string): string {
|
|
|
166
166
|
* if (!preview) throw new Error('No voice candidates returned')
|
|
167
167
|
*
|
|
168
168
|
* const speech = await generateSpeech({
|
|
169
|
-
* adapter: elevenlabsSpeech('
|
|
169
|
+
* adapter: elevenlabsSpeech('eleven_v4'),
|
|
170
170
|
* text: 'Once upon a time...',
|
|
171
171
|
* voice: preview.voiceId,
|
|
172
172
|
* })
|
package/src/activities/index.ts
CHANGED
|
@@ -209,9 +209,10 @@ export {
|
|
|
209
209
|
type VideoAdapterConfig,
|
|
210
210
|
type AnyVideoAdapter,
|
|
211
211
|
type DurationOptions,
|
|
212
|
+
type VideoDurationSpell,
|
|
212
213
|
} from './generateVideo/adapter'
|
|
213
214
|
|
|
214
|
-
export { snapToDurationOption } from './generateVideo/snap'
|
|
215
|
+
export { durationToSeconds, snapToDurationOption } from './generateVideo/snap'
|
|
215
216
|
|
|
216
217
|
// ===========================
|
|
217
218
|
// TTS Activity
|
package/src/adapter-internals.ts
CHANGED
|
@@ -59,3 +59,4 @@ export {
|
|
|
59
59
|
} from './utilities/structured-output-events'
|
|
60
60
|
export { tanstackMetadata } from './utilities/merge-metadata'
|
|
61
61
|
export { isSpecTopLevelKey } from './utilities/spec-event-keys'
|
|
62
|
+
export { REDACTED_THINKING_ID_PREFIX } from './utilities/reasoning-encrypted-value'
|
package/src/client.ts
CHANGED
package/src/index.ts
CHANGED
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({
|
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
|
|
|
@@ -369,7 +381,12 @@ export interface ModelMessage<
|
|
|
369
381
|
name?: string
|
|
370
382
|
toolCalls?: Array<ToolCall>
|
|
371
383
|
toolCallId?: string
|
|
372
|
-
|
|
384
|
+
/**
|
|
385
|
+
* Signed thinking to send back to the provider. `redacted: true` marks a
|
|
386
|
+
* block the provider encrypted: `content` is empty and `signature` holds its
|
|
387
|
+
* opaque data. See `ThinkingPart.signature` for the planned rename.
|
|
388
|
+
*/
|
|
389
|
+
thinking?: Array<{ content: string; signature?: string; redacted?: boolean }>
|
|
373
390
|
/** Error reported by an AG-UI tool message. */
|
|
374
391
|
error?: string
|
|
375
392
|
/** Optional AG-UI message metadata. TanStack-owned fields live under `tanstack`. */
|
|
@@ -442,6 +459,8 @@ export interface ToolResultPart {
|
|
|
442
459
|
toolCallId: string
|
|
443
460
|
content: string | Array<ContentPart>
|
|
444
461
|
state: ToolResultState
|
|
462
|
+
/** Set when the user or middleware cancelled or denied the tool call; state remains `error`. */
|
|
463
|
+
outcome?: ToolResultOutcome
|
|
445
464
|
error?: string // Error message if state is "error"
|
|
446
465
|
metadata?: Record<string, unknown>
|
|
447
466
|
createdAt?: Date
|
|
@@ -451,7 +470,20 @@ export interface ThinkingPart {
|
|
|
451
470
|
type: 'thinking'
|
|
452
471
|
content: string
|
|
453
472
|
stepId?: string
|
|
473
|
+
/**
|
|
474
|
+
* The provider's opaque reasoning artefact, sent back unchanged: an
|
|
475
|
+
* Anthropic signature, Anthropic redacted data, or OpenAI encrypted content.
|
|
476
|
+
* TODO(#1581): rename to `encryptedValue` to match AG-UI's `ReasoningMessage`.
|
|
477
|
+
* Renaming breaks stored messages, so it needs a read shim for `signature`.
|
|
478
|
+
*/
|
|
454
479
|
signature?: string
|
|
480
|
+
/**
|
|
481
|
+
* The provider encrypted this thinking block (Anthropic `redacted_thinking`).
|
|
482
|
+
* `content` is empty, and `signature` holds the opaque data that goes back
|
|
483
|
+
* to the provider unchanged. On the AG-UI wire, the reasoning message id
|
|
484
|
+
* starts with `redacted_thinking-` instead.
|
|
485
|
+
*/
|
|
486
|
+
redacted?: boolean
|
|
455
487
|
}
|
|
456
488
|
|
|
457
489
|
/**
|
|
@@ -560,6 +592,12 @@ export interface TanStackMessageMetadata {
|
|
|
560
592
|
model?: string
|
|
561
593
|
/** Parent chat run that produced this assistant message. */
|
|
562
594
|
runId?: string
|
|
595
|
+
/**
|
|
596
|
+
* The chat run that produced this assistant message. `withPersistence` sets
|
|
597
|
+
* `id`. `reconstructChat` with `includeRuns: true` adds the finished run's
|
|
598
|
+
* timings, in epoch ms.
|
|
599
|
+
*/
|
|
600
|
+
run?: { id: string; startedAt?: number; finishedAt?: number }
|
|
563
601
|
/** Card data on a child wire message. See `uiMessagesToWire`. */
|
|
564
602
|
subagent?: SubagentWireInfo
|
|
565
603
|
/** Thinking signature for a `role: 'reasoning'` fan-out message. */
|
|
@@ -571,6 +609,8 @@ export interface TanStackMessageMetadata {
|
|
|
571
609
|
createdAt?: string
|
|
572
610
|
content?: Array<ContentPart>
|
|
573
611
|
}
|
|
612
|
+
/** Outcome of a cancelled or denied tool result; when present, the UI state is `error`. */
|
|
613
|
+
toolResultOutcome?: ToolResultOutcome
|
|
574
614
|
structuredOutput?: {
|
|
575
615
|
status?: 'streaming' | 'complete' | 'error'
|
|
576
616
|
partial?: unknown
|
|
@@ -679,6 +719,15 @@ export interface EmitCustomEventOptions {
|
|
|
679
719
|
batch?: boolean
|
|
680
720
|
}
|
|
681
721
|
|
|
722
|
+
/**
|
|
723
|
+
* The user's answer to an `mcp_input` interrupt.
|
|
724
|
+
* `resolved` carries the `payload` from `resolveInterrupt`.
|
|
725
|
+
* `cancelled` means the user called `cancel()`.
|
|
726
|
+
*/
|
|
727
|
+
export type ToolInputResponse =
|
|
728
|
+
| { status: 'resolved'; payload: unknown }
|
|
729
|
+
| { status: 'cancelled' }
|
|
730
|
+
|
|
682
731
|
/**
|
|
683
732
|
* Context passed to tool execute functions, providing capabilities like
|
|
684
733
|
* emitting custom events during execution.
|
|
@@ -693,6 +742,12 @@ export type ToolExecutionContext<TContext = unknown> =
|
|
|
693
742
|
* e.g. MCP `callTool` — should forward this to cancel in-flight work.
|
|
694
743
|
*/
|
|
695
744
|
abortSignal?: AbortSignal
|
|
745
|
+
/**
|
|
746
|
+
* The answer to the input request that this tool call raised in the
|
|
747
|
+
* previous run. It is set only when the run resumes an `mcp_input`
|
|
748
|
+
* interrupt for this tool call.
|
|
749
|
+
*/
|
|
750
|
+
inputResponse?: ToolInputResponse
|
|
696
751
|
/**
|
|
697
752
|
* Emit a custom event during tool execution.
|
|
698
753
|
* Events are streamed to the client in real-time as AG-UI CUSTOM events.
|
|
@@ -1051,6 +1106,11 @@ export interface TextOptions<
|
|
|
1051
1106
|
*/
|
|
1052
1107
|
systemPrompts?: Array<SystemPrompt>
|
|
1053
1108
|
agentLoopStrategy?: AgentLoopStrategy
|
|
1109
|
+
/**
|
|
1110
|
+
* How the server tools of one model turn run. `'parallel'` (the default)
|
|
1111
|
+
* starts them together, and `'sequential'` runs them one at a time.
|
|
1112
|
+
*/
|
|
1113
|
+
toolExecution?: 'parallel' | 'sequential'
|
|
1054
1114
|
/**
|
|
1055
1115
|
* Optional configuration for lazy-tool discovery (tools marked `lazy: true`).
|
|
1056
1116
|
* Tunes how much of each lazy tool's description appears in the discovery
|
|
@@ -2240,9 +2300,10 @@ export interface VideoGenerationOptions<
|
|
|
2240
2300
|
/** Video size — format depends on the provider (e.g., "16:9", "1280x720") */
|
|
2241
2301
|
size?: TSize
|
|
2242
2302
|
/**
|
|
2243
|
-
* Video duration
|
|
2244
|
-
*
|
|
2245
|
-
* `adapter.snapDuration(
|
|
2303
|
+
* Video duration. Adapters that declare a per-model duration map narrow
|
|
2304
|
+
* this to that model's union (a number, `"8"`, or `"8s"`). Use
|
|
2305
|
+
* `adapter.snapDuration(input)` to coerce a raw value. `input` may be
|
|
2306
|
+
* seconds, a `"6s"` template, or `"auto"` when the model lists it.
|
|
2246
2307
|
*/
|
|
2247
2308
|
duration?: TDuration
|
|
2248
2309
|
/** Model-specific options for video generation */
|