@tanstack/ai 0.61.0 → 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +1 -0
  2. package/dist/esm/activities/chat/agents/define-agent.d.ts +17 -5
  3. package/dist/esm/activities/chat/agents/define-agent.js.map +1 -1
  4. package/dist/esm/activities/chat/agents/spawn.d.ts +2 -0
  5. package/dist/esm/activities/chat/agents/spawn.js +8 -5
  6. package/dist/esm/activities/chat/agents/spawn.js.map +1 -1
  7. package/dist/esm/activities/chat/index.js +156 -57
  8. package/dist/esm/activities/chat/index.js.map +1 -1
  9. package/dist/esm/activities/chat/messages.js +30 -18
  10. package/dist/esm/activities/chat/messages.js.map +1 -1
  11. package/dist/esm/activities/chat/middleware/types.d.ts +1 -0
  12. package/dist/esm/activities/chat/middleware/types.js.map +1 -1
  13. package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
  14. package/dist/esm/activities/chat/stream/message-updaters.js +2 -1
  15. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  16. package/dist/esm/activities/chat/stream/processor.d.ts +11 -9
  17. package/dist/esm/activities/chat/stream/processor.js +38 -18
  18. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  19. package/dist/esm/activities/chat/tools/tool-calls.d.ts +16 -2
  20. package/dist/esm/activities/chat/tools/tool-calls.js +55 -16
  21. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  22. package/dist/esm/activities/chat/tools/tool-definition.d.ts +4 -0
  23. package/dist/esm/activities/chat/tools/tool-definition.js +4 -0
  24. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  25. package/dist/esm/activities/evaluate/adapter.d.ts +4 -0
  26. package/dist/esm/activities/evaluate/adapter.js.map +1 -1
  27. package/dist/esm/activities/evaluate/index.d.ts +4 -0
  28. package/dist/esm/activities/evaluate/index.js +3 -1
  29. package/dist/esm/activities/evaluate/index.js.map +1 -1
  30. package/dist/esm/client.d.ts +1 -1
  31. package/dist/esm/client.js.map +1 -1
  32. package/dist/esm/index.d.ts +1 -1
  33. package/dist/esm/index.js +2 -2
  34. package/dist/esm/interrupt-resume.js +29 -4
  35. package/dist/esm/interrupt-resume.js.map +1 -1
  36. package/dist/esm/middlewares/otel.d.ts +5 -2
  37. package/dist/esm/middlewares/otel.js +114 -0
  38. package/dist/esm/middlewares/otel.js.map +1 -1
  39. package/dist/esm/types.d.ts +42 -1
  40. package/dist/esm/utilities/ag-ui-wire.js +5 -3
  41. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  42. package/dist/esm/utilities/tool-result.d.ts +2 -1
  43. package/dist/esm/utilities/tool-result.js +4 -1
  44. package/dist/esm/utilities/tool-result.js.map +1 -1
  45. package/package.json +3 -3
  46. package/skills/ai-core/chat-experience/SKILL.md +120 -0
  47. package/skills/ai-core/tool-calling/SKILL.md +103 -0
  48. package/src/activities/chat/agents/define-agent.ts +20 -3
  49. package/src/activities/chat/agents/spawn.ts +20 -12
  50. package/src/activities/chat/index.ts +235 -62
  51. package/src/activities/chat/messages.ts +40 -5
  52. package/src/activities/chat/middleware/types.ts +1 -0
  53. package/src/activities/chat/stream/message-updaters.ts +3 -0
  54. package/src/activities/chat/stream/processor.ts +57 -27
  55. package/src/activities/chat/tools/tool-calls.ts +98 -9
  56. package/src/activities/chat/tools/tool-definition.ts +8 -0
  57. package/src/activities/evaluate/adapter.ts +4 -0
  58. package/src/activities/evaluate/index.ts +6 -0
  59. package/src/client.ts +1 -0
  60. package/src/index.ts +1 -0
  61. package/src/interrupt-resume.ts +55 -4
  62. package/src/middlewares/otel.ts +161 -3
  63. package/src/types.ts +38 -1
  64. package/src/utilities/ag-ui-wire.ts +12 -1
  65. package/src/utilities/tool-result.ts +7 -1
