@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
@@ -72,6 +72,94 @@ function serializeContent(content) {
72
72
  }
73
73
  return parts.join(" ");
74
74
  }
75
+ /**
76
+ * Structured form of `ContentPart[]` for `gen_ai.input.messages`, using the
77
+ * OTel GenAI semconv part shapes. URL and file-handle media keep their
78
+ * reference; inline bytes (and `data:` URLs) stay a `[type]` placeholder so
79
+ * they never blow attribute size limits. `redact` runs on text parts only.
80
+ */
81
+ function serializeParts(content, redact) {
82
+ const parts = [];
83
+ for (const part of content) {
84
+ if (!part || typeof part !== "object") continue;
85
+ const p = part;
86
+ if (p.type === "text") {
87
+ parts.push({
88
+ type: "text",
89
+ content: redact((p.text ?? p.content ?? "").toString())
90
+ });
91
+ continue;
92
+ }
93
+ const modality = p.type ?? "unknown";
94
+ const { type, value, mimeType } = p.source ?? {};
95
+ const mime = mimeType ? { mime_type: mimeType } : {};
96
+ if (type === "url" && value && !value.startsWith("data:")) parts.push({
97
+ type: "uri",
98
+ modality,
99
+ uri: value,
100
+ ...mime
101
+ });
102
+ else if (type === "file" && value) parts.push({
103
+ type: "file",
104
+ modality,
105
+ file_id: value,
106
+ ...mime
107
+ });
108
+ else parts.push({
109
+ type: "text",
110
+ content: `[${modality}]`
111
+ });
112
+ }
113
+ return parts;
114
+ }
115
+ /** A media reference as a content part, so `serializeParts` can map it. */
116
+ function mediaPart(modality, url) {
117
+ return {
118
+ type: modality,
119
+ source: typeof url === "string" ? {
120
+ type: "url",
121
+ value: url
122
+ } : void 0
123
+ };
124
+ }
125
+ /**
126
+ * Content parts for a media call's inputs, read from `artifactInputs`: the
127
+ * prompt (a string or `MediaPrompt` parts), TTS `text`, and transcription
128
+ * `audio`. Audio is a reference only when it is an http(s) URL; a base64
129
+ * string, `File` or `Blob` becomes a placeholder.
130
+ */
131
+ function mediaInputParts(inputs) {
132
+ if (!inputs || typeof inputs !== "object") return [];
133
+ const { prompt, text, audio } = inputs;
134
+ const parts = [];
135
+ for (const value of [prompt, text]) if (typeof value === "string") parts.push({
136
+ type: "text",
137
+ content: value
138
+ });
139
+ else if (Array.isArray(value)) parts.push(...value);
140
+ if (audio !== void 0) {
141
+ const url = typeof audio === "string" && /^https?:\/\//.test(audio) ? audio : null;
142
+ parts.push(mediaPart("audio", url));
143
+ }
144
+ return parts;
145
+ }
146
+ /**
147
+ * Content parts for a media call's result: generated image, audio or video
148
+ * URLs, and transcript text. Base64 output becomes a placeholder.
149
+ */
150
+ function mediaOutputParts(activity, result) {
151
+ if (!result || typeof result !== "object") return [];
152
+ const r = result;
153
+ const parts = [];
154
+ if (Array.isArray(r.images)) for (const image of r.images) parts.push(mediaPart("image", image?.url));
155
+ if (r.audio !== void 0) parts.push(mediaPart("audio", r.audio?.url));
156
+ if (activity === "video" && typeof r.url === "string") parts.push(mediaPart("video", r.url));
157
+ if (typeof r.text === "string") parts.push({
158
+ type: "text",
159
+ content: r.text
160
+ });
161
+ return parts;
162
+ }
75
163
  function messageEventName(role) {
76
164
  switch (role) {
77
165
  case "user": return "gen_ai.user.message";
@@ -146,6 +234,17 @@ function otelMiddleware(options) {
146
234
  ...errorType ? { "error.type": errorType } : {}
147
235
  });
148
236
  };
