@tanstack/ai 0.15.0 → 0.17.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 (54) hide show
  1. package/dist/esm/activities/chat/adapter.d.ts +20 -3
  2. package/dist/esm/activities/chat/adapter.js.map +1 -1
  3. package/dist/esm/activities/chat/index.d.ts +16 -6
  4. package/dist/esm/activities/chat/index.js +235 -9
  5. package/dist/esm/activities/chat/index.js.map +1 -1
  6. package/dist/esm/activities/chat/messages.js +4 -2
  7. package/dist/esm/activities/chat/messages.js.map +1 -1
  8. package/dist/esm/activities/chat/stream/message-updaters.d.ts +1 -0
  9. package/dist/esm/activities/chat/stream/message-updaters.js +3 -1
  10. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  11. package/dist/esm/activities/chat/stream/processor.js +12 -4
  12. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  13. package/dist/esm/activities/chat/stream/types.d.ts +5 -0
  14. package/dist/esm/activities/chat/tools/tool-calls.js +1 -3
  15. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  16. package/dist/esm/activities/error-payload.d.ts +0 -8
  17. package/dist/esm/activities/error-payload.js +20 -2
  18. package/dist/esm/activities/error-payload.js.map +1 -1
  19. package/dist/esm/activities/generateImage/adapter.d.ts +2 -2
  20. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  21. package/dist/esm/activities/generateVideo/adapter.d.ts +2 -2
  22. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  23. package/dist/esm/activities/index.d.ts +1 -0
  24. package/dist/esm/activities/index.js +2 -0
  25. package/dist/esm/activities/index.js.map +1 -1
  26. package/dist/esm/activities/stream-generation-result.js +0 -2
  27. package/dist/esm/activities/stream-generation-result.js.map +1 -1
  28. package/dist/esm/activities/summarize/adapter.d.ts +4 -4
  29. package/dist/esm/activities/summarize/adapter.js.map +1 -1
  30. package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +45 -0
  31. package/dist/esm/activities/summarize/chat-stream-summarize.js +148 -0
  32. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -0
  33. package/dist/esm/activities/summarize/index.d.ts +1 -0
  34. package/dist/esm/activities/summarize/index.js +4 -2
  35. package/dist/esm/activities/summarize/index.js.map +1 -1
  36. package/dist/esm/types.d.ts +109 -10
  37. package/package.json +2 -2
  38. package/skills/ai-core/structured-outputs/SKILL.md +92 -1
  39. package/src/activities/chat/adapter.ts +25 -2
  40. package/src/activities/chat/index.ts +368 -26
  41. package/src/activities/chat/messages.ts +6 -0
  42. package/src/activities/chat/stream/message-updaters.ts +8 -0
  43. package/src/activities/chat/stream/processor.ts +12 -0
  44. package/src/activities/chat/stream/types.ts +5 -0
  45. package/src/activities/chat/tools/tool-calls.ts +1 -3
  46. package/src/activities/error-payload.ts +31 -2
  47. package/src/activities/generateImage/adapter.ts +8 -2
  48. package/src/activities/generateVideo/adapter.ts +8 -2
  49. package/src/activities/index.ts +5 -0
  50. package/src/activities/stream-generation-result.ts +4 -6
  51. package/src/activities/summarize/adapter.ts +8 -4
  52. package/src/activities/summarize/chat-stream-summarize.ts +238 -0
  53. package/src/activities/summarize/index.ts +12 -9
  54. package/src/types.ts +122 -10
@@ -9,6 +9,7 @@ import { devtoolsMiddleware } from '@tanstack/ai-event-client'
9
9
  import { stripToSpecMiddleware } from '../../strip-to-spec-middleware'
10
10
  import { streamToText } from '../../stream-to-response.js'
11
11
  import { resolveDebugOption } from '../../logger/resolve'
12
+ import { EventType } from '../../types'
12
13
  import { LazyToolManager } from './tools/lazy-tool-manager'