@@ -53,7 +53,9 @@ export interface OtelMiddlewareOptions {
53
53
  meter?: Meter;
54
54
  /**
55
55
  * When `true`, prompt and completion content is attached to iteration spans
56
- * as `gen_ai.*.message` / `gen_ai.choice` events. Defaults to `false` so
56
+ * as `gen_ai.*.message` / `gen_ai.choice` events. Media spans get the
57
+ * prompt, input media and output (URLs or transcript text) as
58
+ * `gen_ai.input.messages` / `gen_ai.output.messages`. Defaults to `false` so
57
59
  * that PII never lands on a span by accident.
58
60
  */
59
61
  captureContent?: boolean;
@@ -66,7 +68,8 @@ export interface OtelMiddlewareOptions {
66
68
  redact?: (text: string) => string;
67
69
  /**
68
70
  * Maximum characters kept in the per-iteration assistant text buffer used
69
- * to emit `gen_ai.choice` events. Extra characters are truncated with a
71
+ * to emit `gen_ai.choice` events, and in each text part of a media span's
72
+ * input and output. Extra characters are truncated with a
70
73
  * trailing `"…"` marker. Defaults to 100 000. Set to `0` to disable the
71
74
  * cap. Exporters typically truncate long attribute values anyway.
72
75
  */
@@ -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
  /**
@@ -289,6 +299,8 @@ export interface ToolResultPart {
289
299
  toolCallId: string;
290
300
  content: string | Array<ContentPart>;
291
301
  state: ToolResultState;
302
+ /** Set when the user or middleware cancelled or denied the tool call; state remains `error`. */
303
+ outcome?: ToolResultOutcome;
292
304
  error?: string;
293
305
  metadata?: Record<string, unknown>;
294
306
  createdAt?: Date;
@@ -383,6 +395,16 @@ export interface TanStackMessageMetadata {
383
395
  model?: string;
384
396
  /** Parent chat run that produced this assistant message. */
385
397
  runId?: string;
398
+ /**
399
+ * The chat run that produced this assistant message. `withPersistence` sets
400
+ * `id`. `reconstructChat` with `includeRuns: true` adds the finished run's
401
+ * timings, in epoch ms.
402
+ */
403
+ run?: {
404
+ id: string;
405
+ startedAt?: number;
406
+ finishedAt?: number;
407
+ };
386
408
  /** Card data on a child wire message. See `uiMessagesToWire`. */
387
409
  subagent?: SubagentWireInfo;
388
410
  /** Thinking signature for a `role: 'reasoning'` fan-out message. */
@@ -394,6 +416,8 @@ export interface TanStackMessageMetadata {
394
416
  createdAt?: string;
395
417
  content?: Array<ContentPart>;
396
418
  };
419
+ /** Outcome of a cancelled or denied tool result; when present, the UI state is `error`. */
420
+ toolResultOutcome?: ToolResultOutcome;
397
421
  structuredOutput?: {
398
422
  status?: 'streaming' | 'complete' | 'error';
399
423
  partial?: unknown;
@@ -485,6 +509,17 @@ export interface EmitCustomEventOptions {
485
509
  */
486
510
  batch?: boolean;
487
511
  }
512
+ /**
513
+ * The user's answer to an `mcp_input` interrupt.
514
+ * `resolved` carries the `payload` from `resolveInterrupt`.
515
+ * `cancelled` means the user called `cancel()`.
516
+ */
517
+ export type ToolInputResponse = {
518
+ status: 'resolved';
519
+ payload: unknown;
520
+ } | {
521
+ status: 'cancelled';
522
+ };
488
523
  /**
489
524
  * Context passed to tool execute functions, providing capabilities like
490
525
  * emitting custom events during execution.
@@ -498,6 +533,12 @@ export type ToolExecutionContext<TContext = unknown> = RuntimeContextField<TCont
498
533
  * e.g. MCP `callTool` — should forward this to cancel in-flight work.
499
534
  */
500
535
  abortSignal?: AbortSignal;
536
+ /**
537
+ * The answer to the input request that this tool call raised in the
538
+ * previous run. It is set only when the run resumes an `mcp_input`
539
+ * interrupt for this tool call.
540
+ */
541
+ inputResponse?: ToolInputResponse;
501
542
  /**
502
543
  * Emit a custom event during tool execution.
503
544
  * Events are streamed to the client in real-time as AG-UI CUSTOM events.
@@ -7,7 +7,7 @@ import { coerceCreatedAt, modelMessageToUIMessage } from "../activities/chat/mes
7
7
  function isRecord(value) {
8
8
  return typeof value === "object" && value !== null && !Array.isArray(value);
9
9
  }
10
- function rebuiltToolMetadata(metadata, createdAt, id, content, anchorOwnsUiResources = false) {
10
+ function rebuiltToolMetadata(metadata, createdAt, id, content, anchorOwnsUiResources = false, outcome) {
11
11
  const source = isRecord(metadata) ? metadata : {};
12
12
  const tanstack = isRecord(source.tanstack) ? { ...source.tanstack } : {};
13
13
  if (anchorOwnsUiResources) delete tanstack.uiResources;
@@ -21,6 +21,7 @@ function rebuiltToolMetadata(metadata, createdAt, id, content, anchorOwnsUiResou
21
21
  ...source,
22
22
  tanstack: {
23
23
  ...tanstack,
24
+ ...outcome !== void 0 && { toolResultOutcome: outcome },
24
25
  toolResult
25
26
  }
26
27
  };
@@ -105,7 +106,7 @@ function uiMessagesToWire(messages, options) {
105
106
  const explicitToolResults = new Set(parts.flatMap((part) => part.type === "tool-result" ? [part.toolCallId] : []));
106
107
  for (const part of parts) if (part.type === "tool-result") {
107
108
  const id = uniqueToolWireId(part.id ?? deriveToolMessageId(part.toolCallId), usedWireIds);
108
- const metadata = rebuiltToolMetadata(part.metadata, part.createdAt, part.id, part.content, true);
109
+ const metadata = rebuiltToolMetadata(part.metadata, part.createdAt, part.id, part.content, true, part.outcome);
109
110
  wire.push({
110
111
  role: "tool",
111
112
  id,
@@ -129,7 +130,7 @@ function uiMessagesToWire(messages, options) {
129
130
  id: uniqueToolWireId(deriveToolMessageId(part.id), usedWireIds),
130
131
  toolCallId: part.id,
131
132
  content,
132
- metadata: rebuiltToolMetadata(void 0, void 0, void 0, result)
133
+ metadata: rebuiltToolMetadata(void 0, void 0, void 0, result, false, part.approval?.approved === false ? "denied" : void 0)
133
134
  });
134
135
  }
135
136
  for (const part of parts) if (part.type === "subagent") wire.push(...subagentToWire(part, options));
@@ -166,6 +167,7 @@ function messageMetadata(msg, parts, includeSnapshotStructuredOutput) {
166
167
  const tanstack = {};
167
168
  if (previousTanstack?.model !== void 0) tanstack.model = previousTanstack.model;
168
169
  if (previousTanstack?.runId !== void 0) tanstack.runId = previousTanstack.runId;
170
+ if (previousTanstack?.run?.id !== void 0) tanstack.run = { id: previousTanstack.run.id };
169
171
  if (previousTanstack?.signature !== void 0) tanstack.signature = previousTanstack.signature;
170
172
  const createdAt = coerceCreatedAt(msg.createdAt);
171
173
  if (createdAt !== void 0) tanstack.createdAt = createdAt.toISOString();