@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.
- package/dist/esm/activities/chat/adapter.d.ts +6 -3
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/index.js +61 -9
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +26 -3
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.d.ts +4 -2
- package/dist/esm/activities/chat/stream/message-updaters.js +10 -4
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +21 -5
- package/dist/esm/activities/chat/stream/processor.js +119 -19
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/stream/types.d.ts +9 -1
- package/dist/esm/activities/chat/tools/tool-calls.js +3 -4
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/middlewares/otel.d.ts +75 -0
- package/dist/esm/middlewares/otel.js +624 -0
- package/dist/esm/middlewares/otel.js.map +1 -0
- package/dist/esm/types.d.ts +23 -7
- package/package.json +15 -2
- package/src/activities/chat/adapter.ts +8 -2
- package/src/activities/chat/index.ts +74 -11
- package/src/activities/chat/messages.ts +29 -1
- package/src/activities/chat/stream/message-updaters.ts +18 -3
- package/src/activities/chat/stream/processor.ts +152 -17
- package/src/activities/chat/stream/types.ts +9 -1
- package/src/activities/chat/tools/tool-calls.ts +4 -4
- package/src/middlewares/index.ts +5 -0
- package/src/middlewares/otel.ts +861 -0
- 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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"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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?`, `
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
757
|
+
chunk: Extract<StreamChunk, { type: 'STEP_FINISHED' }>,
|
|
731
758
|
): void {
|
|
732
|
-
|
|
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
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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
|
-
|
|
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(
|
|
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)
|