13
14
  import {
14
15
  MiddlewareAbortError,
@@ -28,7 +29,7 @@ import type {
28
29
  ClientToolRequest,
29
30
  ToolResult,
30
31
  } from './tools/tool-calls'
31
- import type { AnyTextAdapter } from './adapter'
32
+ import type { AnyTextAdapter, StructuredOutputOptions } from './adapter'
32
33
  import type {
33
34
  AgentLoopStrategy,
34
35
  ConstrainedModelMessage,
@@ -38,6 +39,8 @@ import type {
38
39
  RunFinishedEvent,
39
40
  SchemaInput,
40
41
  StreamChunk,
42
+ StructuredOutputCompleteEvent,
43
+ StructuredOutputStream,
41
44
  TextMessageContentEvent,
42
45
  TextOptions,
43
46
  Tool,
@@ -226,16 +229,28 @@ export function createChatOptions<
226
229
 
227
230
  /**
228
231
  * Result type for the text activity.
229
- * - If outputSchema is provided: Promise<InferSchemaType<TSchema>>
230
- * - If stream is false: Promise<string>
231
- * - Otherwise (stream is true, default): AsyncIterable<StreamChunk>
232
+ * - If outputSchema is provided AND stream is explicitly true:
233
+ * StructuredOutputStream<InferSchemaType<TSchema>> — yields raw JSON deltas
234
+ * via TEXT_MESSAGE_CONTENT plus a terminal StructuredOutputCompleteEvent
235
+ * carrying the validated object.
236
+ * - If outputSchema is provided without explicit stream:true:
237
+ * Promise<InferSchemaType<TSchema>>.
238
+ * - If stream is explicitly false (no schema): Promise<string>.
239
+ * - Otherwise (default): AsyncIterable<StreamChunk>.
240
+ *
241
+ * `[TStream] extends [true]` is used (not `TStream extends true`) so that the
242
+ * default `boolean` value of `TStream` does *not* match the streaming branch.
243
+ * Without this, plain `chat({ outputSchema })` would type as a stream while
244
+ * the runtime returns a Promise — see issue #526.
232
245
  */
233
246
  export type TextActivityResult<
234
247
  TSchema extends SchemaInput | undefined,
235
- TStream extends boolean = true,
248
+ TStream extends boolean = boolean,
236
249
  > = TSchema extends SchemaInput
237
- ? Promise<InferSchemaType<TSchema>>
238
- : TStream extends false
250
+ ? [TStream] extends [true]
251
+ ? StructuredOutputStream<InferSchemaType<TSchema>>
252
+ : Promise<InferSchemaType<TSchema>>
253
+ : [TStream] extends [false]
239
254
  ? Promise<string>
240
255
  : AsyncIterable<StreamChunk>
241
256
 
@@ -1575,38 +1590,46 @@ class TextEngine<
1575
1590
  export function chat<
1576
1591
  TAdapter extends AnyTextAdapter,
1577
1592
  TSchema extends SchemaInput | undefined = undefined,
1578
- TStream extends boolean = true,
1593
+ TStream extends boolean = boolean,
1579
1594
  >(
1580
1595
  options: TextActivityOptions<TAdapter, TSchema, TStream>,
1581
1596
  ): TextActivityResult<TSchema, TStream> {
1582
1597
  const { outputSchema, stream } = options
1583
1598
 
1584
- // If outputSchema is provided, run agentic structured output
1599
+ // outputSchema + stream:true is the only branch that streams structured
1600
+ // output. Without an explicit `stream: true`, schema-bearing calls run the
1601
+ // agent loop and resolve to a typed Promise<InferSchemaType<TSchema>>.
1602
+ if (outputSchema && stream === true) {
1603
+ return runStreamingStructuredOutput({
1604
+ ...options,
1605
+ outputSchema,
1606
+ stream,
1607
+ }) as TextActivityResult<TSchema, TStream>
1608
+ }
1609
+
1610
+ // If outputSchema is provided, run agentic structured output (Promise<T>)
1585
1611
  if (outputSchema) {
1586
- return runAgenticStructuredOutput(
1587
- options as unknown as TextActivityOptions<
1588
- AnyTextAdapter,
1589
- SchemaInput,
1590
- boolean
1591
- >,
1592
- ) as TextActivityResult<TSchema, TStream>
1612
+ return runAgenticStructuredOutput({
1613
+ ...options,
1614
+ outputSchema,
1615
+ }) as TextActivityResult<TSchema, TStream>
1593
1616
  }
1594
1617
 
1595
1618
  // If stream is explicitly false, run non-streaming text
1596
1619
  if (stream === false) {
1597
- return runNonStreamingText(
1598
- options as unknown as TextActivityOptions<
1599
- AnyTextAdapter,
1600
- undefined,
1601
- false
1602
- >,
1603
- ) as TextActivityResult<TSchema, TStream>
1620
+ return runNonStreamingText({
1621
+ ...options,
1622
+ outputSchema: undefined,
1623
+ stream,
1624
+ }) as TextActivityResult<TSchema, TStream>
1604
1625
  }
1605
1626
 
1606
1627
  // Otherwise, run streaming text (default)
1607
- return runStreamingText(
1608
- options as unknown as TextActivityOptions<AnyTextAdapter, undefined, true>,
1609
- ) as TextActivityResult<TSchema, TStream>
1628
+ return runStreamingText({
1629
+ ...options,
1630
+ outputSchema: undefined,
1631
+ stream,
1632
+ }) as TextActivityResult<TSchema, TStream>
1610
1633
  }
1611
1634
 
1612
1635
  /**
@@ -1741,6 +1764,325 @@ async function runAgenticStructuredOutput<TSchema extends SchemaInput>(
1741
1764
  return result.data as InferSchemaType<TSchema>
1742
1765
  }
1743
1766
 
1767
+ /**
1768
+ * Synthesize a streaming structured-output stream by wrapping a non-streaming
1769
+ * `structuredOutput` call. Used when an adapter doesn't implement
1770
+ * `structuredOutputStream` natively.
1771
+ */
1772
+ async function* fallbackStructuredOutputStream(
1773
+ adapter: AnyTextAdapter,
1774
+ options: StructuredOutputOptions<Record<string, unknown>>,
1775
+ ): AsyncIterable<StreamChunk> {
1776
+ const { chatOptions } = options
1777
+ const runId = chatOptions.runId ?? `mock-${Date.now()}`
1778
+ const threadId = chatOptions.threadId ?? `mock-${Date.now()}`
1779
+ const messageId = `mock-${Date.now()}-${Math.random().toString(36).slice(2)}`
1780
+ const model = chatOptions.model
1781
+ const timestamp = Date.now()
1782
+
1783
+ yield {
1784
+ type: EventType.RUN_STARTED,
1785
+ runId,
1786
+ threadId,
1787
+ model,
1788
+ timestamp,
1789
+ }
1790
+
1791
+ let result: { data: unknown; rawText: string }
1792
+ try {
1793
+ result = await adapter.structuredOutput(options)
1794
+ } catch (error) {
1795
+ const message = error instanceof Error ? error.message : 'Unknown error'
1796
+ yield {
1797
+ type: EventType.RUN_ERROR,
1798
+ runId,
1799
+ model,
1800
+ timestamp,
1801
+ message,
1802
+ error: { message },
1803
+ }
1804
+ return
1805
+ }
1806
+
1807
+ yield {
1808
+ type: EventType.TEXT_MESSAGE_START,
1809
+ messageId,
1810
+ role: 'assistant',
1811
+ model,
1812
+ timestamp,
1813
+ }
1814
+
1815
+ yield {
1816
+ type: EventType.TEXT_MESSAGE_CONTENT,
1817
+ messageId,
1818
+ delta: result.rawText,
1819
+ model,
1820
+ timestamp,
1821
+ }
1822
+
1823
+ yield {
1824
+ type: EventType.TEXT_MESSAGE_END,
1825
+ messageId,
1826
+ model,
1827
+ timestamp,
1828
+ }
1829
+
1830
+ yield {
1831
+ type: EventType.CUSTOM,
1832
+ name: 'structured-output.complete',
1833
+ value: { object: result.data, raw: result.rawText },
1834
+ model,
1835
+ timestamp,
1836
+ }
1837
+
1838
+ yield {
1839
+ type: EventType.RUN_FINISHED,
1840
+ runId,
1841
+ threadId,
1842
+ model,
1843
+ timestamp,
1844
+ finishReason: 'stop',
1845
+ }
1846
+ }
1847
+
1848
+ /**
1849
+ * Run streaming structured output:
1850
+ * - Without tools: call adapter.structuredOutputStream directly (single
1851
+ * provider request emitting JSON deltas + a final CUSTOM event).
1852
+ * - With tools: run the agent loop, yield its non-terminal chunks, then call
1853
+ * structuredOutputStream on the final messages so the structured stream's
1854
+ * own RUN_STARTED/RUN_FINISHED bracket the run.
1855
+ *
1856
+ * Validates the parsed object against the original Standard Schema (if
1857
+ * applicable) when forwarding the final `structured-output.complete` event.
1858
+ *
1859
+ * Pre-flight validation (missing schema, unconvertible schema) throws
1860
+ * synchronously at call time rather than as a yielded RUN_ERROR mid-stream —
1861
+ * those are programmer errors, not runtime conditions.
1862
+ */
1863
+ function runStreamingStructuredOutput<TSchema extends SchemaInput>(
1864
+ options: TextActivityOptions<AnyTextAdapter, TSchema, true>,
1865
+ ): StructuredOutputStream<InferSchemaType<TSchema>> {
1866
+ const { outputSchema } = options
1867
+
1868
+ if (!outputSchema) {
1869
+ throw new Error('outputSchema is required for streaming structured output')
1870
+ }
1871
+
1872
+ // forStructuredOutput strict-converts the schema once at the activity
1873
+ // boundary. Adapters can re-convert if their wire format diverges, but the
1874
+ // default flow hands them a strict-ready schema.
1875
+ const jsonSchema = convertSchemaToJsonSchema(outputSchema, {
1876
+ forStructuredOutput: true,
1877
+ })
1878
+ if (!jsonSchema) {
1879
+ throw new Error('Failed to convert output schema to JSON Schema')
1880
+ }
1881
+
1882
+ // The implementation generator yields the broader internal type
1883
+ // (`StreamChunk | StructuredOutputCompleteEvent<T>`) so agent-loop
1884
+ // CustomEvents can flow through; the public-facing type narrows to
1885
+ // `Exclude<StreamChunk, CustomEvent> | StructuredOutputCompleteEvent<T>`
1886
+ // which lets consumers narrow `chunk.value` cleanly. The widen→narrow
1887
+ // is contained here so consumers see only the strict type.
1888
+ return runStreamingStructuredOutputImpl(
1889
+ options,
1890
+ jsonSchema,
1891
+ ) as StructuredOutputStream<InferSchemaType<TSchema>>
1892
+ }
1893
+
1894
+ /**
1895
+ * Internal generator return type — broader than the public
1896
+ * `StructuredOutputStream<T>`. The public type pins three tagged `CUSTOM`
1897
+ * events (`structured-output.complete`, `approval-requested`,
1898
+ * `tool-input-available`) so consumers can narrow `chunk.value` cleanly by
1899
+ * literal `name`. At runtime, tools can also emit arbitrary user-defined
1900
+ * `CustomEvent`s through the `emitCustomEvent` context API; those flow
1901
+ * through this generator with `name: string` and are widened out at the
1902
+ * public boundary because keeping them would collapse the typed narrow back
1903
+ * to `any`. The cast inside `runStreamingStructuredOutput` is where that
1904
+ * widening happens.
1905
+ */
1906
+ type StructuredOutputStreamInternal<T> = AsyncIterable<
1907
+ StreamChunk | StructuredOutputCompleteEvent<T>
1908
+ >
1909
+
1910
+ async function* runStreamingStructuredOutputImpl<TSchema extends SchemaInput>(
1911
+ options: TextActivityOptions<AnyTextAdapter, TSchema, true>,
1912
+ jsonSchema: NonNullable<ReturnType<typeof convertSchemaToJsonSchema>>,
1913
+ ): StructuredOutputStreamInternal<InferSchemaType<TSchema>> {
1914
+ const { adapter, outputSchema, middleware, context, debug, ...textOptions } =
1915
+ options
1916
+ const model = adapter.model
1917
+ const logger = resolveDebugOption(debug)
1918
+ const runId = textOptions.runId
1919
+
1920
+ // Inputs may be UIMessages (from useChat) or ModelMessages (from server-side
1921
+ // callers). The agent-loop branch converts via TextEngine; the no-tools
1922
+ // branch must convert here so the adapter sees a uniform ModelMessage shape.
1923
+ let finalMessages = convertMessagesToModelMessages(textOptions.messages ?? [])
1924
+
1925
+ if (textOptions.tools?.length) {
1926
+ const engine = new TextEngine(
1927
+ {
1928
+ adapter,
1929
+ params: { ...textOptions, model, logger, messages: finalMessages },
1930
+ middleware,
1931
+ context,
1932
+ },
1933
+ logger,
1934
+ )
1935
+
1936
+ // The structured-output stream emits its own RUN_STARTED + RUN_FINISHED
1937
+ // pair to bracket the run — drop both from the engine's output so
1938
+ // consumers see exactly one terminal lifecycle pair.
1939
+ let agentLoopErrored = false
1940
+ try {
1941
+ for await (const chunk of engine.run()) {
1942
+ if (chunk.type === 'RUN_STARTED' || chunk.type === 'RUN_FINISHED') {
1943
+ continue
1944
+ }
1945
+ if (chunk.type === 'RUN_ERROR') {
1946
+ // The engine yielded RUN_ERROR without throwing (provider error mid
1947
+ // agent loop). Forward it once and short-circuit before invoking
1948
+ // structuredOutputStream — otherwise consumers would see a confusing
1949
+ // RUN_ERROR → RUN_STARTED → structured-output.complete sequence and
1950
+ // we would bill another provider call after a failed run.
1951
+ agentLoopErrored = true
1952
+ yield chunk
1953
+ continue
1954
+ }
1955
+ yield chunk
1956
+ }
1957
+ } catch (engineError) {
1958
+ const message = (engineError as Error).message || 'Agent loop failed'
1959
+ logger.errors('runStreamingStructuredOutput agent loop failed', {
1960
+ error: engineError,
1961
+ source: 'runStreamingStructuredOutput',
1962
+ })
1963
+ yield {
1964
+ type: EventType.RUN_ERROR,
1965
+ runId,
1966
+ model,
1967
+ timestamp: Date.now(),
1968
+ message,
1969
+ code: 'agent-loop-failed',
1970
+ error: { message, code: 'agent-loop-failed' },
1971
+ }
1972
+ return
1973
+ }
1974
+
1975
+ if (agentLoopErrored) {
1976
+ return
1977
+ }
1978
+
1979
+ finalMessages = engine.getMessages()
1980
+ }
1981
+
1982
+ const {
1983
+ tools: _tools,
1984
+ agentLoopStrategy: _als,
1985
+ ...structuredTextOptions
1986
+ } = textOptions
1987
+
1988
+ logger.request(
1989
+ `activity=chat-structured-stream provider=${adapter.name} model=${model} messages=${finalMessages.length}`,
1990
+ {
1991
+ provider: adapter.name,
1992
+ model,
1993
+ messageCount: finalMessages.length,
1994
+ },
1995
+ )
1996
+
1997
+ // Adapters consume the abort signal via `chatOptions.request?.signal` and
1998
+ // pass it to the underlying network call. Without this, aborting the SSE
1999
+ // response never cancels the upstream provider request and a terminal
2000
+ // structured-output.complete event still gets yielded after stop.
2001
+ const structuredChatOptions = {
2002
+ ...structuredTextOptions,
2003
+ model,
2004
+ messages: finalMessages,
2005
+ logger,
2006
+ request: textOptions.abortController
2007
+ ? { signal: textOptions.abortController.signal }
2008
+ : undefined,
2009
+ }
2010
+
2011
+ // Adapters that don't implement structuredOutputStream natively fall back
2012
+ // to wrapping the non-streaming `structuredOutput` — `fallbackStructuredOutputStream`
2013
+ // synthesizes the AG-UI lifecycle events around it.
2014
+ const stream = adapter.structuredOutputStream
2015
+ ? adapter.structuredOutputStream({
2016
+ chatOptions: structuredChatOptions,
2017
+ outputSchema: jsonSchema,
2018
+ })
2019
+ : fallbackStructuredOutputStream(adapter, {
2020
+ chatOptions: structuredChatOptions,
2021
+ outputSchema: jsonSchema,
2022
+ })
2023
+
2024
+ for await (const chunk of stream) {
2025
+ if (
2026
+ chunk.type === EventType.CUSTOM &&
2027
+ chunk.name === 'structured-output.complete'
2028
+ ) {
2029
+ const value = chunk.value as {
2030
+ object: unknown
2031
+ raw: string
2032
+ reasoning?: string
2033
+ }
2034
+ if (isStandardSchema(outputSchema)) {
2035
+ try {
2036
+ const validated = parseWithStandardSchema<InferSchemaType<TSchema>>(
2037
+ outputSchema,
2038
+ value.object,
2039
+ )
2040
+ yield {
2041
+ ...chunk,
2042
+ // Forward `reasoning` through schema validation so consumers that
2043
+ // only listen for the terminal event don't lose chain-of-thought.
2044
+ value: {
2045
+ object: validated,
2046
+ raw: value.raw,
2047
+ ...(value.reasoning ? { reasoning: value.reasoning } : {}),
2048
+ },
2049
+ }
2050
+ continue
2051
+ } catch (err) {
2052
+ const message = (err as Error).message || 'Schema validation failed'
2053
+ logger.errors(
2054
+ 'runStreamingStructuredOutput schema validation failed',
2055
+ {
2056
+ error: err,
2057
+ source: 'runStreamingStructuredOutput',
2058
+ // Include reasoning in error meta so post-mortems can recover
2059
+ // what the model thought through before producing invalid JSON.
2060
+ ...(value.reasoning ? { reasoning: value.reasoning } : {}),
2061
+ },
2062
+ )
2063
+ yield {
2064
+ type: EventType.RUN_ERROR,
2065
+ runId,
2066
+ model: chunk.model ?? model,
2067
+ timestamp: chunk.timestamp ?? Date.now(),
2068
+ message,
2069
+ code: 'schema-validation',
2070
+ error: {
2071
+ message,
2072
+ code: 'schema-validation',
2073
+ ...(value.reasoning ? { reasoning: value.reasoning } : {}),
2074
+ },
2075
+ }
2076
+ return
2077
+ }
2078
+ }
2079
+ yield chunk
2080
+ continue
2081
+ }
2082
+ yield chunk
2083
+ }
2084
+ }
2085
+
1744
2086
  // Re-export adapter types
1745
2087
  export type {
1746
2088
  TextAdapter,
@@ -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
 
@@ -208,6 +212,7 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
208
212
  name: part.name,
209
213
  arguments: part.arguments,
210
214
  },
215
+ ...(part.metadata !== undefined && { metadata: part.metadata }),
211
216
  })
212
217
  }
