@tanstack/ai 0.61.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.
Files changed (65) 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 +8 -5
  6. package/dist/esm/activities/chat/agents/spawn.js.map +1 -1
  7. package/dist/esm/activities/chat/index.js +156 -57
  8. package/dist/esm/activities/chat/index.js.map +1 -1
  9. package/dist/esm/activities/chat/messages.js +30 -18
  10. package/dist/esm/activities/chat/messages.js.map +1 -1
  11. package/dist/esm/activities/chat/middleware/types.d.ts +1 -0
  12. package/dist/esm/activities/chat/middleware/types.js.map +1 -1
  13. package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
  14. package/dist/esm/activities/chat/stream/message-updaters.js +2 -1
  15. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  16. package/dist/esm/activities/chat/stream/processor.d.ts +11 -9
  17. package/dist/esm/activities/chat/stream/processor.js +38 -18
  18. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  19. package/dist/esm/activities/chat/tools/tool-calls.d.ts +16 -2
  20. package/dist/esm/activities/chat/tools/tool-calls.js +55 -16
  21. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  22. package/dist/esm/activities/chat/tools/tool-definition.d.ts +4 -0
  23. package/dist/esm/activities/chat/tools/tool-definition.js +4 -0
  24. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  25. package/dist/esm/activities/evaluate/adapter.d.ts +4 -0
  26. package/dist/esm/activities/evaluate/adapter.js.map +1 -1
  27. package/dist/esm/activities/evaluate/index.d.ts +4 -0
  28. package/dist/esm/activities/evaluate/index.js +3 -1
  29. package/dist/esm/activities/evaluate/index.js.map +1 -1
  30. package/dist/esm/client.d.ts +1 -1
  31. package/dist/esm/client.js.map +1 -1
  32. package/dist/esm/index.d.ts +1 -1
  33. package/dist/esm/index.js +2 -2
  34. package/dist/esm/interrupt-resume.js +29 -4
  35. package/dist/esm/interrupt-resume.js.map +1 -1
  36. package/dist/esm/middlewares/otel.d.ts +5 -2
  37. package/dist/esm/middlewares/otel.js +114 -0
  38. package/dist/esm/middlewares/otel.js.map +1 -1
  39. package/dist/esm/types.d.ts +42 -1
  40. package/dist/esm/utilities/ag-ui-wire.js +5 -3
  41. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  42. package/dist/esm/utilities/tool-result.d.ts +2 -1
  43. package/dist/esm/utilities/tool-result.js +4 -1
  44. package/dist/esm/utilities/tool-result.js.map +1 -1
  45. package/package.json +3 -3
  46. package/skills/ai-core/chat-experience/SKILL.md +120 -0
  47. package/skills/ai-core/tool-calling/SKILL.md +103 -0
  48. package/src/activities/chat/agents/define-agent.ts +20 -3
  49. package/src/activities/chat/agents/spawn.ts +20 -12
  50. package/src/activities/chat/index.ts +235 -62
  51. package/src/activities/chat/messages.ts +40 -5
  52. package/src/activities/chat/middleware/types.ts +1 -0
  53. package/src/activities/chat/stream/message-updaters.ts +3 -0
  54. package/src/activities/chat/stream/processor.ts +57 -27
  55. package/src/activities/chat/tools/tool-calls.ts +98 -9
  56. package/src/activities/chat/tools/tool-definition.ts +8 -0
  57. package/src/activities/evaluate/adapter.ts +4 -0
  58. package/src/activities/evaluate/index.ts +6 -0
  59. package/src/client.ts +1 -0
  60. package/src/index.ts +1 -0
  61. package/src/interrupt-resume.ts +55 -4
  62. package/src/middlewares/otel.ts +161 -3
  63. package/src/types.ts +38 -1
  64. package/src/utilities/ag-ui-wire.ts +12 -1
  65. package/src/utilities/tool-result.ts +7 -1
@@ -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
 
