@tanstack/ai 0.14.0 → 0.16.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 (30) hide show
  1. package/dist/esm/activities/chat/adapter.d.ts +6 -3
  2. package/dist/esm/activities/chat/adapter.js.map +1 -1
  3. package/dist/esm/activities/chat/index.js +61 -9
  4. package/dist/esm/activities/chat/index.js.map +1 -1
  5. package/dist/esm/activities/chat/messages.js +26 -3
  6. package/dist/esm/activities/chat/messages.js.map +1 -1
  7. package/dist/esm/activities/chat/stream/message-updaters.d.ts +4 -2
  8. package/dist/esm/activities/chat/stream/message-updaters.js +10 -4
  9. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  10. package/dist/esm/activities/chat/stream/processor.d.ts +21 -5
  11. package/dist/esm/activities/chat/stream/processor.js +119 -19
  12. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  13. package/dist/esm/activities/chat/stream/types.d.ts +9 -1
  14. package/dist/esm/activities/chat/tools/tool-calls.js +3 -4
  15. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  16. package/dist/esm/middlewares/otel.d.ts +75 -0
  17. package/dist/esm/middlewares/otel.js +624 -0
  18. package/dist/esm/middlewares/otel.js.map +1 -0
  19. package/dist/esm/types.d.ts +23 -7
  20. package/package.json +15 -2
  21. package/src/activities/chat/adapter.ts +8 -2
  22. package/src/activities/chat/index.ts +74 -11
  23. package/src/activities/chat/messages.ts +29 -1
  24. package/src/activities/chat/stream/message-updaters.ts +18 -3
  25. package/src/activities/chat/stream/processor.ts +152 -17
  26. package/src/activities/chat/stream/types.ts +9 -1
  27. package/src/activities/chat/tools/tool-calls.ts +4 -4
  28. package/src/middlewares/index.ts +5 -0
  29. package/src/middlewares/otel.ts +861 -0
  30. package/src/types.ts +20 -7
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otel.js","sources":["../../../src/middlewares/otel.ts"],"sourcesContent":["import {\n SpanKind,\n SpanStatusCode,\n context as otelContext,\n trace as otelTrace,\n} from '@opentelemetry/api'\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'\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 agent-loop iteration (one model call)\n * - `tool` — one per tool execution inside an iteration\n */\nexport type OtelSpanScope = 'chat' | 'iteration' | 'tool'\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 : never\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\nconst stateByCtx = new WeakMap<ChatMiddlewareContext, RequestState>()\n\nconst DEFAULT_MAX_CONTENT_LENGTH = 100_000\nconst REDACTION_FAILED_SENTINEL = '[redaction_failed]'\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 default:\n parts.push(`[${type ?? 'unknown'}]`)\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 errorMessage(err: unknown): string | undefined {\n if (err instanceof Error) return err.message\n if (typeof err === 'string') return err\n if (err && typeof err === 'object' && 'message' in err) {\n const m = (err as { message?: unknown }).message\n if (typeof m === 'string') return m\n }\n return undefined\n}\n\nfunction errorTypeName(err: unknown): string {\n if (err instanceof Error) return err.name || 'Error'\n if (err && typeof err === 'object' && 'name' in err) {\n const n = (err as { name?: unknown }).name\n if (typeof n === 'string') return n\n }\n return 'Error'\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(options: OtelMiddlewareOptions): 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?.(\n { kind: 'iteration', ctx, iteration } as OtelSpanInfo<'iteration'>,\n span,\n ),\n )\n span.end()\n state.currentIterationSpan = null\n }\n\n return {\n name: 'otel',\n\n onStart(ctx) {\n safeCall('otel.onStart', () => {\n const info: OtelSpanInfo<'chat'> = { kind: 'chat', ctx }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${ctx.model}`\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.request.model': ctx.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(ctx, {\n rootSpan,\n currentIterationSpan: null,\n toolSpans: new Map(),\n iterationCount: 0,\n assistantTextBuffer: '',\n assistantTextBufferTruncated: false,\n startTime: Date.now(),\n })\n })\n },\n\n onConfig(ctx, config) {\n if (ctx.phase !== 'beforeModel') 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 const info: OtelSpanInfo<'iteration'> = {\n kind: 'iteration',\n ctx,\n iteration: ctx.iteration,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${ctx.model} #${ctx.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': ctx.iteration,\n }\n if (config.temperature !== undefined)\n baseAttrs['gen_ai.request.temperature'] = config.temperature\n if (config.topP !== undefined)\n baseAttrs['gen_ai.request.top_p'] = config.topP\n if (config.maxTokens !== undefined)\n baseAttrs['gen_ai.request.max_tokens'] = config.maxTokens\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 // 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 config.systemPrompts) {\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 config.systemPrompts) {\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 span = state.currentIterationSpan\n if (!span) return\n\n if (chunk.finishReason) {\n span.setAttribute('gen_ai.response.finish_reasons', [\n chunk.finishReason,\n ])\n }\n if (chunk.model) span.setAttribute('gen_ai.response.model', chunk.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 if (chunk.usage) {\n span.setAttributes({\n 'gen_ai.usage.input_tokens': chunk.usage.promptTokens,\n 'gen_ai.usage.output_tokens': chunk.usage.completionTokens,\n })\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 safeCall('otel.onUsage', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\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': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.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({\n 'gen_ai.usage.input_tokens': usage.promptTokens,\n 'gen_ai.usage.output_tokens': usage.completionTokens,\n })\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 toolSpan.setStatus({\n code: SpanStatusCode.ERROR,\n message: errorMessage(info.error),\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 } as OtelSpanInfo<'tool'>,\n toolSpan,\n ),\n )\n toolSpan.end()\n state.toolSpans.delete(info.toolCallId)\n })\n },\n\n onError(ctx, info) {\n safeCall('otel.onError', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n const errType = errorTypeName(info.error)\n const message = errorMessage(info.error)\n const exception = info.error as Exception\n\n if (state.currentIterationSpan) {\n state.currentIterationSpan.recordException(exception)\n state.currentIterationSpan.setStatus({\n code: SpanStatusCode.ERROR,\n message,\n })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx,\n iteration: state.iterationCount - 1,\n } as OtelSpanInfo<'iteration'>,\n state.currentIterationSpan!,\n ),\n )\n state.currentIterationSpan.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, message })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n } as OtelSpanInfo<'tool'>,\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n\n state.rootSpan.recordException(exception)\n state.rootSpan.setStatus({ code: SpanStatusCode.ERROR, message })\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'error.type': errType,\n })\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n\n onAbort(ctx, info) {\n safeCall('otel.onAbort', () => {\n const state = stateByCtx.get(ctx)\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 if (state.currentIterationSpan) {\n closeCancelled(state.currentIterationSpan)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx,\n iteration: state.iterationCount - 1,\n } as OtelSpanInfo<'iteration'>,\n state.currentIterationSpan!,\n ),\n )\n state.currentIterationSpan.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,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n } as OtelSpanInfo<'tool'>,\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': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'error.type': 'cancelled',\n })\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n\n onFinish(ctx, info) {\n safeCall('otel.onFinish', () => {\n const state = stateByCtx.get(ctx)\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,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n } as OtelSpanInfo<'tool'>,\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, ctx)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n })\n }\n\n if (info.usage) {\n state.rootSpan.setAttributes({\n 'gen_ai.usage.input_tokens': info.usage.promptTokens,\n 'gen_ai.usage.output_tokens': info.usage.completionTokens,\n })\n }\n if (info.finishReason) {\n state.rootSpan.setAttribute('gen_ai.response.finish_reasons', [\n info.finishReason,\n ])\n }\n state.rootSpan.setAttribute(\n 'tanstack.ai.iterations',\n state.iterationCount,\n )\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n }\n}\n"],"names":["otelTrace","otelContext"],"mappings":"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"}
@@ -70,15 +70,17 @@ export type SchemaInput = StandardJSONSchemaV1<any, any> | JSONSchema;
70
70
  * For plain JSONSchema, returns `any` since we can't infer types from JSON Schema at compile time.
71
71
  */
72
72
  export type InferSchemaType<T> = T extends StandardJSONSchemaV1<infer TInput, unknown> ? TInput : unknown;
73
- export interface ToolCall {
73
+ export interface ToolCall<TMetadata = unknown> {
74
74
  id: string;
75
75
  type: 'function';
76
76
  function: {
77
77
  name: string;
78
78
  arguments: string;
79
79
  };
80
- /** Provider-specific metadata to carry through the tool call lifecycle */
81
- providerMetadata?: Record<string, unknown>;
80
+ /** Provider-specific metadata to carry through the tool call lifecycle.
81
+ * Typed per-adapter via `TToolCallMetadata`. For example,
82
+ * `@tanstack/ai-gemini` sets this to `{ thoughtSignature?: string }`. */
83
+ metadata?: TMetadata;
82
84
  }
83
85
  /**
84
86
  * Supported input modality types for multimodal content.
@@ -208,6 +210,10 @@ export interface ModelMessage<TContent extends string | null | Array<ContentPart
208
210
  name?: string;
209
211
  toolCalls?: Array<ToolCall>;
210
212
  toolCallId?: string;
213
+ thinking?: Array<{
214
+ content: string;
215
+ signature?: string;
216
+ }>;
211
217
  }
212
218
  /**
213
219
  * Message parts - building blocks of UIMessage
@@ -217,7 +223,7 @@ export interface TextPart<TMetadata = unknown> {
217
223
  content: string;
218
224
  metadata?: TMetadata;
219
225
  }
220
- export interface ToolCallPart {
226
+ export interface ToolCallPart<TMetadata = unknown> {
221
227
  type: 'tool-call';
222
228
  id: string;
223
229
  name: string;
@@ -231,6 +237,9 @@ export interface ToolCallPart {
231
237
  };
232
238
  /** Tool execution output (for client tools or after approval) */
233
239
  output?: any;
240
+ /** Provider-specific metadata that round-trips with the tool call.
241
+ * Typed per-adapter via `TToolCallMetadata`. */
242
+ metadata?: TMetadata;
234
243
  }
235
244
  export interface ToolResultPart {
236
245
  type: 'tool-result';
@@ -242,6 +251,8 @@ export interface ToolResultPart {
242
251
  export interface ThinkingPart {
243
252
  type: 'thinking';
244
253
  content: string;
254
+ stepId?: string;
255
+ signature?: string;
245
256
  }
246
257
  export type MessagePart = TextPart | ImagePart | AudioPart | VideoPart | DocumentPart | ToolCallPart | ToolResultPart | ThinkingPart;
247
258
  /**
@@ -734,7 +745,7 @@ export interface TextMessageEndEvent extends AGUITextMessageEndEvent {
734
745
  * Emitted when a tool call starts.
735
746
  *
736
747
  * @ag-ui/core provides: `toolCallId`, `toolCallName`, `parentMessageId?`
737
- * TanStack AI adds: `model?`, `toolName` (deprecated alias), `index?`, `providerMetadata?`
748
+ * TanStack AI adds: `model?`, `toolName` (deprecated alias), `index?`, `metadata?`
738
749
  */
739
750
  export interface ToolCallStartEvent extends AGUIToolCallStartEvent {
740
751
  /** Model identifier for multi-model support */
@@ -746,8 +757,11 @@ export interface ToolCallStartEvent extends AGUIToolCallStartEvent {
746
757
  toolName: string;
747
758
  /** Index for parallel tool calls */
748
759
  index?: number;
749
- /** Provider-specific metadata to carry into the ToolCall */
750
- providerMetadata?: Record<string, unknown>;
760
+ /** Provider-specific metadata to carry into the ToolCall.
761
+ * Untyped at the event layer because events flow through a discriminated
762
+ * union that does not survive generics; adapters cast it to their typed
763
+ * `TToolCallMetadata` shape when emitting. */
764
+ metadata?: Record<string, unknown>;
751
765
  }
752
766
  /**
753
767
  * Emitted when tool call arguments are streaming.
@@ -827,6 +841,8 @@ export interface StepFinishedEvent extends AGUIStepFinishedEvent {
827
841
  delta?: string;
828
842
  /** Full accumulated thinking content (TanStack AI internal) */
829
843
  content?: string;
844
+ /** Provider signature for the thinking block */
845
+ signature?: string;
830
846
  }
831
847
  /**
832
848
  * Emitted to provide a snapshot of all messages in a conversation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Core TanStack AI library - Open source AI SDK",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -25,6 +25,10 @@
25
25
  "types": "./dist/esm/middlewares/index.d.ts",
26
26
  "import": "./dist/esm/middlewares/index.js"
27
27
  },
28
+ "./middlewares/otel": {
29
+ "types": "./dist/esm/middlewares/otel.d.ts",
30
+ "import": "./dist/esm/middlewares/otel.js"
31
+ },
28
32
  "./adapter-internals": {
29
33
  "types": "./dist/esm/adapter-internals.d.ts",
30
34
  "import": "./dist/esm/adapter-internals.js"
@@ -51,9 +55,18 @@
51
55
  "dependencies": {
52
56
  "@ag-ui/core": "0.0.49",
53
57
  "partial-json": "^0.1.7",
54
- "@tanstack/ai-event-client": "0.2.8"
58
+ "@tanstack/ai-event-client": "0.3.0"
59
+ },
60
+ "peerDependencies": {
61
+ "@opentelemetry/api": ">=1.9.0"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "@opentelemetry/api": {
65
+ "optional": true
66
+ }
55
67
  },
56
68
  "devDependencies": {
69
+ "@opentelemetry/api": "^1.9.0",
57
70
  "@standard-schema/spec": "^1.1.0",
58
71
  "@vitest/coverage-v8": "4.0.14",
59
72
  "zod": "^4.2.0"
@@ -54,6 +54,7 @@ export interface StructuredOutputResult<T = unknown> {
54
54
  * - TInputModalities: Supported input modalities for this model (already resolved)
55
55
  * - TMessageMetadata: Metadata types for content parts (already resolved)
56
56
  * - TToolCapabilities: Tuple of tool-kind strings supported by this model, resolved from `supports.tools`
57
+ * - TToolCallMetadata: Metadata type that round-trips with tool calls (e.g. Gemini's `thoughtSignature`)
57
58
  */
58
59
  export interface TextAdapter<
59
60
  TModel extends string,
@@ -61,6 +62,7 @@ export interface TextAdapter<
61
62
  TInputModalities extends ReadonlyArray<Modality>,
62
63
  TMessageMetadataByModality extends DefaultMessageMetadataByModality,
63
64
  TToolCapabilities extends ReadonlyArray<string> = ReadonlyArray<string>,
65
+ TToolCallMetadata = unknown,
64
66
  > {
65
67
  /** Discriminator for adapter kind */
66
68
  readonly kind: 'text'
@@ -77,6 +79,7 @@ export interface TextAdapter<
77
79
  inputModalities: TInputModalities
78
80
  messageMetadataByModality: TMessageMetadataByModality
79
81
  toolCapabilities: TToolCapabilities
82
+ toolCallMetadata: TToolCallMetadata
80
83
  }
81
84
 
82
85
  /**
@@ -103,7 +106,7 @@ export interface TextAdapter<
103
106
  * A TextAdapter with any/unknown type parameters.
104
107
  * Useful as a constraint in generic functions and interfaces.
105
108
  */
106
- export type AnyTextAdapter = TextAdapter<any, any, any, any, any>
109
+ export type AnyTextAdapter = TextAdapter<any, any, any, any, any, any>
107
110
 
108
111
  /**
109
112
  * Abstract base class for text adapters.
@@ -117,12 +120,14 @@ export abstract class BaseTextAdapter<
117
120
  TInputModalities extends ReadonlyArray<Modality>,
118
121
  TMessageMetadataByModality extends DefaultMessageMetadataByModality,
119
122
  TToolCapabilities extends ReadonlyArray<string> = ReadonlyArray<string>,
123
+ TToolCallMetadata = unknown,
120
124
  > implements TextAdapter<
121
125
  TModel,
122
126
  TProviderOptions,
123
127
  TInputModalities,
124
128
  TMessageMetadataByModality,
125
- TToolCapabilities
129
+ TToolCapabilities,
130
+ TToolCallMetadata
126
131
  > {
127
132
  readonly kind = 'text' as const
128
133
  abstract readonly name: string
@@ -134,6 +139,7 @@ export abstract class BaseTextAdapter<
134
139
  inputModalities: TInputModalities
135
140
  messageMetadataByModality: TMessageMetadataByModality
136
141
  toolCapabilities: TToolCapabilities
142
+ toolCallMetadata: TToolCallMetadata
137
143
  }
138
144
 
139
145
  protected config: TextAdapterConfig
@@ -281,6 +281,10 @@ class TextEngine<
281
281
  private totalChunkCount = 0
282
282
  private currentMessageId: string | null = null
283
283
  private accumulatedContent = ''
284
+ private accumulatedThinking: Array<{ content: string; signature?: string }> =
285
+ []
286
+ private currentThinkingContent = ''
287
+ private currentThinkingSignature = ''
284
288
  private eventOptions?: Record<string, unknown>
285
289
  private eventToolNames?: Array<string>
286
290
  private finishedEvent: RunFinishedEvent | null = null
@@ -556,6 +560,9 @@ class TextEngine<
556
560
  private async beginIteration(): Promise<void> {
557
561
  this.currentMessageId = this.createId('msg')
558
562
  this.accumulatedContent = ''
563
+ this.accumulatedThinking = []
564
+ this.currentThinkingContent = ''
565
+ this.currentThinkingSignature = ''
559
566
  this.finishedEvent = null
560
567
 
561
568
  // Update mutable context fields
@@ -666,6 +673,9 @@ class TextEngine<
666
673
  case 'RUN_ERROR':
667
674
  this.handleRunErrorEvent(chunk)
668
675
  break
676
+ case 'STEP_STARTED':
677
+ this.handleStepStartedEvent()
678
+ break
669
679
  case 'STEP_FINISHED':
670
680
  this.handleStepFinishedEvent(chunk)
671
681
  break
@@ -683,7 +693,7 @@ class TextEngine<
683
693
  break
684
694
 
685
695
  default:
686
- // RUN_STARTED, TEXT_MESSAGE_START, TEXT_MESSAGE_END, STEP_STARTED,
696
+ // RUN_STARTED, TEXT_MESSAGE_START, TEXT_MESSAGE_END,
687
697
  // STATE_SNAPSHOT, STATE_DELTA, CUSTOM
688
698
  // - no special handling needed in chat activity
689
699
  break
@@ -726,10 +736,32 @@ class TextEngine<
726
736
  this.earlyTermination = true
727
737
  }
728
738
 
739
+ private finalizeCurrentThinkingStep(): void {
740
+ if (this.currentThinkingContent) {
741
+ this.accumulatedThinking.push({
742
+ content: this.currentThinkingContent,
743
+ ...(this.currentThinkingSignature && {
744
+ signature: this.currentThinkingSignature,
745
+ }),
746
+ })
747
+ this.currentThinkingContent = ''
748
+ this.currentThinkingSignature = ''
749
+ }
750
+ }
751
+
752
+ private handleStepStartedEvent(): void {
753
+ this.finalizeCurrentThinkingStep()
754
+ }
755
+
729
756
  private handleStepFinishedEvent(
730
- _chunk: Extract<StreamChunk, { type: 'STEP_FINISHED' }>,
757
+ chunk: Extract<StreamChunk, { type: 'STEP_FINISHED' }>,
731
758
  ): void {
732
- // State tracking for STEP_FINISHED is handled by middleware
759
+ if (chunk.delta) {
760
+ this.currentThinkingContent += chunk.delta
761
+ }
762
+ if (chunk.signature) {
763
+ this.currentThinkingSignature = chunk.signature
764
+ }
733
765
  }
734
766
 
735
767
  private async *checkForPendingToolCalls(): AsyncGenerator<
@@ -1057,12 +1089,17 @@ class TextEngine<
1057
1089
  }
1058
1090
 
1059
1091
  private addAssistantToolCallMessage(toolCalls: Array<ToolCall>): void {
1092
+ this.finalizeCurrentThinkingStep()
1093
+
1060
1094
  this.messages = [
1061
1095
  ...this.messages,
1062
1096
  {
1063
1097
  role: 'assistant',
1064
1098
  content: this.accumulatedContent || null,
1065
1099
  toolCalls,
1100
+ ...(this.accumulatedThinking.length > 0 && {
1101
+ thinking: this.accumulatedThinking,
1102
+ }),
1066
1103
  },
1067
1104
  ]
1068
1105
  }
@@ -1213,6 +1250,7 @@ class TextEngine<
1213
1250
  timestamp: Date.now(),
1214
1251
  model: finishEvent.model,
1215
1252
  toolCallId: result.toolCallId,
1253
+ toolCallName: result.toolName,
1216
1254
  toolName: result.toolName,
1217
1255
  } as StreamChunk)
1218
1256
 
@@ -1248,14 +1286,39 @@ class TextEngine<
1248
1286
  role: 'tool',
1249
1287
  } as StreamChunk)
1250
1288
 
1251
- this.messages = [
1252
- ...this.messages,
1253
- {
1254
- role: 'tool',
1255
- content,
1256
- toolCallId: result.toolCallId,
1257
- },
1258
- ]
1289
+ // If a placeholder tool message exists for this toolCallId (created by
1290
+ // uiMessageToModelMessages for an approval-responded part with no
1291
+ // output yet), replace it with the real result. Otherwise the LLM sees
1292
+ // both messages — and since the Anthropic adapter dedupes tool_result
1293
+ // blocks by tool_use_id keeping the first match, the placeholder wins
1294
+ // and the real result is dropped (see issue #532).
1295
+ const placeholderIdx = this.messages.findIndex((m) => {
1296
+ if (m.role !== 'tool' || m.toolCallId !== result.toolCallId) {
1297
+ return false
1298
+ }
1299
+ if (typeof m.content !== 'string') return false
1300
+ try {
1301
+ return JSON.parse(m.content)?.pendingExecution === true
1302
+ } catch {
1303
+ return false
1304
+ }
1305
+ })
1306
+
1307
+ const newToolMessage: ModelMessage = {
1308
+ role: 'tool',
1309
+ content,
1310
+ toolCallId: result.toolCallId,
1311
+ }
1312
+
1313
+ if (placeholderIdx >= 0) {
1314
+ this.messages = [
1315
+ ...this.messages.slice(0, placeholderIdx),
1316
+ newToolMessage,
1317
+ ...this.messages.slice(placeholderIdx + 1),
1318
+ ]
1319
+ } else {
1320
+ this.messages = [...this.messages, newToolMessage]
1321
+ }
1259
1322
  }
1260
1323
 
1261
1324
  return chunks
@@ -138,6 +138,10 @@ interface AssistantSegment {
138
138
  id: string
139
139
  type: 'function'
140
140
  function: { name: string; arguments: string }
141
+ /** Provider-specific metadata that round-trips with the tool call.
142
+ * Untyped at this framework layer; adapters narrow it via their
143
+ * `TToolCallMetadata` generic. */
144
+ metadata?: unknown
141
145
  }>
142
146
  }
143
147
 
@@ -165,6 +169,7 @@ function isToolCallIncluded(part: ToolCallPart): boolean {
165
169
  function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
166
170
  const messageList: Array<ModelMessage> = []
167
171
  let current = createSegment()
172
+ let pendingThinking: Array<{ content: string; signature?: string }> = []
168
173
 
169
174
  // Track emitted tool result IDs to avoid duplicates.
170
175
  // A tool call can have BOTH an explicit tool-result part AND an output
@@ -181,7 +186,9 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
181
186
  role: 'assistant',
182
187
  content,
183
188
  ...(hasToolCalls && { toolCalls: current.toolCalls }),
189
+ ...(pendingThinking.length > 0 && { thinking: pendingThinking }),
184
190
  })
191
+ pendingThinking = []
185
192
  }