213
218
  break
@@ -362,6 +367,7 @@ export function modelMessageToUIMessage(
362
367
  name: toolCall.function.name,
363
368
  arguments: toolCall.function.arguments,
364
369
  state: 'input-complete', // Model messages have complete arguments
370
+ ...(toolCall.metadata !== undefined && { metadata: toolCall.metadata }),
365
371
  })
366
372
  }
367
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) {
@@ -936,6 +936,11 @@ export class StreamProcessor {
936
936
  const toolName =
937
937
  (chunk as { toolCallName?: string }).toolCallName ?? chunk.toolName
938
938
 
939
+ // Capture provider metadata that arrived on TOOL_CALL_START so it
940
+ // round-trips back through the assistant message on the next turn
941
+ // (e.g. Gemini's thoughtSignature).
942
+ const chunkMetadata = chunk.metadata
943
+
939
944
  const newToolCall: InternalToolCallState = {
940
945
  id: chunk.toolCallId,
941
946
  name: toolName,
@@ -943,6 +948,7 @@ export class StreamProcessor {
943
948
  state: initialState,
944
949
  parsedArguments: undefined,
945
950
  index: chunk.index ?? state.toolCalls.size,
951
+ ...(chunkMetadata !== undefined && { metadata: chunkMetadata }),
946
952
  }
947
953
 
948
954
  state.toolCalls.set(toolCallId, newToolCall)
@@ -957,6 +963,7 @@ export class StreamProcessor {
957
963
  name: toolName,
958
964
  arguments: '',
959
965
  state: initialState,
966
+ ...(chunkMetadata !== undefined && { metadata: chunkMetadata }),
960
967
  })
961
968
  this.emitMessagesChange()
962
969
 
@@ -1504,6 +1511,7 @@ export class StreamProcessor {
1504
1511
  name: toolCall.name,
1505
1512
  arguments: toolCall.arguments,
1506
1513
  state: 'input-complete',
1514
+ ...(toolCall.metadata !== undefined && { metadata: toolCall.metadata }),
1507
1515
  })
1508
1516
  this.emitMessagesChange()
1509
1517
 
@@ -1619,6 +1627,10 @@ export class StreamProcessor {
1619
1627
  name: tc.name,
1620
1628
  arguments: tc.arguments,
1621
1629
  },
1630
+ // Preserve provider metadata (e.g. Gemini thoughtSignature) on
1631
+ // ProcessorResult.toolCalls so callers using process()/getResult()
1632
+ // get the same round-trip support as the streaming UI path.
1633
+ ...(tc.metadata !== undefined && { metadata: tc.metadata }),
1622
1634
  })
