@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.
- package/dist/esm/activities/chat/adapter.d.ts +20 -3
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/index.d.ts +16 -6
- package/dist/esm/activities/chat/index.js +235 -9
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +4 -2
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.d.ts +1 -0
- package/dist/esm/activities/chat/stream/message-updaters.js +3 -1
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.js +12 -4
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/stream/types.d.ts +5 -0
- package/dist/esm/activities/chat/tools/tool-calls.js +1 -3
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/error-payload.d.ts +0 -8
- package/dist/esm/activities/error-payload.js +20 -2
- package/dist/esm/activities/error-payload.js.map +1 -1
- package/dist/esm/activities/generateImage/adapter.d.ts +2 -2
- package/dist/esm/activities/generateImage/adapter.js.map +1 -1
- package/dist/esm/activities/generateVideo/adapter.d.ts +2 -2
- package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
- package/dist/esm/activities/index.d.ts +1 -0
- package/dist/esm/activities/index.js +2 -0
- package/dist/esm/activities/index.js.map +1 -1
- package/dist/esm/activities/stream-generation-result.js +0 -2
- package/dist/esm/activities/stream-generation-result.js.map +1 -1
- package/dist/esm/activities/summarize/adapter.d.ts +4 -4
- package/dist/esm/activities/summarize/adapter.js.map +1 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +45 -0
- package/dist/esm/activities/summarize/chat-stream-summarize.js +148 -0
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -0
- package/dist/esm/activities/summarize/index.d.ts +1 -0
- package/dist/esm/activities/summarize/index.js +4 -2
- package/dist/esm/activities/summarize/index.js.map +1 -1
- package/dist/esm/types.d.ts +109 -10
- package/package.json +2 -2
- package/skills/ai-core/structured-outputs/SKILL.md +92 -1
- package/src/activities/chat/adapter.ts +25 -2
- package/src/activities/chat/index.ts +368 -26
- package/src/activities/chat/messages.ts +6 -0
- package/src/activities/chat/stream/message-updaters.ts +8 -0
- package/src/activities/chat/stream/processor.ts +12 -0
- package/src/activities/chat/stream/types.ts +5 -0
- package/src/activities/chat/tools/tool-calls.ts +1 -3
- package/src/activities/error-payload.ts +31 -2
- package/src/activities/generateImage/adapter.ts +8 -2
- package/src/activities/generateVideo/adapter.ts +8 -2
- package/src/activities/index.ts +5 -0
- package/src/activities/stream-generation-result.ts +4 -6
- package/src/activities/summarize/adapter.ts +8 -4
- package/src/activities/summarize/chat-stream-summarize.ts +238 -0
- package/src/activities/summarize/index.ts +12 -9
- 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:
|
|
230
|
-
*
|
|
231
|
-
*
|
|
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 =
|
|
248
|
+
TStream extends boolean = boolean,
|
|
236
249
|
> = TSchema extends SchemaInput
|
|
237
|
-
?
|
|
238
|
-
|
|
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 =
|
|
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
|
-
//
|
|
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
|
|
1588
|
-
|
|
1589
|
-
|
|
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
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
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
|
|
1609
|
-
|
|
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
|
/**
|
|
@@ -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:
|
|
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:
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
98
|
+
TModelSizeByName extends Record<string, string | undefined> = Record<
|
|
99
|
+
string,
|
|
100
|
+
string
|
|
101
|
+
>,
|
|
96
102
|
> implements VideoAdapter<
|
|
97
103
|
TModel,
|
|
98
104
|
TProviderOptions,
|