@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.
Files changed (104) hide show
  1. package/README.md +1 -0
  2. package/dist/esm/activities/chat/agents/define-agent.d.ts +17 -5
  3. package/dist/esm/activities/chat/agents/define-agent.js.map +1 -1
  4. package/dist/esm/activities/chat/agents/spawn.d.ts +2 -0
  5. package/dist/esm/activities/chat/agents/spawn.js +10 -33
  6. package/dist/esm/activities/chat/agents/spawn.js.map +1 -1
  7. package/dist/esm/activities/chat/index.d.ts +2 -0
  8. package/dist/esm/activities/chat/index.js +166 -60
  9. package/dist/esm/activities/chat/index.js.map +1 -1
  10. package/dist/esm/activities/chat/messages.js +43 -22
  11. package/dist/esm/activities/chat/messages.js.map +1 -1
  12. package/dist/esm/activities/chat/middleware/types.d.ts +1 -0
  13. package/dist/esm/activities/chat/middleware/types.js.map +1 -1
  14. package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
  15. package/dist/esm/activities/chat/stream/message-updaters.js +6 -2
  16. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  17. package/dist/esm/activities/chat/stream/processor.d.ts +11 -9
  18. package/dist/esm/activities/chat/stream/processor.js +38 -18
  19. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  20. package/dist/esm/activities/chat/tools/schema-converter.d.ts +8 -0
  21. package/dist/esm/activities/chat/tools/schema-converter.js +6 -5
  22. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  23. package/dist/esm/activities/chat/tools/tool-calls.d.ts +20 -3
  24. package/dist/esm/activities/chat/tools/tool-calls.js +126 -36
  25. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  26. package/dist/esm/activities/chat/tools/tool-definition.d.ts +4 -0
  27. package/dist/esm/activities/chat/tools/tool-definition.js +4 -0
  28. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  29. package/dist/esm/activities/evaluate/adapter.d.ts +4 -0
  30. package/dist/esm/activities/evaluate/adapter.js.map +1 -1
  31. package/dist/esm/activities/evaluate/index.d.ts +4 -0
  32. package/dist/esm/activities/evaluate/index.js +3 -1
  33. package/dist/esm/activities/evaluate/index.js.map +1 -1
  34. package/dist/esm/activities/generateSpeech/index.d.ts +1 -1
  35. package/dist/esm/activities/generateSpeech/index.js +1 -1
  36. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  37. package/dist/esm/activities/generateVideo/adapter.d.ts +14 -6
  38. package/dist/esm/activities/generateVideo/adapter.js +6 -3
  39. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  40. package/dist/esm/activities/generateVideo/index.d.ts +5 -4
  41. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  42. package/dist/esm/activities/generateVideo/snap.d.ts +12 -3
  43. package/dist/esm/activities/generateVideo/snap.js +47 -8
  44. package/dist/esm/activities/generateVideo/snap.js.map +1 -1
  45. package/dist/esm/activities/generateVoice/index.d.ts +1 -1
  46. package/dist/esm/activities/generateVoice/index.js +1 -1
  47. package/dist/esm/activities/generateVoice/index.js.map +1 -1
  48. package/dist/esm/activities/index.d.ts +2 -2
  49. package/dist/esm/activities/index.js +2 -2
  50. package/dist/esm/adapter-internals.d.ts +1 -0
  51. package/dist/esm/adapter-internals.js +2 -1
  52. package/dist/esm/client.d.ts +1 -1
  53. package/dist/esm/client.js.map +1 -1
  54. package/dist/esm/index.d.ts +1 -1
  55. package/dist/esm/index.js +2 -2
  56. package/dist/esm/interrupt-resume.js +29 -4
  57. package/dist/esm/interrupt-resume.js.map +1 -1
  58. package/dist/esm/middlewares/otel.d.ts +5 -2
  59. package/dist/esm/middlewares/otel.js +114 -0
  60. package/dist/esm/middlewares/otel.js.map +1 -1
  61. package/dist/esm/types.d.ts +70 -4
  62. package/dist/esm/utilities/ag-ui-wire.js +8 -4
  63. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  64. package/dist/esm/utilities/merge-streams.d.ts +6 -0
  65. package/dist/esm/utilities/merge-streams.js +37 -0
  66. package/dist/esm/utilities/merge-streams.js.map +1 -0
  67. package/dist/esm/utilities/reasoning-encrypted-value.d.ts +8 -0
  68. package/dist/esm/utilities/reasoning-encrypted-value.js +11 -1
  69. package/dist/esm/utilities/reasoning-encrypted-value.js.map +1 -1
  70. package/dist/esm/utilities/tool-result.d.ts +2 -1
  71. package/dist/esm/utilities/tool-result.js +4 -1
  72. package/dist/esm/utilities/tool-result.js.map +1 -1
  73. package/package.json +3 -3
  74. package/skills/ai-core/chat-experience/SKILL.md +120 -0
  75. package/skills/ai-core/media-generation/SKILL.md +3 -3
  76. package/skills/ai-core/tool-calling/SKILL.md +103 -0
  77. package/src/activities/chat/agents/define-agent.ts +20 -3
  78. package/src/activities/chat/agents/spawn.ts +22 -43
  79. package/src/activities/chat/index.ts +248 -65
  80. package/src/activities/chat/messages.ts +54 -7
  81. package/src/activities/chat/middleware/types.ts +1 -0
  82. package/src/activities/chat/stream/message-updaters.ts +8 -0
  83. package/src/activities/chat/stream/processor.ts +57 -27
  84. package/src/activities/chat/tools/schema-converter.ts +17 -5
  85. package/src/activities/chat/tools/tool-calls.ts +215 -68
  86. package/src/activities/chat/tools/tool-definition.ts +8 -0
  87. package/src/activities/evaluate/adapter.ts +4 -0
  88. package/src/activities/evaluate/index.ts +6 -0
  89. package/src/activities/generateSpeech/index.ts +1 -1
  90. package/src/activities/generateVideo/adapter.ts +21 -7
  91. package/src/activities/generateVideo/index.ts +5 -4
  92. package/src/activities/generateVideo/snap.ts +64 -6
  93. package/src/activities/generateVoice/index.ts +1 -1
  94. package/src/activities/index.ts +2 -1
  95. package/src/adapter-internals.ts +1 -0
  96. package/src/client.ts +1 -0
  97. package/src/index.ts +1 -0
  98. package/src/interrupt-resume.ts +55 -4
  99. package/src/middlewares/otel.ts +161 -3
  100. package/src/types.ts +66 -5
  101. package/src/utilities/ag-ui-wire.ts +17 -2
  102. package/src/utilities/merge-streams.ts +34 -0
  103. package/src/utilities/reasoning-encrypted-value.ts +12 -0
  104. 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 — e.g. `'auto'`.
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
- const stripped = entry.endsWith('s') ? entry.slice(0, -1) : entry
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 raw seconds value to the closest valid duration for a model's
22
- * `DurationOptions`.
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('eleven_v3'),
169
+ * adapter: elevenlabsSpeech('eleven_v4'),
170
170
  * text: 'Once upon a time...',