1623
1635
  }
1624
1636
  }
@@ -25,6 +25,11 @@ export interface InternalToolCallState {
25
25
  state: ToolCallState
26
26
  parsedArguments?: any
27
27
  index: number
28
+ /** Provider-specific metadata that round-trips with the tool call
29
+ * (e.g. Gemini's `thoughtSignature`). Untyped at this layer because
30
+ * the stream processor is provider-agnostic; adapters narrow it
31
+ * via their `TToolCallMetadata` generic. */
32
+ metadata?: Record<string, unknown>
28
33
  }
29
34
 
30
35
  /**
@@ -103,9 +103,7 @@ export class ToolCallManager {
103
103
  name,
104
104
  arguments: '',
105
105
  },
106
- ...(event.providerMetadata && {
107
- providerMetadata: event.providerMetadata,
108
- }),
106
+ ...(event.metadata !== undefined && { metadata: event.metadata }),
109
107
  })
110
108
  }
111
109
 
@@ -5,16 +5,45 @@
5
5
  * Accepts Error instances, objects with string-ish `message`/`code`, or bare
6
6
  * strings; always returns a shape safe to serialize. Never leaks the full
7
7
  * error object (which may carry request/response state from an SDK).
8
+ *
9
+ * Abort-shaped errors (DOM `AbortError`, OpenAI `APIUserAbortError`,
10
+ * OpenRouter `RequestAbortedError`) are normalized to a stable
11
+ * `{ message: 'Request aborted', code: 'aborted' }` shape so callers can
12
+ * discriminate user-initiated cancellation from other failures without
13
+ * matching on provider-specific message strings.
8
14
  */