186
193
  current = createSegment()
187
194
  }
@@ -205,6 +212,7 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
205
212
  name: part.name,
206
213
  arguments: part.arguments,
207
214
  },
215
+ ...(part.metadata !== undefined && { metadata: part.metadata }),
208
216
  })
209
217
  }
210
218
  break
@@ -227,7 +235,15 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
227
235
  }
228
236
  break
229
237
 
230
- // thinking parts are skipped - they're UI-only
238
+ case 'thinking':
239
+ if (part.content) {
240
+ pendingThinking.push({
241
+ content: part.content,
242
+ ...(part.signature && { signature: part.signature }),
243
+ })
244
+ }
245
+ break
246
+
231
247
  default:
232
248
  break
233
249
  }
@@ -306,6 +322,17 @@ export function modelMessageToUIMessage(
306
322
  ): UIMessage {
307
323
  const parts: Array<MessagePart> = []
308
324
 
325
+ if (modelMessage.role === 'assistant' && modelMessage.thinking?.length) {
326
+ for (const thinking of modelMessage.thinking) {
327
+ if (!thinking.content) continue
328
+ parts.push({
329
+ type: 'thinking',
330
+ content: thinking.content,
331
+ ...(thinking.signature && { signature: thinking.signature }),
332
+ })
333
+ }
334
+ }
335
+
309
336
  // Handle tool results (when role is "tool") - only produce tool-result part,
310
337
  // not a text part (the content IS the tool result, not display text)
311
338
  if (modelMessage.role === 'tool' && modelMessage.toolCallId) {
@@ -340,6 +367,7 @@ export function modelMessageToUIMessage(
340
367
  name: toolCall.function.name,
341
368
  arguments: toolCall.function.arguments,
342
369
  state: 'input-complete', // Model messages have complete arguments
370
+ ...(toolCall.metadata !== undefined && { metadata: toolCall.metadata }),
343
371
  })
344
372
  }