171
171
  * voice: preview.voiceId,
172
172
  * })
@@ -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
@@ -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
@@ -371,6 +371,7 @@ export type {
371
371
  ThinkingPart,
372
372
  ToolCall,
373
373
  ToolCallPart,
374
+ ToolResultOutcome,
374
375
  ToolResultPart,
375
376
  UIMessage,
376
377
  UIResourcePart,
package/src/index.ts CHANGED
@@ -535,6 +535,7 @@ export type { WireMessage } from './utilities/ag-ui-wire'
535
535
  export {
536
536
  isContentPart,
537
537
  isContentPartArray,
538
+ isToolResultOutcome,
538
539
  normalizeToolResult,
539
540
  } from './utilities/tool-result'
540
541
  export {
@@ -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
- if (responseSchema !== undefined) {
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 (tool.outputSchema !== undefined) {
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: entry.payload,
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
- clientToolResults.set(binding.toolCallId, entry.payload)
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,
@@ -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. Defaults to `false` so
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. Extra characters are truncated with a
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
- const inputMessages: Array<{ role: string; content: string }> = []
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
- thinking?: Array<{ content: string; signature?: string }>
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 in seconds. Adapters that declare a per-model duration
2244
- * map narrow this to the model's valid union; use
2245
- * `adapter.snapDuration(seconds)` to coerce raw seconds to a valid value.
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 */