15
+ const ABORT_ERROR_NAMES = new Set([
16
+ 'AbortError',
17
+ 'APIUserAbortError',
18
+ 'RequestAbortedError',
19
+ ])
20
+
21
+ // HTTP status codes carried as numbers (e.g. `error.status = 429`) are a
22
+ // common variant on SDK error classes; coerce so the resulting `code` field
23
+ // is stable as a string for downstream consumers.
24
+ function normalizeCode(codeField: unknown): string | undefined {
25
+ if (typeof codeField === 'string') return codeField
26
+ if (typeof codeField === 'number' && Number.isFinite(codeField)) {
27
+ return String(codeField)
28
+ }
29
+ return undefined
30
+ }
31
+
9
32
  export function toRunErrorPayload(
10
33
  error: unknown,
11
34
  fallbackMessage = 'Unknown error occurred',
12
35
  ): { message: string; code: string | undefined } {
36
+ if (error && typeof error === 'object') {
37
+ const name = (error as { name?: unknown }).name
38
+ if (typeof name === 'string' && ABORT_ERROR_NAMES.has(name)) {
39
+ return { message: 'Request aborted', code: 'aborted' }
40
+ }
41
+ }
13
42
  if (error instanceof Error) {
14
43
  const codeField = (error as Error & { code?: unknown }).code
15
44
  return {
16
45
  message: error.message || fallbackMessage,
17
- code: typeof codeField === 'string' ? codeField : undefined,
46
+ code: normalizeCode(codeField),
18
47
  }
19
48
  }
20
49
  if (typeof error === 'object' && error !== null) {
@@ -25,7 +54,7 @@ export function toRunErrorPayload(
25
54
  typeof messageField === 'string' && messageField.length > 0
26
55
  ? messageField
27
56
  : fallbackMessage,
28
- code: typeof codeField === 'string' ? codeField : undefined,
57
+ code: normalizeCode(codeField),
29
58
  }
30
59
  }
31
60
  if (typeof error === 'string' && error.length > 0) {
@@ -34,7 +34,10 @@ export interface ImageAdapter<
34
34
  TModel extends string = string,
35
35
  TProviderOptions extends object = Record<string, unknown>,
36
36
  TModelProviderOptionsByName extends Record<string, any> = Record<string, any>,
37
- TModelSizeByName extends Record<string, string> = Record<string, string>,
37
+ TModelSizeByName extends Record<string, string | undefined> = Record<
38
+ string,
39
+ string
40
+ >,
38
41
  > {
39
42
  /** Discriminator for adapter kind - used by generate() to determine API shape */
40
43
  readonly kind: 'image'
@@ -76,7 +79,10 @@ export abstract class BaseImageAdapter<
76
79
  TModel extends string = string,
77
80
  TProviderOptions extends object = Record<string, unknown>,
78
81
  TModelProviderOptionsByName extends Record<string, any> = Record<string, any>,
79
- TModelSizeByName extends Record<string, string> = Record<string, string>,
82
+ TModelSizeByName extends Record<string, string | undefined> = Record<
83
+ string,
84
+ string
85
+ >,
80
86
  > implements ImageAdapter<
81
87
  TModel,
82
88
  TProviderOptions,
@@ -36,7 +36,10 @@ export interface VideoAdapter<
36
36
  TModel extends string = string,
37
37
  TProviderOptions extends object = Record<string, unknown>,
38
38
  TModelProviderOptionsByName extends Record<string, any> = Record<string, any>,
39
- TModelSizeByName extends Record<string, string> = Record<string, string>,
39
+ TModelSizeByName extends Record<string, string | undefined> = Record<
40
+ string,
41
+ string
42
+ >,
40
43
  > {
41
44
  /** Discriminator for adapter kind - used to determine API shape */
42
45
  readonly kind: 'video'
@@ -92,7 +95,10 @@ export abstract class BaseVideoAdapter<
92
95
  TModel extends string = string,
93
96
  TProviderOptions extends object = Record<string, unknown>,
94
97
  TModelProviderOptionsByName extends Record<string, any> = Record<string, any>,
95
- TModelSizeByName extends Record<string, string> = Record<string, string>,
98
+ TModelSizeByName extends Record<string, string | undefined> = Record<
99
+ string,
100
+ string
101
+ >,
96
102
  > implements VideoAdapter<
97
103
  TModel,
98
104
  TProviderOptions,