345
373
  }
@@ -55,6 +55,7 @@ export function updateToolCallPart(
55
55
  name: string
56
56
  arguments: string
57
57
  state: ToolCallState
58
+ metadata?: Record<string, unknown>
58
59
  },
59
60
  ): Array<UIMessage> {
60
61
  return messages.map((msg) => {
@@ -67,6 +68,12 @@ export function updateToolCallPart(
67
68
  (p): p is ToolCallPart => p.type === 'tool-call' && p.id === toolCall.id,
68
69
  )
69
70
 
71
+ // Carry forward metadata from either the new toolCall or the existing
72
+ // part. Once the adapter has emitted metadata for a tool call (e.g.
73
+ // Gemini's thoughtSignature on TOOL_CALL_START) we must not lose it on
74
+ // subsequent updates that don't re-supply it.
75
+ const metadata = toolCall.metadata ?? existing?.metadata
76
+
70
77
  const toolCallPart: ToolCallPart = {
71
78
  type: 'tool-call',
72
79
  id: toolCall.id,
@@ -76,6 +83,7 @@ export function updateToolCallPart(
76
83
  // Carry forward approval and output from the existing part
77
84
  ...(existing?.approval && { approval: { ...existing.approval } }),
78
85
  ...(existing?.output !== undefined && { output: existing.output }),
86
+ ...(metadata !== undefined && { metadata }),
79
87
  }
80
88
 
81
89
  if (existing) {
@@ -244,12 +252,15 @@ export function updateToolCallApprovalResponse(
244
252
  }
245
253
 
246
254
  /**
247
- * Update or add a thinking part to a message.
255
+ * Update or add a thinking part to a message, keyed by stepId.
256
+ * Each distinct stepId produces its own ThinkingPart.
248
257
  */
249
258
  export function updateThinkingPart(
250
259
  messages: Array<UIMessage>,
251
260
  messageId: string,
261
+ stepId: string,
252
262
  content: string,
263
+ signature?: string,
253
264
  ): Array<UIMessage> {
254
265
  return messages.map((msg) => {
255
266
  if (msg.id !== messageId) {
@@ -257,15 +268,19 @@ export function updateThinkingPart(
257
268
  }
258
269
 
259
270
  const parts = [...msg.parts]
260
- const thinkingPartIndex = parts.findIndex((p) => p.type === 'thinking')
271
+ const thinkingPartIndex = parts.findIndex(
272
+ (p) => p.type === 'thinking' && p.stepId === stepId,
273
+ )
261
274
 
262
275
  const thinkingPart: ThinkingPart = {
263
276
  type: 'thinking',
264
277
  content,
278
+ stepId,
279
+ ...(signature && { signature }),
265
280
  }
266
281
 
267
282
  if (thinkingPartIndex >= 0) {
268
- // Update existing thinking part
283
+ // Update existing thinking part for this step
269
284
  parts[thinkingPartIndex] = thinkingPart
270
285
  } else {
271
286
  // Add new thinking part at the end (preserve natural streaming order)