@@ -442,6 +454,8 @@ export interface ToolResultPart {
442
454
  toolCallId: string
443
455
  content: string | Array<ContentPart>
444
456
  state: ToolResultState
457
+ /** Set when the user or middleware cancelled or denied the tool call; state remains `error`. */
458
+ outcome?: ToolResultOutcome
445
459
  error?: string // Error message if state is "error"
446
460
  metadata?: Record<string, unknown>
447
461
  createdAt?: Date
@@ -560,6 +574,12 @@ export interface TanStackMessageMetadata {
560
574
  model?: string
561
575
  /** Parent chat run that produced this assistant message. */
562
576
  runId?: string
577
+ /**
578
+ * The chat run that produced this assistant message. `withPersistence` sets
579
+ * `id`. `reconstructChat` with `includeRuns: true` adds the finished run's
580
+ * timings, in epoch ms.
581
+ */
582
+ run?: { id: string; startedAt?: number; finishedAt?: number }
563
583
  /** Card data on a child wire message. See `uiMessagesToWire`. */
564
584
  subagent?: SubagentWireInfo
565
585
  /** Thinking signature for a `role: 'reasoning'` fan-out message. */
@@ -571,6 +591,8 @@ export interface TanStackMessageMetadata {
571
591
  createdAt?: string
572
592
  content?: Array<ContentPart>
573
593
  }
594
+ /** Outcome of a cancelled or denied tool result; when present, the UI state is `error`. */
595
+ toolResultOutcome?: ToolResultOutcome
574
596
  structuredOutput?: {
575
597
  status?: 'streaming' | 'complete' | 'error'
576
598
  partial?: unknown
@@ -679,6 +701,15 @@ export interface EmitCustomEventOptions {
679
701
  batch?: boolean
680
702
  }
681
703
 
704
+ /**
705
+ * The user's answer to an `mcp_input` interrupt.
706
+ * `resolved` carries the `payload` from `resolveInterrupt`.
707
+ * `cancelled` means the user called `cancel()`.
708
+ */
709
+ export type ToolInputResponse =
710
+ | { status: 'resolved'; payload: unknown }
711
+ | { status: 'cancelled' }
712
+
682
713
  /**
683
714
  * Context passed to tool execute functions, providing capabilities like
684
715
  * emitting custom events during execution.
@@ -693,6 +724,12 @@ export type ToolExecutionContext<TContext = unknown> =
693
724
  * e.g. MCP `callTool` — should forward this to cancel in-flight work.
694
725
  */
695
726
  abortSignal?: AbortSignal
727
+ /**
728
+ * The answer to the input request that this tool call raised in the
729
+ * previous run. It is set only when the run resumes an `mcp_input`
730
+ * interrupt for this tool call.
731
+ */
732
+ inputResponse?: ToolInputResponse
696
733
  /**
697
734
  * Emit a custom event during tool execution.
698
735
  * Events are streamed to the client in real-time as AG-UI CUSTOM events.
@@ -14,6 +14,7 @@ import type {
14
14
  StructuredOutputPart,
15
15
  SubagentPart,
16
16
  TanStackMessageMetadata,
17
+ ToolResultOutcome,
17
18
  UIMessage,
18
19
  UIResourcePart,
19
20
  } from '../types'
@@ -49,6 +50,7 @@ function rebuiltToolMetadata(
49
50
  id: string | undefined,
50
51
  content: string | null | Array<ContentPart>,
51
52
  anchorOwnsUiResources = false,
53
+ outcome?: ToolResultOutcome,
52
54
  ): MetadataRecord | undefined {
53
55
  const source: MetadataRecord = isRecord(metadata) ? metadata : {}
54
56
  const tanstack = isRecord(source.tanstack) ? { ...source.tanstack } : {}
@@ -61,7 +63,11 @@ function rebuiltToolMetadata(
61
63
  }
62
64
  const result = {
63
65
  ...source,
64
- tanstack: { ...tanstack, toolResult },
66
+ tanstack: {
67
+ ...tanstack,
68
+ ...(outcome !== undefined && { toolResultOutcome: outcome }),
69
+ toolResult,
70
+ },
65
71
  }
66
72
  return Object.keys(result).length ? result : undefined
67
73
  }
@@ -262,6 +268,7 @@ export function uiMessagesToWire(
262
268
  part.id,
263
269
  part.content,
264
270
  true,
271
+ part.outcome,
265
272
  )
266
273
  wire.push({
267
274
  role: 'tool',
@@ -306,6 +313,8 @@ export function uiMessagesToWire(
306
313
  undefined,
307
314
  undefined,
308
315
  result,
316
+ false,
317
+ part.approval?.approved === false ? 'denied' : undefined,
309
318
  ),
310
319
  })
311
320
  }
@@ -385,6 +394,8 @@ function messageMetadata(
385
394
  tanstack.model = previousTanstack.model
386
395
  if (previousTanstack?.runId !== undefined)
387
396
  tanstack.runId = previousTanstack.runId
397
+ if (previousTanstack?.run?.id !== undefined)
398
+ tanstack.run = { id: previousTanstack.run.id }
388
399
  if (previousTanstack?.signature !== undefined)
389
400
  tanstack.signature = previousTanstack.signature
390
401
  const createdAt = coerceCreatedAt(msg.createdAt)
@@ -1,4 +1,4 @@
1
- import type { ContentPart } from '../types'
1
+ import type { ContentPart, ToolResultOutcome } from '../types'
2
2
 
3
3
  const CONTENT_PART_TYPES = new Set([
4
4
  'text',
@@ -8,6 +8,12 @@ const CONTENT_PART_TYPES = new Set([
8
8
  'document',
9
9
  ])
10
10
 
11
+ export function isToolResultOutcome(
12
+ value: unknown,
13
+ ): value is ToolResultOutcome {
14
+ return value === 'cancelled' || value === 'denied'
15
+ }
16
+
11
17
  /**
12
18
  * Structural check for a single `ContentPart`. A text part must carry a string
13
19
  * `content`. Every other part carries a source with a string `value`; a file