237
+ const redactMediaText = (text) => redactContent(maxContentLength > 0 && text.length > maxContentLength ? text.slice(0, maxContentLength) + "…" : text);
238
+ const setMediaMessages = (span, direction, parts) => {
239
+ const content = serializeParts(parts, redactMediaText);
240
+ if (content.length === 0) return;
241
+ const json = JSON.stringify([{
242
+ role: direction === "input" ? "user" : "assistant",
243
+ content
244
+ }]);
245
+ span.setAttribute(`gen_ai.${direction}.messages`, json);
246
+ span.setAttribute(`langfuse.observation.${direction}`, json);
247
+ };
149
248
  const startMediaSpan = (ctx) => {
150
249
  safeCall("otel.onStart", () => {
151
250
  const operationName = OPERATION_NAME[ctx.activity];
@@ -167,6 +266,12 @@ function otelMiddleware(options) {
167
266
  const enriched = safeCall("otel.attributeEnricher", () => attributeEnricher?.(info));
168
267
  if (enriched) span.setAttributes(enriched);
169
268
  mediaSpans.set(ctx, span);
269
+ if (captureContent) {
270
+ setMediaMessages(span, "input", mediaInputParts(ctx.artifactInputs));
271
+ ctx.resultTransforms.push((result) => {
272
+ safeCall("otel.captureOutput", () => setMediaMessages(span, "output", mediaOutputParts(ctx.activity, result)));
273
+ });
274
+ }
170
275
  });
171
276
  };
172
277
  const endMediaSpan = (ctx, finalize) => {
@@ -277,6 +382,15 @@ function otelMiddleware(options) {
277
382
  content: redactContent(sys)
278
383
  });
279
384
  for (const m of config.messages) {
385
+ if (Array.isArray(m.content)) {
386
+ const parts = serializeParts(m.content, redactContent);
387
+ if (parts.length === 0) continue;
388
+ inputMessages.push({
389
+ role: m.role,
390
+ content: parts
391
+ });
392
+ continue;
393
+ }
280
394
  const body = serializeContent(m.content);
281
395
  if (body.length === 0) continue;
282
396
  inputMessages.push({
@@ -1 +1 @@
1
- {"version":3,"file":"otel.js","names":[],"sources":["../../../src/middlewares/otel.ts"],"sourcesContent":["import {\n SpanKind,\n SpanStatusCode,\n context as otelContext,\n trace as otelTrace,\n} from '@opentelemetry/api'\nimport {\n MAX_TOKENS_KEYS,\n NESTED_MAX_TOKENS_KEY,\n} from '../utilities/sampling-keys'\nimport { firstNumber } from '../utilities/numbers'\nimport { errorMessage, errorTypeName } from '../utilities/errors'\nimport { rebuildTokenUsage } from '../utilities/ag-ui-usage'\nimport { tanstackMetadata } from '../utilities/merge-metadata'\nimport { usageAttributes } from './usage-attributes'\nimport type {\n AttributeValue,\n Exception,\n Meter,\n Span,\n SpanOptions,\n Tracer,\n} from '@opentelemetry/api'\nimport type {\n ChatMiddleware,\n ChatMiddlewareContext,\n} from '../activities/chat/middleware/types'\nimport type {\n GenerationActivity,\n GenerationMiddleware,\n GenerationMiddlewareContext,\n} from '../activities/middleware/types'\nimport type { TokenUsage } from '../types'\n\n/**\n * Scope (role) of an OTel span emitted by this middleware.\n *\n * - `chat` — the root span for a single `chat()` call\n * - `iteration` — one per provider model call (agent-loop `beforeModel`\n * turn, or the separate `structuredOutput` finalization when\n * `outputSchema` skips the agent loop — see #1054)\n * - `tool` — one per tool execution inside an iteration\n * - `generation` — the single span for a media activity call\n * (`generateImage`, `generateVideo`, `generateSpeech`, …)\n */\nexport type OtelSpanScope = 'chat' | 'iteration' | 'tool' | 'generation'\n\n/**\n * Alias retained for backwards compatibility. Prefer {@link OtelSpanScope}.\n *\n * @deprecated Use `OtelSpanScope` instead — the name shadows OTel's built-in\n * `SpanKind` which is also imported by integrations of this middleware.\n */\nexport type OtelSpanKind = OtelSpanScope\n\n/**\n * Span metadata passed to `spanNameFormatter`, `attributeEnricher`,\n * `onBeforeSpanStart`, and `onSpanEnd`. Discriminated by `kind` so that\n * tool-only fields narrow automatically inside callback bodies.\n */\nexport type OtelSpanInfo<TScope extends OtelSpanScope = OtelSpanScope> =\n TScope extends 'chat'\n ? { kind: 'chat'; ctx: ChatMiddlewareContext }\n : TScope extends 'iteration'\n ? { kind: 'iteration'; ctx: ChatMiddlewareContext; iteration: number }\n : TScope extends 'tool'\n ? {\n kind: 'tool'\n ctx: ChatMiddlewareContext\n iteration: number\n toolName: string\n toolCallId: string\n }\n : TScope extends 'generation'\n ? { kind: 'generation'; ctx: GenerationMiddlewareContext }\n : never\n\n/**\n * `gen_ai.operation.name` per activity. Chat uses the GenAI semconv value;\n * media operations have no semconv entry yet, so these are the de-facto names\n * consumed by GenAI backends (PostHog, Langfuse, …). Documented in\n * `docs/advanced/otel.md`.\n */\nconst OPERATION_NAME: Record<GenerationActivity, string> = {\n chat: 'chat',\n image: 'image_generation',\n video: 'video_generation',\n audio: 'audio_generation',\n tts: 'text_to_speech',\n voice: 'voice_generation',\n transcription: 'transcription',\n embedding: 'embeddings',\n rerank: 'rerank',\n evaluate: 'evaluate',\n summarize: 'summarize',\n world: 'world_generation',\n liveVideo: 'live_video_generation',\n}\n\nexport interface OtelMiddlewareOptions {\n /** OTel `Tracer` used to start root, iteration, and tool spans. */\n tracer: Tracer\n /**\n * Optional OTel `Meter`. When provided, the middleware records\n * `gen_ai.client.operation.duration` and `gen_ai.client.token.usage`\n * histograms. Omit to disable metrics without disabling tracing.\n */\n meter?: Meter\n /**\n * When `true`, prompt and completion content is attached to iteration spans\n * as `gen_ai.*.message` / `gen_ai.choice` events. Defaults to `false` so\n * that PII never lands on a span by accident.\n */\n captureContent?: boolean\n /**\n * Invoked on every captured content string before it lands on a span.\n * Return a redacted version. If this function throws, the middleware emits\n * the literal sentinel `\"[redaction_failed]\"` instead of the original text\n * — it never falls back to raw content.\n */\n redact?: (text: string) => string\n /**\n * Maximum characters kept in the per-iteration assistant text buffer used\n * to emit `gen_ai.choice` events. Extra characters are truncated with a\n * trailing `\"…\"` marker. Defaults to 100 000. Set to `0` to disable the\n * cap. Exporters typically truncate long attribute values anyway.\n */\n maxContentLength?: number\n /** Override the default span name for each `kind`. */\n spanNameFormatter?: (info: OtelSpanInfo) => string\n /** Add extra attributes to each span. */\n attributeEnricher?: (info: OtelSpanInfo) => Record<string, AttributeValue>\n /** Mutate `SpanOptions` immediately before `tracer.startSpan(...)`. */\n onBeforeSpanStart?: (info: OtelSpanInfo, options: SpanOptions) => SpanOptions\n /** Fires just before every `span.end()`. */\n onSpanEnd?: (info: OtelSpanInfo, span: Span) => void\n}\n\ninterface RequestState {\n rootSpan: Span\n currentIterationSpan: Span | null\n toolSpans: Map<string, { span: Span; toolName: string }>\n iterationCount: number\n assistantTextBuffer: string\n assistantTextBufferTruncated: boolean\n startTime: number\n /**\n * Finish reason from the most recent `RUN_FINISHED` chunk. Captured in\n * `onChunk` so `onFinish` can stamp it on the root span without reading it\n * from the (base-shaped) finish info, which doesn't carry it.\n */\n lastFinishReason: string | null\n rootUsageAttributes: Record<string, number> | null\n rootUsageApplied: boolean\n}\n\nconst stateByCtx = new WeakMap<ChatMiddlewareContext, RequestState>()\n\nconst DEFAULT_MAX_CONTENT_LENGTH = 100_000\nconst REDACTION_FAILED_SENTINEL = '[redaction_failed]'\n\nfunction accumulateUsageAttributes(\n current: Record<string, number> | null,\n usage: TokenUsage,\n): Record<string, number> {\n const accumulated = current ?? {}\n for (const [key, value] of Object.entries(usageAttributes(usage))) {\n if (typeof value === 'number') {\n accumulated[key] = (accumulated[key] ?? 0) + value\n }\n }\n return accumulated\n}\n\nfunction applyRootUsage(state: RequestState, fallbackUsage?: TokenUsage): void {\n if (state.rootUsageApplied) return\n\n const attributes =\n state.rootUsageAttributes ??\n (fallbackUsage ? usageAttributes(fallbackUsage) : null)\n if (attributes) state.rootSpan.setAttributes(attributes)\n state.rootUsageApplied = true\n}\n\nfunction serializeContent(content: unknown): string {\n if (typeof content === 'string') return content\n if (!Array.isArray(content)) return ''\n const parts: Array<string> = []\n for (const part of content) {\n if (!part || typeof part !== 'object') continue\n const type = (part as { type?: string }).type\n switch (type) {\n case 'text':\n parts.push(\n (\n (part as { text?: string }).text ??\n (part as { content?: string }).content ??\n ''\n ).toString(),\n )\n break\n case 'image':\n parts.push('[image]')\n break\n case 'audio':\n parts.push('[audio]')\n break\n case 'video':\n parts.push('[video]')\n break\n case 'document':\n parts.push('[document]')\n break\n case undefined:\n parts.push('[unknown]')\n break\n default:\n parts.push(`[${type}]`)\n }\n }\n return parts.join(' ')\n}\n\nfunction messageEventName(role: string): string {\n switch (role) {\n case 'user':\n return 'gen_ai.user.message'\n case 'assistant':\n return 'gen_ai.assistant.message'\n case 'tool':\n return 'gen_ai.tool.message'\n case 'system':\n return 'gen_ai.system.message'\n default:\n return `gen_ai.${role}.message`\n }\n}\n\nfunction safeCall<T>(label: string, fn: () => T): T | undefined {\n try {\n return fn()\n } catch (err) {\n // Keep middleware non-fatal, but surface callback failures so that broken\n // extension points (attributeEnricher, spanNameFormatter, onSpanEnd, ...)\n // are observable. Matches the guarantee documented in docs/advanced/otel.md.\n console.warn(`[otelMiddleware] ${label} failed`, err)\n return undefined\n }\n}\n\nexport function otelMiddleware(\n options: OtelMiddlewareOptions,\n): GenerationMiddleware & ChatMiddleware {\n const {\n tracer,\n meter,\n captureContent = false,\n redact = (s) => s,\n maxContentLength = DEFAULT_MAX_CONTENT_LENGTH,\n spanNameFormatter,\n attributeEnricher,\n onBeforeSpanStart,\n onSpanEnd,\n } = options\n\n const durationHistogram = meter?.createHistogram(\n 'gen_ai.client.operation.duration',\n {\n description: 'GenAI client operation duration',\n unit: 's',\n },\n )\n const tokenHistogram = meter?.createHistogram('gen_ai.client.token.usage', {\n description: 'GenAI client token usage',\n unit: '{token}',\n })\n\n // Redact user content, failing closed to a sentinel string instead of ever\n // letting raw text through. Callers that pass `captureContent: true` with a\n // third-party PII redactor depend on this invariant.\n const redactContent = (text: string): string => {\n try {\n return redact(text)\n } catch (err) {\n console.warn('[otelMiddleware] otel.redact failed', err)\n return REDACTION_FAILED_SENTINEL\n }\n }\n\n const appendAssistantText = (state: RequestState, delta: string): void => {\n if (maxContentLength > 0) {\n if (state.assistantTextBufferTruncated) return\n const remaining = maxContentLength - state.assistantTextBuffer.length\n if (remaining <= 0) {\n state.assistantTextBufferTruncated = true\n state.assistantTextBuffer += '…'\n return\n }\n if (delta.length > remaining) {\n state.assistantTextBuffer += delta.slice(0, remaining) + '…'\n state.assistantTextBufferTruncated = true\n return\n }\n }\n state.assistantTextBuffer += delta\n }\n\n const closeIterationSpan = (\n state: RequestState,\n ctx: ChatMiddlewareContext,\n ): void => {\n if (!state.currentIterationSpan) return\n const span = state.currentIterationSpan\n const iteration = state.iterationCount - 1\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'iteration', ctx, iteration }, span),\n )\n span.end()\n state.currentIterationSpan = null\n }\n\n // --- Media activities -----------------------------------------------------\n // Media calls (image/video/audio/tts/transcription) are single request →\n // response, so they get exactly one CLIENT span — opened in `onStart` and\n // closed in the terminal hook. Keyed by the per-call context object, which is\n // distinct from the chat state map above so the two paths never collide.\n const mediaSpans = new WeakMap<GenerationMiddlewareContext, Span>()\n\n const recordMediaDuration = (\n ctx: GenerationMiddlewareContext,\n durationMs: number,\n errorType?: string,\n ): void => {\n if (!durationHistogram) return\n durationHistogram.record(durationMs / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': OPERATION_NAME[ctx.activity],\n 'gen_ai.request.model': ctx.model,\n ...(errorType ? { 'error.type': errorType } : {}),\n })\n }\n\n const startMediaSpan = (ctx: GenerationMiddlewareContext): void => {\n safeCall('otel.onStart', () => {\n const operationName = OPERATION_NAME[ctx.activity]\n const info: OtelSpanInfo<'generation'> = { kind: 'generation', ctx }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `${operationName} ${ctx.model}`\n const baseOptions: SpanOptions = {\n kind: SpanKind.CLIENT,\n attributes: {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': operationName,\n 'gen_ai.request.model': ctx.model,\n },\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n const span = tracer.startSpan(name, spanOptions)\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) span.setAttributes(enriched)\n mediaSpans.set(ctx, span)\n })\n }\n\n const endMediaSpan = (\n ctx: GenerationMiddlewareContext,\n finalize: (span: Span) => void,\n ): void => {\n const span = mediaSpans.get(ctx)\n mediaSpans.delete(ctx)\n if (!span) return\n finalize(span)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'generation', ctx }, span),\n )\n span.end()\n }\n\n return {\n name: 'otel',\n\n onStart(ctx) {\n // Media activities get one CLIENT span; chat builds the root/iteration\n // tree below. The cast is sound: the chat runner only ever passes a\n // ChatMiddlewareContext, which `activity: 'chat'` narrows to at runtime.\n if (ctx.activity !== 'chat') {\n startMediaSpan(ctx)\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onStart', () => {\n const info: OtelSpanInfo<'chat'> = { kind: 'chat', ctx: chatCtx }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${chatCtx.model}`\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.request.model': chatCtx.model,\n // NOTE: `gen_ai.operation.name` is deliberately NOT set on the\n // root span. The root represents a `chat()` invocation that may\n // span multiple model calls; only iteration spans correspond to\n // a single chat operation. Backends that map `operation.name=chat`\n // to a \"generation\" event (e.g. PostHog LLM Analytics) would\n // otherwise emit a duplicate generation for the wrapper span.\n },\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n const rootSpan = tracer.startSpan(name, spanOptions)\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) rootSpan.setAttributes(enriched)\n\n stateByCtx.set(chatCtx, {\n rootSpan,\n currentIterationSpan: null,\n toolSpans: new Map(),\n iterationCount: 0,\n assistantTextBuffer: '',\n assistantTextBufferTruncated: false,\n startTime: Date.now(),\n lastFinishReason: null,\n rootUsageAttributes: null,\n rootUsageApplied: false,\n })\n })\n },\n\n onConfig(ctx, config) {\n // Open an iteration span for every provider model call:\n // - `beforeModel`: agent-loop chatStream turns\n // - `structuredOutput`: separate structured-output finalization\n // (no-tools + outputSchema skips the agent loop, so without this\n // phase there is no generation span and captureContent is a silent\n // no-op — see #1054).\n // Native-combined mode never fires `structuredOutput`, so a run that\n // already opened spans via `beforeModel` is not double-counted.\n if (ctx.phase !== 'beforeModel' && ctx.phase !== 'structuredOutput')\n return\n safeCall('otel.onConfig', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n // The previous iteration's span stays open through tool execution and\n // onUsage so that tool spans nest under it and token attributes land\n // on it. Close it here, just before opening the next iteration.\n closeIterationSpan(state, ctx)\n\n // Number spans by the order of model calls this middleware has seen,\n // not by `ctx.iteration`. After an agent-loop turn, structured-output\n // finalization reuses the engine's last iteration index; using our\n // own counter keeps finalization as a distinct leaf (#N+1).\n const iteration = state.iterationCount\n\n const info: OtelSpanInfo<'iteration'> = {\n kind: 'iteration',\n ctx,\n iteration,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${ctx.model} #${iteration}`\n\n const baseAttrs: Record<string, AttributeValue> = {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'tanstack.ai.iteration': iteration,\n }\n // Sampling options now live in provider-native `modelOptions`, and\n // providers spell them differently (e.g. `max_output_tokens`,\n // `max_completion_tokens`, `maxOutputTokens`, `num_predict`). Read the\n // first numeric value among the known spellings — including Ollama's\n // nested `options` — so gen_ai attributes populate across providers.\n const sampling = config.modelOptions ?? {}\n const nestedOptions =\n sampling['options'] && typeof sampling['options'] === 'object'\n ? (sampling['options'] as Record<string, unknown>)\n : undefined\n const samplingTemperature = firstNumber(\n sampling['temperature'],\n nestedOptions?.['temperature'],\n )\n const samplingTopP = firstNumber(\n sampling['top_p'],\n sampling['topP'],\n nestedOptions?.['top_p'],\n )\n // Spellings come from the shared `MAX_TOKENS_KEYS` table so this stays\n // in lockstep with the summarize wrapper's caller-limit detection.\n const samplingMaxTokens = firstNumber(\n ...MAX_TOKENS_KEYS.map((k) => sampling[k]),\n nestedOptions?.[NESTED_MAX_TOKENS_KEY],\n )\n if (samplingTemperature !== undefined)\n baseAttrs['gen_ai.request.temperature'] = samplingTemperature\n if (samplingTopP !== undefined)\n baseAttrs['gen_ai.request.top_p'] = samplingTopP\n if (samplingMaxTokens !== undefined)\n baseAttrs['gen_ai.request.max_tokens'] = samplingMaxTokens\n\n const baseOptions: SpanOptions = {\n kind: SpanKind.CLIENT,\n attributes: baseAttrs,\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n\n const parentCtx = otelTrace.setSpan(\n otelContext.active(),\n state.rootSpan,\n )\n let iterSpan!: Span\n otelContext.with(parentCtx, () => {\n // Pass the parent context explicitly as the 3rd arg — this is a\n // real-OTel-compatible way to ensure the span is parented to\n // `rootSpan` even when the host app has not registered a context\n // manager (e.g. in tests or minimal setups).\n iterSpan = tracer.startSpan(name, spanOptions, parentCtx)\n })\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) iterSpan.setAttributes(enriched)\n\n state.currentIterationSpan = iterSpan\n state.assistantTextBuffer = ''\n state.assistantTextBufferTruncated = false\n\n if (captureContent) {\n const systemPromptContents = config.systemPrompts.map((p) =>\n typeof p === 'string' ? p : p.content,\n )\n // Anthropic prompt-caching users need to know which prompt carried\n // `cache_control`: it's the one attribute that explains cache\n // hit/miss in observability. Serialise per-prompt metadata as a\n // single JSON span attribute so backends that don't understand\n // GenAI events can still surface it. Kept off span events to\n // avoid breaking the one-event-per-message GenAI semconv contract.\n const systemPromptMetadata = config.systemPrompts.map((p) =>\n typeof p === 'string' || p.metadata === undefined\n ? null\n : p.metadata,\n )\n if (systemPromptMetadata.some((m) => m !== null)) {\n iterSpan.setAttribute(\n 'tanstack.ai.system_prompt.metadata',\n JSON.stringify(systemPromptMetadata),\n )\n }\n // Span events follow the original GenAI semconv (one event per\n // message). Backends that read events get content this way.\n for (const sys of systemPromptContents) {\n iterSpan.addEvent('gen_ai.system.message', {\n content: redactContent(sys),\n })\n }\n for (const m of config.messages) {\n const body = serializeContent(m.content)\n if (body.length === 0) continue\n iterSpan.addEvent(messageEventName(m.role), {\n content: redactContent(body),\n })\n }\n\n // Also emit the current GenAI-semconv attribute form\n // (`gen_ai.input.messages`) — backends like PostHog read prompt\n // content from this attribute, not from span events.\n const inputMessages: Array<{ role: string; content: string }> = []\n for (const sys of systemPromptContents) {\n inputMessages.push({\n role: 'system',\n content: redactContent(sys),\n })\n }\n for (const m of config.messages) {\n const body = serializeContent(m.content)\n if (body.length === 0) continue\n inputMessages.push({\n role: m.role,\n content: redactContent(body),\n })\n }\n if (inputMessages.length > 0) {\n const inputJson = JSON.stringify(inputMessages)\n // Current OTel GenAI semconv — Sentry / PostHog / Datadog read\n // prompt content from this attribute.\n iterSpan.setAttribute('gen_ai.input.messages', inputJson)\n // Langfuse-native attribute. Highest priority in Langfuse's OTLP\n // ingestion (checked before events and gen_ai.input.messages) so\n // the Input panel populates reliably. Harmless to other backends —\n // the attribute is namespaced and unrecognised keys are ignored.\n iterSpan.setAttribute('langfuse.observation.input', inputJson)\n\n // Mirror the first iteration's input onto the root span and at\n // trace level so Langfuse fills Input on the trace card and the\n // chat-level observation. Later iterations append tool-call /\n // assistant messages that are useful per-iteration but noise at\n // the chat / trace level.\n if (state.iterationCount === 0) {\n state.rootSpan.setAttribute(\n 'langfuse.observation.input',\n inputJson,\n )\n state.rootSpan.setAttribute('langfuse.trace.input', inputJson)\n }\n }\n }\n\n state.iterationCount += 1\n })\n return undefined\n },\n\n onChunk(ctx, chunk) {\n safeCall('otel.onChunk', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n if (captureContent && chunk.type === 'TEXT_MESSAGE_CONTENT') {\n appendAssistantText(state, chunk.delta)\n }\n\n if (chunk.type !== 'RUN_FINISHED') return\n const tanstack = tanstackMetadata(chunk)\n const extra = chunk as {\n finishReason?: string | null\n model?: string\n }\n const finishReason = extra.finishReason ?? tanstack?.finishReason\n const model = extra.model ?? tanstack?.model\n // Capture for the root-span finish_reasons attribute set in onFinish,\n // which receives base-shaped info without a finishReason field.\n if (finishReason) state.lastFinishReason = finishReason\n const span = state.currentIterationSpan\n if (!span) return\n\n if (finishReason) {\n span.setAttribute('gen_ai.response.finish_reasons', [finishReason])\n }\n if (model) span.setAttribute('gen_ai.response.model', model)\n\n // Set usage attributes on the iteration span directly from the chunk\n // so they're available before `onUsage` fires. Histogram recording is\n // deliberately NOT done here — the chat runner always invokes\n // `runOnUsage` when `chunk.usage` is present, and `onUsage` is the\n // canonical place for the metric. Recording in both would double-count.\n const tokenUsage = rebuildTokenUsage(chunk.usage, tanstack?.usage)\n if (tokenUsage) {\n span.setAttributes(usageAttributes(tokenUsage))\n }\n\n if (captureContent && state.assistantTextBuffer.length > 0) {\n const completion = redactContent(state.assistantTextBuffer)\n const outputJson = JSON.stringify([\n { role: 'assistant', content: completion },\n ])\n // Event form (older semconv) — kept for backends that consume it.\n span.addEvent('gen_ai.choice', { content: completion })\n // Attribute form (current semconv) — required by backends like\n // PostHog that read completion content from `gen_ai.output.messages`.\n span.setAttribute('gen_ai.output.messages', outputJson)\n // Langfuse-native attribute (highest priority in Langfuse mapping).\n span.setAttribute('langfuse.observation.output', outputJson)\n // Mirror to the root span and trace card. Each iteration overwrites,\n // so the final iteration's completion lands on the root — which is\n // the final answer the user saw, not an intermediate tool-call turn.\n state.rootSpan.setAttribute('langfuse.observation.output', outputJson)\n state.rootSpan.setAttribute('langfuse.trace.output', outputJson)\n state.assistantTextBuffer = ''\n state.assistantTextBufferTruncated = false\n }\n\n // Intentionally leave the iteration span open: tool spans started\n // after `RUN_FINISHED` (tool_calls finishReason) must nest under it,\n // and `onUsage` may still fire. The span is closed in `onConfig` when\n // the next iteration starts, or in `onFinish` / `onError` / `onAbort`.\n })\n return undefined\n },\n\n onUsage(ctx, usage) {\n if (ctx.activity !== 'chat') {\n // Media: stamp usage on the single span. No token histogram — media\n // unit billing lands as span attributes via usageAttributes, matching\n // prior media behavior and avoiding chat-shaped token metrics.\n safeCall('otel.onUsage', () => {\n const span = mediaSpans.get(ctx)\n if (span) span.setAttributes(usageAttributes(usage))\n })\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onUsage', () => {\n const state = stateByCtx.get(chatCtx)\n if (!state) return\n\n state.rootUsageAttributes = accumulateUsageAttributes(\n state.rootUsageAttributes,\n usage,\n )\n\n // Always record the token histogram — metrics don't depend on having\n // an iteration span, and skipping here would drop metric data if an\n // adapter emits `onUsage` outside the iteration window.\n if (tokenHistogram) {\n const metricAttrs = {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': chatCtx.model,\n }\n tokenHistogram.record(usage.promptTokens, {\n ...metricAttrs,\n 'gen_ai.token.type': 'input',\n })\n tokenHistogram.record(usage.completionTokens, {\n ...metricAttrs,\n 'gen_ai.token.type': 'output',\n })\n }\n\n const span = state.currentIterationSpan ?? state.rootSpan\n span.setAttributes(usageAttributes(usage))\n })\n },\n\n onBeforeToolCall(ctx, hookCtx) {\n safeCall('otel.onBeforeToolCall', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n const parent = state.currentIterationSpan ?? state.rootSpan\n\n const info: OtelSpanInfo<'tool'> = {\n kind: 'tool',\n ctx,\n toolName: hookCtx.toolName,\n toolCallId: hookCtx.toolCallId,\n iteration: state.iterationCount - 1,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `execute_tool ${hookCtx.toolName}`\n\n const baseAttrs: Record<string, AttributeValue> = {\n 'gen_ai.tool.name': hookCtx.toolName,\n 'gen_ai.tool.call.id': hookCtx.toolCallId,\n 'gen_ai.tool.type': 'function',\n }\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: baseAttrs,\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n\n const parentCtx = otelTrace.setSpan(otelContext.active(), parent)\n let toolSpan!: Span\n otelContext.with(parentCtx, () => {\n toolSpan = tracer.startSpan(name, spanOptions, parentCtx)\n })\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) toolSpan.setAttributes(enriched)\n\n // Stamp the tool args onto the tool span so backends that render an\n // input panel per span (e.g. PostHog) have something to show.\n if (captureContent) {\n const argsBody =\n typeof hookCtx.args === 'string'\n ? hookCtx.args\n : (safeCall('otel.serializeToolArgs', () =>\n JSON.stringify(hookCtx.args ?? null),\n ) ?? '[unserializable_tool_args]')\n const redactedArgs = redactContent(argsBody)\n const toolInputJson = JSON.stringify([\n { role: 'tool', content: redactedArgs },\n ])\n toolSpan.setAttribute('gen_ai.input.messages', toolInputJson)\n // Langfuse-native (highest priority in Langfuse mapping).\n toolSpan.setAttribute('langfuse.observation.input', toolInputJson)\n }\n\n state.toolSpans.set(hookCtx.toolCallId, {\n span: toolSpan,\n toolName: hookCtx.toolName,\n })\n })\n return undefined\n },\n\n onAfterToolCall(ctx, info) {\n safeCall('otel.onAfterToolCall', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n const entry = state.toolSpans.get(info.toolCallId)\n if (!entry) return\n const { span: toolSpan } = entry\n\n const outcome = info.ok ? 'success' : 'error'\n toolSpan.setAttribute('tanstack.ai.tool.outcome', outcome)\n\n if (!info.ok && info.error !== undefined) {\n toolSpan.recordException(info.error as Exception)\n const msg = errorMessage(info.error)\n toolSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...(msg !== undefined && { message: msg }),\n })\n }\n\n if (captureContent) {\n // Serialization can throw on circular refs or `BigInt` values. If it\n // does, fall back to a sentinel so the rest of this handler (span\n // end, onSpanEnd, toolSpans cleanup) still runs — otherwise the tool\n // span would dangle until the onFinish/onError sweep.\n const body =\n typeof info.result === 'string'\n ? info.result\n : (safeCall('otel.serializeToolResult', () =>\n JSON.stringify(info.result ?? null),\n ) ?? '[unserializable_tool_result]')\n const redactedBody = redactContent(body)\n if (state.currentIterationSpan) {\n state.currentIterationSpan.addEvent('gen_ai.tool.message', {\n content: redactedBody,\n tool_call_id: info.toolCallId,\n })\n }\n // Output panel of the tool span itself — `gen_ai.output.messages` is\n // what current GenAI semconv consumers (e.g. PostHog) read.\n const toolOutputJson = JSON.stringify([\n { role: 'tool', content: redactedBody },\n ])\n toolSpan.setAttribute('gen_ai.output.messages', toolOutputJson)\n // Langfuse-native (highest priority in Langfuse mapping).\n toolSpan.setAttribute('langfuse.observation.output', toolOutputJson)\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolName: info.toolName,\n toolCallId: info.toolCallId,\n iteration: state.iterationCount - 1,\n },\n toolSpan,\n ),\n )\n toolSpan.end()\n state.toolSpans.delete(info.toolCallId)\n })\n },\n\n onError(ctx, info) {\n if (ctx.activity !== 'chat') {\n safeCall('otel.onError', () => {\n const message = errorMessage(info.error)\n endMediaSpan(ctx, (span) => {\n span.recordException(info.error as Exception)\n span.setStatus({\n code: SpanStatusCode.ERROR,\n ...(message !== undefined ? { message } : {}),\n })\n })\n recordMediaDuration(ctx, info.duration, errorTypeName(info.error))\n })\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onError', () => {\n const state = stateByCtx.get(chatCtx)\n if (!state) return\n\n const errType = errorTypeName(info.error)\n const message = errorMessage(info.error)\n const statusMessage =\n message !== undefined ? { message } : ({} as const)\n const exception = info.error as Exception\n\n const iterationSpan = state.currentIterationSpan\n if (iterationSpan) {\n iterationSpan.recordException(exception)\n iterationSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...statusMessage,\n })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx: chatCtx,\n iteration: state.iterationCount - 1,\n },\n iterationSpan,\n ),\n )\n iterationSpan.end()\n state.currentIterationSpan = null\n }\n\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n span.recordException(exception)\n span.setStatus({ code: SpanStatusCode.ERROR, ...statusMessage })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx: chatCtx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n\n state.rootSpan.recordException(exception)\n state.rootSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...statusMessage,\n })\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': chatCtx.model,\n 'error.type': errType,\n })\n }\n\n applyRootUsage(state)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx: chatCtx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(chatCtx)\n })\n },\n\n onAbort(ctx, info) {\n if (ctx.activity !== 'chat') {\n // Media abandonment (e.g. a video stream dropped before completion).\n safeCall('otel.onAbort', () => {\n endMediaSpan(ctx, (span) => {\n span.setAttribute('tanstack.ai.completion.reason', 'cancelled')\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: info.reason ?? 'cancelled',\n })\n })\n recordMediaDuration(ctx, info.duration, 'cancelled')\n })\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onAbort', () => {\n const state = stateByCtx.get(chatCtx)\n if (!state) return\n\n const closeCancelled = (span: Span): void => {\n // `gen_ai.completion.reason` is not part of the GenAI semconv; use a\n // TanStack-namespaced attribute so downstream exporters don't treat\n // it as standard. The span status still carries the error code.\n span.setAttribute('tanstack.ai.completion.reason', 'cancelled')\n span.setStatus({ code: SpanStatusCode.ERROR, message: 'cancelled' })\n }\n\n const iterationSpan = state.currentIterationSpan\n if (iterationSpan) {\n closeCancelled(iterationSpan)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx: chatCtx,\n iteration: state.iterationCount - 1,\n },\n iterationSpan,\n ),\n )\n iterationSpan.end()\n state.currentIterationSpan = null\n }\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n closeCancelled(span)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx: chatCtx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n closeCancelled(state.rootSpan)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': chatCtx.model,\n 'error.type': 'cancelled',\n })\n }\n\n applyRootUsage(state)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx: chatCtx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(chatCtx)\n })\n },\n\n onFinish(ctx, info) {\n if (ctx.activity !== 'chat') {\n safeCall('otel.onFinish', () => {\n endMediaSpan(ctx, (span) => {\n if (info.usage) span.setAttributes(usageAttributes(info.usage))\n })\n recordMediaDuration(ctx, info.duration)\n })\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onFinish', () => {\n const state = stateByCtx.get(chatCtx)\n if (!state) return\n\n // Close any tool spans that never received `onAfterToolCall` (adapter\n // quirk). Done before the iteration span so the hierarchy is closed\n // in depth-first order.\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n span.setAttribute('tanstack.ai.tool.outcome', 'unknown')\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx: chatCtx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n\n // The final iteration's span is still open because we keep it open\n // through tool execution and `onUsage`. Close it now.\n closeIterationSpan(state, chatCtx)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': chatCtx.model,\n })\n }\n\n if (state.lastFinishReason) {\n state.rootSpan.setAttribute('gen_ai.response.finish_reasons', [\n state.lastFinishReason,\n ])\n }\n state.rootSpan.setAttribute(\n 'tanstack.ai.iterations',\n state.iterationCount,\n )\n\n applyRootUsage(state, info.usage)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx: chatCtx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(chatCtx)\n })\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"otel.js","names":[],"sources":["../../../src/middlewares/otel.ts"],"sourcesContent":["import {\n SpanKind,\n SpanStatusCode,\n context as otelContext,\n trace as otelTrace,\n} from '@opentelemetry/api'\nimport {\n MAX_TOKENS_KEYS,\n NESTED_MAX_TOKENS_KEY,\n} from '../utilities/sampling-keys'\nimport { firstNumber } from '../utilities/numbers'\nimport { errorMessage, errorTypeName } from '../utilities/errors'\nimport { rebuildTokenUsage } from '../utilities/ag-ui-usage'\nimport { tanstackMetadata } from '../utilities/merge-metadata'\nimport { usageAttributes } from './usage-attributes'\nimport type {\n AttributeValue,\n Exception,\n Meter,\n Span,\n SpanOptions,\n Tracer,\n} from '@opentelemetry/api'\nimport type {\n ChatMiddleware,\n ChatMiddlewareContext,\n} from '../activities/chat/middleware/types'\nimport type {\n GenerationActivity,\n GenerationMiddleware,\n GenerationMiddlewareContext,\n} from '../activities/middleware/types'\nimport type { TokenUsage } from '../types'\n\n/**\n * Scope (role) of an OTel span emitted by this middleware.\n *\n * - `chat` — the root span for a single `chat()` call\n * - `iteration` — one per provider model call (agent-loop `beforeModel`\n * turn, or the separate `structuredOutput` finalization when\n * `outputSchema` skips the agent loop — see #1054)\n * - `tool` — one per tool execution inside an iteration\n * - `generation` — the single span for a media activity call\n * (`generateImage`, `generateVideo`, `generateSpeech`, …)\n */\nexport type OtelSpanScope = 'chat' | 'iteration' | 'tool' | 'generation'\n\n/**\n * Alias retained for backwards compatibility. Prefer {@link OtelSpanScope}.\n *\n * @deprecated Use `OtelSpanScope` instead — the name shadows OTel's built-in\n * `SpanKind` which is also imported by integrations of this middleware.\n */\nexport type OtelSpanKind = OtelSpanScope\n\n/**\n * Span metadata passed to `spanNameFormatter`, `attributeEnricher`,\n * `onBeforeSpanStart`, and `onSpanEnd`. Discriminated by `kind` so that\n * tool-only fields narrow automatically inside callback bodies.\n */\nexport type OtelSpanInfo<TScope extends OtelSpanScope = OtelSpanScope> =\n TScope extends 'chat'\n ? { kind: 'chat'; ctx: ChatMiddlewareContext }\n : TScope extends 'iteration'\n ? { kind: 'iteration'; ctx: ChatMiddlewareContext; iteration: number }\n : TScope extends 'tool'\n ? {\n kind: 'tool'\n ctx: ChatMiddlewareContext\n iteration: number\n toolName: string\n toolCallId: string\n }\n : TScope extends 'generation'\n ? { kind: 'generation'; ctx: GenerationMiddlewareContext }\n : never\n\n/**\n * `gen_ai.operation.name` per activity. Chat uses the GenAI semconv value;\n * media operations have no semconv entry yet, so these are the de-facto names\n * consumed by GenAI backends (PostHog, Langfuse, …). Documented in\n * `docs/advanced/otel.md`.\n */\nconst OPERATION_NAME: Record<GenerationActivity, string> = {\n chat: 'chat',\n image: 'image_generation',\n video: 'video_generation',\n audio: 'audio_generation',\n tts: 'text_to_speech',\n voice: 'voice_generation',\n transcription: 'transcription',\n embedding: 'embeddings',\n rerank: 'rerank',\n evaluate: 'evaluate',\n summarize: 'summarize',\n world: 'world_generation',\n liveVideo: 'live_video_generation',\n}\n\nexport interface OtelMiddlewareOptions {\n /** OTel `Tracer` used to start root, iteration, and tool spans. */\n tracer: Tracer\n /**\n * Optional OTel `Meter`. When provided, the middleware records\n * `gen_ai.client.operation.duration` and `gen_ai.client.token.usage`\n * histograms. Omit to disable metrics without disabling tracing.\n */\n meter?: Meter\n /**\n * When `true`, prompt and completion content is attached to iteration spans\n * as `gen_ai.*.message` / `gen_ai.choice` events. Media spans get the\n * prompt, input media and output (URLs or transcript text) as\n * `gen_ai.input.messages` / `gen_ai.output.messages`. Defaults to `false` so\n * that PII never lands on a span by accident.\n */\n captureContent?: boolean\n /**\n * Invoked on every captured content string before it lands on a span.\n * Return a redacted version. If this function throws, the middleware emits\n * the literal sentinel `\"[redaction_failed]\"` instead of the original text\n * — it never falls back to raw content.\n */\n redact?: (text: string) => string\n /**\n * Maximum characters kept in the per-iteration assistant text buffer used\n * to emit `gen_ai.choice` events, and in each text part of a media span's\n * input and output. Extra characters are truncated with a\n * trailing `\"…\"` marker. Defaults to 100 000. Set to `0` to disable the\n * cap. Exporters typically truncate long attribute values anyway.\n */\n maxContentLength?: number\n /** Override the default span name for each `kind`. */\n spanNameFormatter?: (info: OtelSpanInfo) => string\n /** Add extra attributes to each span. */\n attributeEnricher?: (info: OtelSpanInfo) => Record<string, AttributeValue>\n /** Mutate `SpanOptions` immediately before `tracer.startSpan(...)`. */\n onBeforeSpanStart?: (info: OtelSpanInfo, options: SpanOptions) => SpanOptions\n /** Fires just before every `span.end()`. */\n onSpanEnd?: (info: OtelSpanInfo, span: Span) => void\n}\n\ninterface RequestState {\n rootSpan: Span\n currentIterationSpan: Span | null\n toolSpans: Map<string, { span: Span; toolName: string }>\n iterationCount: number\n assistantTextBuffer: string\n assistantTextBufferTruncated: boolean\n startTime: number\n /**\n * Finish reason from the most recent `RUN_FINISHED` chunk. Captured in\n * `onChunk` so `onFinish` can stamp it on the root span without reading it\n * from the (base-shaped) finish info, which doesn't carry it.\n */\n lastFinishReason: string | null\n rootUsageAttributes: Record<string, number> | null\n rootUsageApplied: boolean\n}\n\nconst stateByCtx = new WeakMap<ChatMiddlewareContext, RequestState>()\n\nconst DEFAULT_MAX_CONTENT_LENGTH = 100_000\nconst REDACTION_FAILED_SENTINEL = '[redaction_failed]'\n\nfunction accumulateUsageAttributes(\n current: Record<string, number> | null,\n usage: TokenUsage,\n): Record<string, number> {\n const accumulated = current ?? {}\n for (const [key, value] of Object.entries(usageAttributes(usage))) {\n if (typeof value === 'number') {\n accumulated[key] = (accumulated[key] ?? 0) + value\n }\n }\n return accumulated\n}\n\nfunction applyRootUsage(state: RequestState, fallbackUsage?: TokenUsage): void {\n if (state.rootUsageApplied) return\n\n const attributes =\n state.rootUsageAttributes ??\n (fallbackUsage ? usageAttributes(fallbackUsage) : null)\n if (attributes) state.rootSpan.setAttributes(attributes)\n state.rootUsageApplied = true\n}\n\nfunction serializeContent(content: unknown): string {\n if (typeof content === 'string') return content\n if (!Array.isArray(content)) return ''\n const parts: Array<string> = []\n for (const part of content) {\n if (!part || typeof part !== 'object') continue\n const type = (part as { type?: string }).type\n switch (type) {\n case 'text':\n parts.push(\n (\n (part as { text?: string }).text ??\n (part as { content?: string }).content ??\n ''\n ).toString(),\n )\n break\n case 'image':\n parts.push('[image]')\n break\n case 'audio':\n parts.push('[audio]')\n break\n case 'video':\n parts.push('[video]')\n break\n case 'document':\n parts.push('[document]')\n break\n case undefined:\n parts.push('[unknown]')\n break\n default:\n parts.push(`[${type}]`)\n }\n }\n return parts.join(' ')\n}\n\ntype InputPart =\n | { type: 'text'; content: string }\n | { type: 'uri'; modality: string; uri: string; mime_type?: string }\n | { type: 'file'; modality: string; file_id: string; mime_type?: string }\n\n/**\n * Structured form of `ContentPart[]` for `gen_ai.input.messages`, using the\n * OTel GenAI semconv part shapes. URL and file-handle media keep their\n * reference; inline bytes (and `data:` URLs) stay a `[type]` placeholder so\n * they never blow attribute size limits. `redact` runs on text parts only.\n */\nfunction serializeParts(\n content: Array<unknown>,\n redact: (text: string) => string,\n): Array<InputPart> {\n const parts: Array<InputPart> = []\n for (const part of content) {\n if (!part || typeof part !== 'object') continue\n const p = part as {\n type?: string\n text?: string\n content?: string\n source?: { type?: string; value?: string; mimeType?: string }\n }\n if (p.type === 'text') {\n parts.push({\n type: 'text',\n content: redact((p.text ?? p.content ?? '').toString()),\n })\n continue\n }\n const modality = p.type ?? 'unknown'\n const { type, value, mimeType } = p.source ?? {}\n const mime = mimeType ? { mime_type: mimeType } : {}\n if (type === 'url' && value && !value.startsWith('data:')) {\n parts.push({ type: 'uri', modality, uri: value, ...mime })\n } else if (type === 'file' && value) {\n parts.push({ type: 'file', modality, file_id: value, ...mime })\n } else {\n parts.push({ type: 'text', content: `[${modality}]` })\n }\n }\n return parts\n}\n\n/** A media reference as a content part, so `serializeParts` can map it. */\nfunction mediaPart(modality: string, url: unknown): unknown {\n return {\n type: modality,\n source: typeof url === 'string' ? { type: 'url', value: url } : undefined,\n }\n}\n\n/**\n * Content parts for a media call's inputs, read from `artifactInputs`: the\n * prompt (a string or `MediaPrompt` parts), TTS `text`, and transcription\n * `audio`. Audio is a reference only when it is an http(s) URL; a base64\n * string, `File` or `Blob` becomes a placeholder.\n */\nfunction mediaInputParts(inputs: unknown): Array<unknown> {\n if (!inputs || typeof inputs !== 'object') return []\n const { prompt, text, audio } = inputs as Record<string, unknown>\n const parts: Array<unknown> = []\n for (const value of [prompt, text]) {\n if (typeof value === 'string') parts.push({ type: 'text', content: value })\n else if (Array.isArray(value)) parts.push(...value)\n }\n if (audio !== undefined) {\n const url =\n typeof audio === 'string' && /^https?:\\/\\//.test(audio) ? audio : null\n parts.push(mediaPart('audio', url))\n }\n return parts\n}\n\n/**\n * Content parts for a media call's result: generated image, audio or video\n * URLs, and transcript text. Base64 output becomes a placeholder.\n */\nfunction mediaOutputParts(\n activity: GenerationActivity,\n result: unknown,\n): Array<unknown> {\n if (!result || typeof result !== 'object') return []\n const r = result as Record<string, unknown>\n const parts: Array<unknown> = []\n if (Array.isArray(r.images)) {\n for (const image of r.images) {\n parts.push(mediaPart('image', (image as { url?: unknown } | null)?.url))\n }\n }\n // `TTSResult.audio` is a base64 string; `AudioGenerationResult.audio` is a\n // `{ url } | { b64Json }` source.\n if (r.audio !== undefined) {\n parts.push(mediaPart('audio', (r.audio as { url?: unknown } | null)?.url))\n }\n // Only video puts its asset on a top-level `url`. A world `url` is a viewer\n // page, not media.\n if (activity === 'video' && typeof r.url === 'string') {\n parts.push(mediaPart('video', r.url))\n }\n if (typeof r.text === 'string') parts.push({ type: 'text', content: r.text })\n return parts\n}\n\nfunction messageEventName(role: string): string {\n switch (role) {\n case 'user':\n return 'gen_ai.user.message'\n case 'assistant':\n return 'gen_ai.assistant.message'\n case 'tool':\n return 'gen_ai.tool.message'\n case 'system':\n return 'gen_ai.system.message'\n default:\n return `gen_ai.${role}.message`\n }\n}\n\nfunction safeCall<T>(label: string, fn: () => T): T | undefined {\n try {\n return fn()\n } catch (err) {\n // Keep middleware non-fatal, but surface callback failures so that broken\n // extension points (attributeEnricher, spanNameFormatter, onSpanEnd, ...)\n // are observable. Matches the guarantee documented in docs/advanced/otel.md.\n console.warn(`[otelMiddleware] ${label} failed`, err)\n return undefined\n }\n}\n\nexport function otelMiddleware(\n options: OtelMiddlewareOptions,\n): GenerationMiddleware & ChatMiddleware {\n const {\n tracer,\n meter,\n captureContent = false,\n redact = (s) => s,\n maxContentLength = DEFAULT_MAX_CONTENT_LENGTH,\n spanNameFormatter,\n attributeEnricher,\n onBeforeSpanStart,\n onSpanEnd,\n } = options\n\n const durationHistogram = meter?.createHistogram(\n 'gen_ai.client.operation.duration',\n {\n description: 'GenAI client operation duration',\n unit: 's',\n },\n )\n const tokenHistogram = meter?.createHistogram('gen_ai.client.token.usage', {\n description: 'GenAI client token usage',\n unit: '{token}',\n })\n\n // Redact user content, failing closed to a sentinel string instead of ever\n // letting raw text through. Callers that pass `captureContent: true` with a\n // third-party PII redactor depend on this invariant.\n const redactContent = (text: string): string => {\n try {\n return redact(text)\n } catch (err) {\n console.warn('[otelMiddleware] otel.redact failed', err)\n return REDACTION_FAILED_SENTINEL\n }\n }\n\n const appendAssistantText = (state: RequestState, delta: string): void => {\n if (maxContentLength > 0) {\n if (state.assistantTextBufferTruncated) return\n const remaining = maxContentLength - state.assistantTextBuffer.length\n if (remaining <= 0) {\n state.assistantTextBufferTruncated = true\n state.assistantTextBuffer += '…'\n return\n }\n if (delta.length > remaining) {\n state.assistantTextBuffer += delta.slice(0, remaining) + '…'\n state.assistantTextBufferTruncated = true\n return\n }\n }\n state.assistantTextBuffer += delta\n }\n\n const closeIterationSpan = (\n state: RequestState,\n ctx: ChatMiddlewareContext,\n ): void => {\n if (!state.currentIterationSpan) return\n const span = state.currentIterationSpan\n const iteration = state.iterationCount - 1\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'iteration', ctx, iteration }, span),\n )\n span.end()\n state.currentIterationSpan = null\n }\n\n // --- Media activities -----------------------------------------------------\n // Media calls (image/video/audio/tts/transcription) are single request →\n // response, so they get exactly one CLIENT span — opened in `onStart` and\n // closed in the terminal hook. Keyed by the per-call context object, which is\n // distinct from the chat state map above so the two paths never collide.\n const mediaSpans = new WeakMap<GenerationMiddlewareContext, Span>()\n\n const recordMediaDuration = (\n ctx: GenerationMiddlewareContext,\n durationMs: number,\n errorType?: string,\n ): void => {\n if (!durationHistogram) return\n durationHistogram.record(durationMs / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': OPERATION_NAME[ctx.activity],\n 'gen_ai.request.model': ctx.model,\n ...(errorType ? { 'error.type': errorType } : {}),\n })\n }\n\n // Media prompts and transcripts are single strings, so cap each text part\n // with `maxContentLength` the same way the chat completion buffer is capped.\n const redactMediaText = (text: string): string =>\n redactContent(\n maxContentLength > 0 && text.length > maxContentLength\n ? text.slice(0, maxContentLength) + '…'\n : text,\n )\n\n const setMediaMessages = (\n span: Span,\n direction: 'input' | 'output',\n parts: Array<unknown>,\n ): void => {\n const content = serializeParts(parts, redactMediaText)\n if (content.length === 0) return\n const json = JSON.stringify([\n { role: direction === 'input' ? 'user' : 'assistant', content },\n ])\n span.setAttribute(`gen_ai.${direction}.messages`, json)\n span.setAttribute(`langfuse.observation.${direction}`, json)\n }\n\n const startMediaSpan = (ctx: GenerationMiddlewareContext): void => {\n safeCall('otel.onStart', () => {\n const operationName = OPERATION_NAME[ctx.activity]\n const info: OtelSpanInfo<'generation'> = { kind: 'generation', ctx }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `${operationName} ${ctx.model}`\n const baseOptions: SpanOptions = {\n kind: SpanKind.CLIENT,\n attributes: {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': operationName,\n 'gen_ai.request.model': ctx.model,\n },\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n const span = tracer.startSpan(name, spanOptions)\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) span.setAttributes(enriched)\n mediaSpans.set(ctx, span)\n\n if (captureContent) {\n setMediaMessages(span, 'input', mediaInputParts(ctx.artifactInputs))\n // Observe the result through a transform: the terminal hooks never\n // see it. Returns `undefined`, so the result is left unchanged.\n ctx.resultTransforms.push((result) => {\n safeCall('otel.captureOutput', () =>\n setMediaMessages(\n span,\n 'output',\n mediaOutputParts(ctx.activity, result),\n ),\n )\n return undefined\n })\n }\n })\n }\n\n const endMediaSpan = (\n ctx: GenerationMiddlewareContext,\n finalize: (span: Span) => void,\n ): void => {\n const span = mediaSpans.get(ctx)\n mediaSpans.delete(ctx)\n if (!span) return\n finalize(span)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'generation', ctx }, span),\n )\n span.end()\n }\n\n return {\n name: 'otel',\n\n onStart(ctx) {\n // Media activities get one CLIENT span; chat builds the root/iteration\n // tree below. The cast is sound: the chat runner only ever passes a\n // ChatMiddlewareContext, which `activity: 'chat'` narrows to at runtime.\n if (ctx.activity !== 'chat') {\n startMediaSpan(ctx)\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onStart', () => {\n const info: OtelSpanInfo<'chat'> = { kind: 'chat', ctx: chatCtx }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${chatCtx.model}`\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.request.model': chatCtx.model,\n // NOTE: `gen_ai.operation.name` is deliberately NOT set on the\n // root span. The root represents a `chat()` invocation that may\n // span multiple model calls; only iteration spans correspond to\n // a single chat operation. Backends that map `operation.name=chat`\n // to a \"generation\" event (e.g. PostHog LLM Analytics) would\n // otherwise emit a duplicate generation for the wrapper span.\n },\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n const rootSpan = tracer.startSpan(name, spanOptions)\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) rootSpan.setAttributes(enriched)\n\n stateByCtx.set(chatCtx, {\n rootSpan,\n currentIterationSpan: null,\n toolSpans: new Map(),\n iterationCount: 0,\n assistantTextBuffer: '',\n assistantTextBufferTruncated: false,\n startTime: Date.now(),\n lastFinishReason: null,\n rootUsageAttributes: null,\n rootUsageApplied: false,\n })\n })\n },\n\n onConfig(ctx, config) {\n // Open an iteration span for every provider model call:\n // - `beforeModel`: agent-loop chatStream turns\n // - `structuredOutput`: separate structured-output finalization\n // (no-tools + outputSchema skips the agent loop, so without this\n // phase there is no generation span and captureContent is a silent\n // no-op — see #1054).\n // Native-combined mode never fires `structuredOutput`, so a run that\n // already opened spans via `beforeModel` is not double-counted.\n if (ctx.phase !== 'beforeModel' && ctx.phase !== 'structuredOutput')\n return\n safeCall('otel.onConfig', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n // The previous iteration's span stays open through tool execution and\n // onUsage so that tool spans nest under it and token attributes land\n // on it. Close it here, just before opening the next iteration.\n closeIterationSpan(state, ctx)\n\n // Number spans by the order of model calls this middleware has seen,\n // not by `ctx.iteration`. After an agent-loop turn, structured-output\n // finalization reuses the engine's last iteration index; using our\n // own counter keeps finalization as a distinct leaf (#N+1).\n const iteration = state.iterationCount\n\n const info: OtelSpanInfo<'iteration'> = {\n kind: 'iteration',\n ctx,\n iteration,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${ctx.model} #${iteration}`\n\n const baseAttrs: Record<string, AttributeValue> = {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'tanstack.ai.iteration': iteration,\n }\n // Sampling options now live in provider-native `modelOptions`, and\n // providers spell them differently (e.g. `max_output_tokens`,\n // `max_completion_tokens`, `maxOutputTokens`, `num_predict`). Read the\n // first numeric value among the known spellings — including Ollama's\n // nested `options` — so gen_ai attributes populate across providers.\n const sampling = config.modelOptions ?? {}\n const nestedOptions =\n sampling['options'] && typeof sampling['options'] === 'object'\n ? (sampling['options'] as Record<string, unknown>)\n : undefined\n const samplingTemperature = firstNumber(\n sampling['temperature'],\n nestedOptions?.['temperature'],\n )\n const samplingTopP = firstNumber(\n sampling['top_p'],\n sampling['topP'],\n nestedOptions?.['top_p'],\n )\n // Spellings come from the shared `MAX_TOKENS_KEYS` table so this stays\n // in lockstep with the summarize wrapper's caller-limit detection.\n const samplingMaxTokens = firstNumber(\n ...MAX_TOKENS_KEYS.map((k) => sampling[k]),\n nestedOptions?.[NESTED_MAX_TOKENS_KEY],\n )\n if (samplingTemperature !== undefined)\n baseAttrs['gen_ai.request.temperature'] = samplingTemperature\n if (samplingTopP !== undefined)\n baseAttrs['gen_ai.request.top_p'] = samplingTopP\n if (samplingMaxTokens !== undefined)\n baseAttrs['gen_ai.request.max_tokens'] = samplingMaxTokens\n\n const baseOptions: SpanOptions = {\n kind: SpanKind.CLIENT,\n attributes: baseAttrs,\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n\n const parentCtx = otelTrace.setSpan(\n otelContext.active(),\n state.rootSpan,\n )\n let iterSpan!: Span\n otelContext.with(parentCtx, () => {\n // Pass the parent context explicitly as the 3rd arg — this is a\n // real-OTel-compatible way to ensure the span is parented to\n // `rootSpan` even when the host app has not registered a context\n // manager (e.g. in tests or minimal setups).\n iterSpan = tracer.startSpan(name, spanOptions, parentCtx)\n })\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) iterSpan.setAttributes(enriched)\n\n state.currentIterationSpan = iterSpan\n state.assistantTextBuffer = ''\n state.assistantTextBufferTruncated = false\n\n if (captureContent) {\n const systemPromptContents = config.systemPrompts.map((p) =>\n typeof p === 'string' ? p : p.content,\n )\n // Anthropic prompt-caching users need to know which prompt carried\n // `cache_control`: it's the one attribute that explains cache\n // hit/miss in observability. Serialise per-prompt metadata as a\n // single JSON span attribute so backends that don't understand\n // GenAI events can still surface it. Kept off span events to\n // avoid breaking the one-event-per-message GenAI semconv contract.\n const systemPromptMetadata = config.systemPrompts.map((p) =>\n typeof p === 'string' || p.metadata === undefined\n ? null\n : p.metadata,\n )\n if (systemPromptMetadata.some((m) => m !== null)) {\n iterSpan.setAttribute(\n 'tanstack.ai.system_prompt.metadata',\n JSON.stringify(systemPromptMetadata),\n )\n }\n // Span events follow the original GenAI semconv (one event per\n // message). Backends that read events get content this way.\n for (const sys of systemPromptContents) {\n iterSpan.addEvent('gen_ai.system.message', {\n content: redactContent(sys),\n })\n }\n for (const m of config.messages) {\n const body = serializeContent(m.content)\n if (body.length === 0) continue\n iterSpan.addEvent(messageEventName(m.role), {\n content: redactContent(body),\n })\n }\n\n // Also emit the current GenAI-semconv attribute form\n // (`gen_ai.input.messages`) — backends like PostHog read prompt\n // content from this attribute, not from span events.\n // Multimodal messages keep their parts structured so image / audio /\n // video / document references survive into the trace (#1525).\n const inputMessages: Array<{\n role: string\n content: string | Array<InputPart>\n }> = []\n for (const sys of systemPromptContents) {\n inputMessages.push({\n role: 'system',\n content: redactContent(sys),\n })\n }\n for (const m of config.messages) {\n if (Array.isArray(m.content)) {\n const parts = serializeParts(m.content, redactContent)\n if (parts.length === 0) continue\n inputMessages.push({ role: m.role, content: parts })\n continue\n }\n const body = serializeContent(m.content)\n if (body.length === 0) continue\n inputMessages.push({\n role: m.role,\n content: redactContent(body),\n })\n }\n if (inputMessages.length > 0) {\n const inputJson = JSON.stringify(inputMessages)\n // Current OTel GenAI semconv — Sentry / PostHog / Datadog read\n // prompt content from this attribute.\n iterSpan.setAttribute('gen_ai.input.messages', inputJson)\n // Langfuse-native attribute. Highest priority in Langfuse's OTLP\n // ingestion (checked before events and gen_ai.input.messages) so\n // the Input panel populates reliably. Harmless to other backends —\n // the attribute is namespaced and unrecognised keys are ignored.\n iterSpan.setAttribute('langfuse.observation.input', inputJson)\n\n // Mirror the first iteration's input onto the root span and at\n // trace level so Langfuse fills Input on the trace card and the\n // chat-level observation. Later iterations append tool-call /\n // assistant messages that are useful per-iteration but noise at\n // the chat / trace level.\n if (state.iterationCount === 0) {\n state.rootSpan.setAttribute(\n 'langfuse.observation.input',\n inputJson,\n )\n state.rootSpan.setAttribute('langfuse.trace.input', inputJson)\n }\n }\n }\n\n state.iterationCount += 1\n })\n return undefined\n },\n\n onChunk(ctx, chunk) {\n safeCall('otel.onChunk', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n if (captureContent && chunk.type === 'TEXT_MESSAGE_CONTENT') {\n appendAssistantText(state, chunk.delta)\n }\n\n if (chunk.type !== 'RUN_FINISHED') return\n const tanstack = tanstackMetadata(chunk)\n const extra = chunk as {\n finishReason?: string | null\n model?: string\n }\n const finishReason = extra.finishReason ?? tanstack?.finishReason\n const model = extra.model ?? tanstack?.model\n // Capture for the root-span finish_reasons attribute set in onFinish,\n // which receives base-shaped info without a finishReason field.\n if (finishReason) state.lastFinishReason = finishReason\n const span = state.currentIterationSpan\n if (!span) return\n\n if (finishReason) {\n span.setAttribute('gen_ai.response.finish_reasons', [finishReason])\n }\n if (model) span.setAttribute('gen_ai.response.model', model)\n\n // Set usage attributes on the iteration span directly from the chunk\n // so they're available before `onUsage` fires. Histogram recording is\n // deliberately NOT done here — the chat runner always invokes\n // `runOnUsage` when `chunk.usage` is present, and `onUsage` is the\n // canonical place for the metric. Recording in both would double-count.\n const tokenUsage = rebuildTokenUsage(chunk.usage, tanstack?.usage)\n if (tokenUsage) {\n span.setAttributes(usageAttributes(tokenUsage))\n }\n\n if (captureContent && state.assistantTextBuffer.length > 0) {\n const completion = redactContent(state.assistantTextBuffer)\n const outputJson = JSON.stringify([\n { role: 'assistant', content: completion },\n ])\n // Event form (older semconv) — kept for backends that consume it.\n span.addEvent('gen_ai.choice', { content: completion })\n // Attribute form (current semconv) — required by backends like\n // PostHog that read completion content from `gen_ai.output.messages`.\n span.setAttribute('gen_ai.output.messages', outputJson)\n // Langfuse-native attribute (highest priority in Langfuse mapping).\n span.setAttribute('langfuse.observation.output', outputJson)\n // Mirror to the root span and trace card. Each iteration overwrites,\n // so the final iteration's completion lands on the root — which is\n // the final answer the user saw, not an intermediate tool-call turn.\n state.rootSpan.setAttribute('langfuse.observation.output', outputJson)\n state.rootSpan.setAttribute('langfuse.trace.output', outputJson)\n state.assistantTextBuffer = ''\n state.assistantTextBufferTruncated = false\n }\n\n // Intentionally leave the iteration span open: tool spans started\n // after `RUN_FINISHED` (tool_calls finishReason) must nest under it,\n // and `onUsage` may still fire. The span is closed in `onConfig` when\n // the next iteration starts, or in `onFinish` / `onError` / `onAbort`.\n })\n return undefined\n },\n\n onUsage(ctx, usage) {\n if (ctx.activity !== 'chat') {\n // Media: stamp usage on the single span. No token histogram — media\n // unit billing lands as span attributes via usageAttributes, matching\n // prior media behavior and avoiding chat-shaped token metrics.\n safeCall('otel.onUsage', () => {\n const span = mediaSpans.get(ctx)\n if (span) span.setAttributes(usageAttributes(usage))\n })\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onUsage', () => {\n const state = stateByCtx.get(chatCtx)\n if (!state) return\n\n state.rootUsageAttributes = accumulateUsageAttributes(\n state.rootUsageAttributes,\n usage,\n )\n\n // Always record the token histogram — metrics don't depend on having\n // an iteration span, and skipping here would drop metric data if an\n // adapter emits `onUsage` outside the iteration window.\n if (tokenHistogram) {\n const metricAttrs = {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': chatCtx.model,\n }\n tokenHistogram.record(usage.promptTokens, {\n ...metricAttrs,\n 'gen_ai.token.type': 'input',\n })\n tokenHistogram.record(usage.completionTokens, {\n ...metricAttrs,\n 'gen_ai.token.type': 'output',\n })\n }\n\n const span = state.currentIterationSpan ?? state.rootSpan\n span.setAttributes(usageAttributes(usage))\n })\n },\n\n onBeforeToolCall(ctx, hookCtx) {\n safeCall('otel.onBeforeToolCall', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n const parent = state.currentIterationSpan ?? state.rootSpan\n\n const info: OtelSpanInfo<'tool'> = {\n kind: 'tool',\n ctx,\n toolName: hookCtx.toolName,\n toolCallId: hookCtx.toolCallId,\n iteration: state.iterationCount - 1,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `execute_tool ${hookCtx.toolName}`\n\n const baseAttrs: Record<string, AttributeValue> = {\n 'gen_ai.tool.name': hookCtx.toolName,\n 'gen_ai.tool.call.id': hookCtx.toolCallId,\n 'gen_ai.tool.type': 'function',\n }\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: baseAttrs,\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n\n const parentCtx = otelTrace.setSpan(otelContext.active(), parent)\n let toolSpan!: Span\n otelContext.with(parentCtx, () => {\n toolSpan = tracer.startSpan(name, spanOptions, parentCtx)\n })\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) toolSpan.setAttributes(enriched)\n\n // Stamp the tool args onto the tool span so backends that render an\n // input panel per span (e.g. PostHog) have something to show.\n if (captureContent) {\n const argsBody =\n typeof hookCtx.args === 'string'\n ? hookCtx.args\n : (safeCall('otel.serializeToolArgs', () =>\n JSON.stringify(hookCtx.args ?? null),\n ) ?? '[unserializable_tool_args]')\n const redactedArgs = redactContent(argsBody)\n const toolInputJson = JSON.stringify([\n { role: 'tool', content: redactedArgs },\n ])\n toolSpan.setAttribute('gen_ai.input.messages', toolInputJson)\n // Langfuse-native (highest priority in Langfuse mapping).\n toolSpan.setAttribute('langfuse.observation.input', toolInputJson)\n }\n\n state.toolSpans.set(hookCtx.toolCallId, {\n span: toolSpan,\n toolName: hookCtx.toolName,\n })\n })\n return undefined\n },\n\n onAfterToolCall(ctx, info) {\n safeCall('otel.onAfterToolCall', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n const entry = state.toolSpans.get(info.toolCallId)\n if (!entry) return\n const { span: toolSpan } = entry\n\n const outcome = info.ok ? 'success' : 'error'\n toolSpan.setAttribute('tanstack.ai.tool.outcome', outcome)\n\n if (!info.ok && info.error !== undefined) {\n toolSpan.recordException(info.error as Exception)\n const msg = errorMessage(info.error)\n toolSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...(msg !== undefined && { message: msg }),\n })\n }\n\n if (captureContent) {\n // Serialization can throw on circular refs or `BigInt` values. If it\n // does, fall back to a sentinel so the rest of this handler (span\n // end, onSpanEnd, toolSpans cleanup) still runs — otherwise the tool\n // span would dangle until the onFinish/onError sweep.\n const body =\n typeof info.result === 'string'\n ? info.result\n : (safeCall('otel.serializeToolResult', () =>\n JSON.stringify(info.result ?? null),\n ) ?? '[unserializable_tool_result]')\n const redactedBody = redactContent(body)\n if (state.currentIterationSpan) {\n state.currentIterationSpan.addEvent('gen_ai.tool.message', {\n content: redactedBody,\n tool_call_id: info.toolCallId,\n })\n }\n // Output panel of the tool span itself — `gen_ai.output.messages` is\n // what current GenAI semconv consumers (e.g. PostHog) read.\n const toolOutputJson = JSON.stringify([\n { role: 'tool', content: redactedBody },\n ])\n toolSpan.setAttribute('gen_ai.output.messages', toolOutputJson)\n // Langfuse-native (highest priority in Langfuse mapping).\n toolSpan.setAttribute('langfuse.observation.output', toolOutputJson)\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolName: info.toolName,\n toolCallId: info.toolCallId,\n iteration: state.iterationCount - 1,\n },\n toolSpan,\n ),\n )\n toolSpan.end()\n state.toolSpans.delete(info.toolCallId)\n })\n },\n\n onError(ctx, info) {\n if (ctx.activity !== 'chat') {\n safeCall('otel.onError', () => {\n const message = errorMessage(info.error)\n endMediaSpan(ctx, (span) => {\n span.recordException(info.error as Exception)\n span.setStatus({\n code: SpanStatusCode.ERROR,\n ...(message !== undefined ? { message } : {}),\n })\n })\n recordMediaDuration(ctx, info.duration, errorTypeName(info.error))\n })\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onError', () => {\n const state = stateByCtx.get(chatCtx)\n if (!state) return\n\n const errType = errorTypeName(info.error)\n const message = errorMessage(info.error)\n const statusMessage =\n message !== undefined ? { message } : ({} as const)\n const exception = info.error as Exception\n\n const iterationSpan = state.currentIterationSpan\n if (iterationSpan) {\n iterationSpan.recordException(exception)\n iterationSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...statusMessage,\n })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx: chatCtx,\n iteration: state.iterationCount - 1,\n },\n iterationSpan,\n ),\n )\n iterationSpan.end()\n state.currentIterationSpan = null\n }\n\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n span.recordException(exception)\n span.setStatus({ code: SpanStatusCode.ERROR, ...statusMessage })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx: chatCtx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n\n state.rootSpan.recordException(exception)\n state.rootSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...statusMessage,\n })\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': chatCtx.model,\n 'error.type': errType,\n })\n }\n\n applyRootUsage(state)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx: chatCtx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(chatCtx)\n })\n },\n\n onAbort(ctx, info) {\n if (ctx.activity !== 'chat') {\n // Media abandonment (e.g. a video stream dropped before completion).\n safeCall('otel.onAbort', () => {\n endMediaSpan(ctx, (span) => {\n span.setAttribute('tanstack.ai.completion.reason', 'cancelled')\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: info.reason ?? 'cancelled',\n })\n })\n recordMediaDuration(ctx, info.duration, 'cancelled')\n })\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onAbort', () => {\n const state = stateByCtx.get(chatCtx)\n if (!state) return\n\n const closeCancelled = (span: Span): void => {\n // `gen_ai.completion.reason` is not part of the GenAI semconv; use a\n // TanStack-namespaced attribute so downstream exporters don't treat\n // it as standard. The span status still carries the error code.\n span.setAttribute('tanstack.ai.completion.reason', 'cancelled')\n span.setStatus({ code: SpanStatusCode.ERROR, message: 'cancelled' })\n }\n\n const iterationSpan = state.currentIterationSpan\n if (iterationSpan) {\n closeCancelled(iterationSpan)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx: chatCtx,\n iteration: state.iterationCount - 1,\n },\n iterationSpan,\n ),\n )\n iterationSpan.end()\n state.currentIterationSpan = null\n }\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n closeCancelled(span)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx: chatCtx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n closeCancelled(state.rootSpan)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': chatCtx.model,\n 'error.type': 'cancelled',\n })\n }\n\n applyRootUsage(state)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx: chatCtx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(chatCtx)\n })\n },\n\n onFinish(ctx, info) {\n if (ctx.activity !== 'chat') {\n safeCall('otel.onFinish', () => {\n endMediaSpan(ctx, (span) => {\n if (info.usage) span.setAttributes(usageAttributes(info.usage))\n })\n recordMediaDuration(ctx, info.duration)\n })\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onFinish', () => {\n const state = stateByCtx.get(chatCtx)\n if (!state) return\n\n // Close any tool spans that never received `onAfterToolCall` (adapter\n // quirk). Done before the iteration span so the hierarchy is closed\n // in depth-first order.\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n span.setAttribute('tanstack.ai.tool.outcome', 'unknown')\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx: chatCtx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n\n // The final iteration's span is still open because we keep it open\n // through tool execution and `onUsage`. Close it now.\n closeIterationSpan(state, chatCtx)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': chatCtx.model,\n })\n }\n\n if (state.lastFinishReason) {\n state.rootSpan.setAttribute('gen_ai.response.finish_reasons', [\n state.lastFinishReason,\n ])\n }\n state.rootSpan.setAttribute(\n 'tanstack.ai.iterations',\n state.iterationCount,\n )\n\n applyRootUsage(state, info.usage)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx: chatCtx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(chatCtx)\n })\n },\n }\n}\n"],"mappings":"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"}
@@ -16,6 +16,8 @@ export type ToolCallState = 'awaiting-input' | 'input-streaming' | 'input-comple
16
16
  * Tool result states - track the lifecycle of a tool result
17
17
  */
18
18
  export type ToolResultState = 'streaming' | 'complete' | 'error';
19
+ /** Why a tool result ended without executing successfully. */
20
+ export type ToolResultOutcome = 'cancelled' | 'denied';
19
21
  export type ToolOutputState = 'output-available' | 'output-error';
20
22
  /**
21
23
  * JSON Schema type for defining tool input/output schemas as raw JSON Schema objects.
@@ -95,6 +97,12 @@ export interface ToolCall<TMetadata = unknown> extends Omit<AGUIToolCall, 'metad
95
97
  * `@tanstack/ai-gemini` sets this to `{ thoughtSignature?: string }`. */
96
98
  metadata?: TMetadata;
97
99
  }
100
+ /** One source link from a provider-executed web search. */
101
+ export interface ProviderExecutedToolSource {
102
+ url: string;
103
+ title?: string;
104
+ pageAge?: string;
105
+ }
98
106
  /**
99
107
  * Convention for tool-call `metadata` that marks a call as **provider-executed**
100
108
  * — run by the provider's own infrastructure (e.g. Anthropic `web_search` /
@@ -108,10 +116,12 @@ export interface ToolCall<TMetadata = unknown> extends Omit<AGUIToolCall, 'metad
108
116
  *
109
117
  * Provider-specific payloads live under a namespaced key (e.g. `anthropic`),
110
118
  * keeping this convention opaque to the framework core. The index signature
111
- * preserves those per-adapter fields.
119
+ * preserves those per-adapter fields. `sources` is the normalized list of
120
+ * links a web search used, shared across providers.
112
121
  */
113
122
  export interface ProviderExecutedToolMetadata {
114
123
  providerExecuted?: boolean;
124
+ sources?: Array<ProviderExecutedToolSource>;
115
125
  [key: string]: unknown;
116
126
  }
117
127
  /**
@@ -216,9 +226,15 @@ export interface ModelMessage<TContent extends string | null | Array<ContentPart
216
226
  name?: string;
217
227
  toolCalls?: Array<ToolCall>;
218
228
  toolCallId?: string;
229
+ /**
230
+ * Signed thinking to send back to the provider. `redacted: true` marks a
231
+ * block the provider encrypted: `content` is empty and `signature` holds its
232
+ * opaque data. See `ThinkingPart.signature` for the planned rename.
233
+ */
219
234
  thinking?: Array<{
220
235
  content: string;
221
236
  signature?: string;
237
+ redacted?: boolean;
222
238
  }>;
223
239
  /** Error reported by an AG-UI tool message. */
224
240
  error?: string;
@@ -289,6 +305,8 @@ export interface ToolResultPart {
289
305
  toolCallId: string;
290
306
  content: string | Array<ContentPart>;
291
307
  state: ToolResultState;
308
+ /** Set when the user or middleware cancelled or denied the tool call; state remains `error`. */
309
+ outcome?: ToolResultOutcome;
292
310
  error?: string;
293
311
  metadata?: Record<string, unknown>;
294
312
  createdAt?: Date;
@@ -297,7 +315,20 @@ export interface ThinkingPart {
297
315
  type: 'thinking';
298
316
  content: string;
299
317
  stepId?: string;
318
+ /**
319
+ * The provider's opaque reasoning artefact, sent back unchanged: an
320
+ * Anthropic signature, Anthropic redacted data, or OpenAI encrypted content.
321
+ * TODO(#1581): rename to `encryptedValue` to match AG-UI's `ReasoningMessage`.
322
+ * Renaming breaks stored messages, so it needs a read shim for `signature`.
323
+ */
300
324
  signature?: string;
325
+ /**
326
+ * The provider encrypted this thinking block (Anthropic `redacted_thinking`).
327
+ * `content` is empty, and `signature` holds the opaque data that goes back
328
+ * to the provider unchanged. On the AG-UI wire, the reasoning message id
329
+ * starts with `redacted_thinking-` instead.
330
+ */
331
+ redacted?: boolean;
301
332
  }
302
333
  /**
303
334
  * Recursive `Partial` — every nested field becomes optional. Used as the
@@ -383,6 +414,16 @@ export interface TanStackMessageMetadata {
383
414
  model?: string;
384
415
  /** Parent chat run that produced this assistant message. */
385
416
  runId?: string;
417
+ /**
418
+ * The chat run that produced this assistant message. `withPersistence` sets
419
+ * `id`. `reconstructChat` with `includeRuns: true` adds the finished run's
420
+ * timings, in epoch ms.
421
+ */
422
+ run?: {
423
+ id: string;
424
+ startedAt?: number;
425
+ finishedAt?: number;
426
+ };
386
427
  /** Card data on a child wire message. See `uiMessagesToWire`. */
387
428
  subagent?: SubagentWireInfo;
388
429
  /** Thinking signature for a `role: 'reasoning'` fan-out message. */
@@ -394,6 +435,8 @@ export interface TanStackMessageMetadata {
394
435
  createdAt?: string;
395
436
  content?: Array<ContentPart>;
396
437
  };
438
+ /** Outcome of a cancelled or denied tool result; when present, the UI state is `error`. */
439
+ toolResultOutcome?: ToolResultOutcome;
397
440
  structuredOutput?: {
398
441
  status?: 'streaming' | 'complete' | 'error';
399
442
  partial?: unknown;
@@ -485,6 +528,17 @@ export interface EmitCustomEventOptions {
485
528
  */
486
529
  batch?: boolean;
487
530
  }
531
+ /**
532
+ * The user's answer to an `mcp_input` interrupt.
533
+ * `resolved` carries the `payload` from `resolveInterrupt`.
534
+ * `cancelled` means the user called `cancel()`.
535
+ */
536
+ export type ToolInputResponse = {
537
+ status: 'resolved';
538
+ payload: unknown;
539
+ } | {
540
+ status: 'cancelled';
541
+ };
488
542
  /**
489
543
  * Context passed to tool execute functions, providing capabilities like
490
544
  * emitting custom events during execution.
@@ -498,6 +552,12 @@ export type ToolExecutionContext<TContext = unknown> = RuntimeContextField<TCont
498
552
  * e.g. MCP `callTool` — should forward this to cancel in-flight work.
499
553
  */
500
554
  abortSignal?: AbortSignal;
555
+ /**
556
+ * The answer to the input request that this tool call raised in the
557
+ * previous run. It is set only when the run resumes an `mcp_input`
558
+ * interrupt for this tool call.
559
+ */
560
+ inputResponse?: ToolInputResponse;
501
561
  /**
502
562
  * Emit a custom event during tool execution.
503
563
  * Events are streamed to the client in real-time as AG-UI CUSTOM events.
@@ -810,6 +870,11 @@ export interface TextOptions<TProviderOptionsSuperset extends Record<string, any
810
870
  */
811
871
  systemPrompts?: Array<SystemPrompt>;
812
872
  agentLoopStrategy?: AgentLoopStrategy;
873
+ /**
874
+ * How the server tools of one model turn run. `'parallel'` (the default)
875
+ * starts them together, and `'sequential'` runs them one at a time.
876
+ */
877
+ toolExecution?: 'parallel' | 'sequential';
813
878
  /**
814
879
  * Optional configuration for lazy-tool discovery (tools marked `lazy: true`).
815
880
  * Tunes how much of each lazy tool's description appears in the discovery
@@ -1845,9 +1910,10 @@ export interface VideoGenerationOptions<TProviderOptions extends object = object
1845
1910
  /** Video size — format depends on the provider (e.g., "16:9", "1280x720") */
1846
1911
  size?: TSize;
1847
1912
  /**
1848
- * Video duration in seconds. Adapters that declare a per-model duration
1849
- * map narrow this to the model's valid union; use
1850
- * `adapter.snapDuration(seconds)` to coerce raw seconds to a valid value.
1913
+ * Video duration. Adapters that declare a per-model duration map narrow
1914
+ * this to that model's union (a number, `"8"`, or `"8s"`). Use
1915
+ * `adapter.snapDuration(input)` to coerce a raw value. `input` may be
1916
+ * seconds, a `"6s"` template, or `"auto"` when the model lists it.
1851
1917
  */
1852
1918
  duration?: TDuration;
1853
1919
  /** Model-specific options for video generation */