@tanstack/ai 0.27.0 → 0.29.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/index.d.ts +7 -0
- package/dist/esm/activities/chat/index.js +86 -20
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/mcp/manager.d.ts +25 -0
- package/dist/esm/activities/chat/mcp/manager.js +71 -0
- package/dist/esm/activities/chat/mcp/manager.js.map +1 -0
- package/dist/esm/activities/chat/mcp/types.d.ts +56 -0
- package/dist/esm/activities/chat/messages.js +1 -1
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.js +20 -8
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +6 -0
- package/dist/esm/activities/chat/stream/processor.js +18 -3
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.js +2 -1
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.d.ts +2 -1
- package/dist/esm/activities/generateVideo/index.js +12 -2
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/extend-adapter.d.ts +22 -6
- package/dist/esm/extend-adapter.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/logger/console-logger.d.ts +18 -0
- package/dist/esm/logger/console-logger.js +64 -8
- package/dist/esm/logger/console-logger.js.map +1 -1
- package/dist/esm/logger/types.d.ts +4 -4
- package/dist/esm/realtime/index.d.ts +1 -3
- package/dist/esm/realtime/index.js.map +1 -1
- package/dist/esm/types.d.ts +13 -1
- package/package.json +2 -2
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +2 -2
- package/skills/ai-core/chat-experience/SKILL.md +71 -0
- package/skills/ai-core/media-generation/SKILL.md +29 -1
- package/skills/ai-core/tool-calling/SKILL.md +287 -0
- package/src/activities/chat/index.ts +109 -25
- package/src/activities/chat/mcp/manager.ts +85 -0
- package/src/activities/chat/mcp/types.ts +66 -0
- package/src/activities/chat/messages.ts +1 -0
- package/src/activities/chat/stream/message-updaters.ts +22 -9
- package/src/activities/chat/stream/processor.ts +30 -3
- package/src/activities/chat/tools/tool-calls.ts +2 -0
- package/src/activities/generateVideo/index.ts +12 -0
- package/src/extend-adapter.ts +42 -24
- package/src/index.ts +10 -0
- package/src/logger/console-logger.ts +112 -17
- package/src/logger/types.ts +4 -4
- package/src/realtime/index.ts +1 -3
- package/src/types.ts +13 -0
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { maxIterations as maxIterationsStrategy } from './agent-loop-strategies'
|
|
26
26
|
import { convertMessagesToModelMessages, generateMessageId } from './messages'
|
|
27
27
|
import { MiddlewareRunner } from './middleware/compose'
|
|
28
|
+
import { MCPManager } from './mcp/manager'
|
|
28
29
|
import type {
|
|
29
30
|
ApprovalRequest,
|
|
30
31
|
ClientToolRequest,
|
|
@@ -69,6 +70,7 @@ import type {
|
|
|
69
70
|
MergeContext,
|
|
70
71
|
UnionToIntersection,
|
|
71
72
|
} from './runtime-context-types'
|
|
73
|
+
import type { ChatMCPOptions } from './mcp/types'
|
|
72
74
|
|
|
73
75
|
// ===========================
|
|
74
76
|
// Activity Kind
|
|
@@ -208,6 +210,12 @@ export interface TextActivityOptions<
|
|
|
208
210
|
| ProviderTool<string, TAdapter['~types']['toolCapabilities'][number]>
|
|
209
211
|
>
|
|
210
212
|
| undefined
|
|
213
|
+
/**
|
|
214
|
+
* Hand MCP clients/pools to chat(): their tools are discovered at run start
|
|
215
|
+
* and merged into the run; `connection` controls whether chat() closes them
|
|
216
|
+
* when the run ends. See docs/tools/mcp.md "Managing MCP clients with chat()".
|
|
217
|
+
*/
|
|
218
|
+
mcp?: ChatMCPOptions
|
|
211
219
|
/** Additional metadata to attach to the request. */
|
|
212
220
|
metadata?: TextOptions['metadata']
|
|
213
221
|
/** Model-specific provider options (type comes from adapter) */
|
|
@@ -432,6 +440,28 @@ interface TextEngineConfig<
|
|
|
432
440
|
type ToolPhaseResult = 'continue' | 'stop' | 'wait'
|
|
433
441
|
type CyclePhase = 'processText' | 'executeToolCalls'
|
|
434
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Combine two optional AbortSignals into one that aborts when either does.
|
|
445
|
+
* Returns the other signal directly when one is absent or already aborted.
|
|
446
|
+
* (Manual implementation — `AbortSignal.any` requires Node >= 20.3.)
|
|
447
|
+
*/
|
|
448
|
+
function combineAbortSignals(
|
|
449
|
+
a: AbortSignal | undefined,
|
|
450
|
+
b: AbortSignal | undefined,
|
|
451
|
+
): AbortSignal | undefined {
|
|
452
|
+
if (!a) return b
|
|
453
|
+
if (!b) return a
|
|
454
|
+
if (a.aborted) return a
|
|
455
|
+
if (b.aborted) return b
|
|
456
|
+
const controller = new AbortController()
|
|
457
|
+
const onAbort = (source: AbortSignal) => () => {
|
|
458
|
+
controller.abort(source.reason)
|
|
459
|
+
}
|
|
460
|
+
a.addEventListener('abort', onAbort(a), { once: true })
|
|
461
|
+
b.addEventListener('abort', onAbort(b), { once: true })
|
|
462
|
+
return controller.signal
|
|
463
|
+
}
|
|
464
|
+
|
|
435
465
|
class TextEngine<
|
|
436
466
|
TAdapter extends AnyTextAdapter,
|
|
437
467
|
TContext = unknown,
|
|
@@ -486,6 +516,9 @@ class TextEngine<
|
|
|
486
516
|
private readonly deferredPromises: Array<Promise<unknown>> = []
|
|
487
517
|
private abortReason?: string
|
|
488
518
|
private readonly middlewareAbortController?: AbortController
|
|
519
|
+
// Combines the caller's signal with middleware abort() so running tools
|
|
520
|
+
// observe both cancellation sources via ctx.abortSignal.
|
|
521
|
+
private readonly toolAbortSignal?: AbortSignal
|
|
489
522
|
private terminalHookCalled = false
|
|
490
523
|
|
|
491
524
|
private readonly logger: InternalLogger
|
|
@@ -583,6 +616,10 @@ class TextEngine<
|
|
|
583
616
|
]
|
|
584
617
|
this.middlewareRunner = new MiddlewareRunner(allMiddleware, logger)
|
|
585
618
|
this.middlewareAbortController = new AbortController()
|
|
619
|
+
this.toolAbortSignal = combineAbortSignals(
|
|
620
|
+
this.effectiveSignal,
|
|
621
|
+
this.middlewareAbortController.signal,
|
|
622
|
+
)
|
|
586
623
|
this.middlewareCtx = {
|
|
587
624
|
requestId: this.requestId,
|
|
588
625
|
streamId: this.streamId,
|
|
@@ -1225,6 +1262,7 @@ class TextEngine<
|
|
|
1225
1262
|
},
|
|
1226
1263
|
},
|
|
1227
1264
|
this.middlewareCtx.context,
|
|
1265
|
+
this.toolAbortSignal,
|
|
1228
1266
|
)
|
|
1229
1267
|
|
|
1230
1268
|
// Consume the async generator, yielding custom events and collecting the return value
|
|
@@ -1386,6 +1424,7 @@ class TextEngine<
|
|
|
1386
1424
|
},
|
|
1387
1425
|
},
|
|
1388
1426
|
this.middlewareCtx.context,
|
|
1427
|
+
this.toolAbortSignal,
|
|
1389
1428
|
)
|
|
1390
1429
|
|
|
1391
1430
|
// Consume the async generator, yielding custom events and collecting the return value
|
|
@@ -1639,8 +1678,9 @@ class TextEngine<
|
|
|
1639
1678
|
const wireContent =
|
|
1640
1679
|
typeof content === 'string' ? content : JSON.stringify(content)
|
|
1641
1680
|
|
|
1642
|
-
//
|
|
1643
|
-
//
|
|
1681
|
+
// argsMap is set only on continuation re-executions, where the adapter
|
|
1682
|
+
// never streamed these calls. Otherwise it already emitted END, so a
|
|
1683
|
+
// second one here would be an orphan that fails verifyEvents (#519).
|
|
1644
1684
|
if (argsMap) {
|
|
1645
1685
|
chunks.push({
|
|
1646
1686
|
type: 'TOOL_CALL_START',
|
|
@@ -1660,18 +1700,18 @@ class TextEngine<
|
|
|
1660
1700
|
delta: args,
|
|
1661
1701
|
args,
|
|
1662
1702
|
} as StreamChunk)
|
|
1663
|
-
}
|
|
1664
1703
|
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1704
|
+
chunks.push({
|
|
1705
|
+
type: 'TOOL_CALL_END',
|
|
1706
|
+
timestamp: Date.now(),
|
|
1707
|
+
model: finishEvent.model,
|
|
1708
|
+
toolCallId: result.toolCallId,
|
|
1709
|
+
toolCallName: result.toolName,
|
|
1710
|
+
toolName: result.toolName,
|
|
1711
|
+
result: wireContent,
|
|
1712
|
+
...(result.state !== undefined && { state: result.state }),
|
|
1713
|
+
} as StreamChunk)
|
|
1714
|
+
}
|
|
1675
1715
|
|
|
1676
1716
|
// AG-UI spec TOOL_CALL_RESULT event (content is string-only per spec)
|
|
1677
1717
|
chunks.push({
|
|
@@ -2568,10 +2608,16 @@ export function chat<
|
|
|
2568
2608
|
async function* runStreamingText<TContext = unknown>(
|
|
2569
2609
|
options: TextActivityOptions<AnyTextAdapter, undefined, true, TContext>,
|
|
2570
2610
|
): AsyncIterable<StreamChunk> {
|
|
2571
|
-
const { adapter, middleware, context, debug, ...textOptions } = options
|
|
2611
|
+
const { adapter, middleware, context, debug, mcp, ...textOptions } = options
|
|
2572
2612
|
const model = adapter.model
|
|
2573
2613
|
const logger = resolveDebugOption(debug)
|
|
2574
2614
|
|
|
2615
|
+
const mcpManager = MCPManager.from(mcp)
|
|
2616
|
+
const mcpTools = await mcpManager.discover()
|
|
2617
|
+
if (mcpTools.length > 0) {
|
|
2618
|
+
textOptions.tools = [...(textOptions.tools ?? []), ...mcpTools]
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2575
2621
|
const engine = new TextEngine(
|
|
2576
2622
|
{
|
|
2577
2623
|
adapter,
|
|
@@ -2586,8 +2632,12 @@ async function* runStreamingText<TContext = unknown>(
|
|
|
2586
2632
|
logger,
|
|
2587
2633
|
)
|
|
2588
2634
|
|
|
2589
|
-
|
|
2590
|
-
|
|
2635
|
+
try {
|
|
2636
|
+
for await (const chunk of engine.run()) {
|
|
2637
|
+
yield chunk
|
|
2638
|
+
}
|
|
2639
|
+
} finally {
|
|
2640
|
+
await mcpManager.dispose()
|
|
2591
2641
|
}
|
|
2592
2642
|
}
|
|
2593
2643
|
|
|
@@ -2624,8 +2674,15 @@ async function runAgenticStructuredOutput<
|
|
|
2624
2674
|
>(
|
|
2625
2675
|
options: TextActivityOptions<AnyTextAdapter, TSchema, boolean, TContext>,
|
|
2626
2676
|
): Promise<InferSchemaType<TSchema>> {
|
|
2627
|
-
const {
|
|
2628
|
-
|
|
2677
|
+
const {
|
|
2678
|
+
adapter,
|
|
2679
|
+
outputSchema,
|
|
2680
|
+
middleware,
|
|
2681
|
+
context,
|
|
2682
|
+
debug,
|
|
2683
|
+
mcp,
|
|
2684
|
+
...textOptions
|
|
2685
|
+
} = options
|
|
2629
2686
|
const model = adapter.model
|
|
2630
2687
|
const logger = resolveDebugOption(debug)
|
|
2631
2688
|
|
|
@@ -2659,6 +2716,12 @@ async function runAgenticStructuredOutput<
|
|
|
2659
2716
|
const nativeCombined =
|
|
2660
2717
|
adapter.supportsCombinedToolsAndSchema?.(options.modelOptions) === true
|
|
2661
2718
|
|
|
2719
|
+
const mcpManager = MCPManager.from(mcp)
|
|
2720
|
+
const mcpTools = await mcpManager.discover()
|
|
2721
|
+
if (mcpTools.length > 0) {
|
|
2722
|
+
textOptions.tools = [...(textOptions.tools ?? []), ...mcpTools]
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2662
2725
|
const engine = new TextEngine(
|
|
2663
2726
|
{
|
|
2664
2727
|
adapter,
|
|
@@ -2679,9 +2742,13 @@ async function runAgenticStructuredOutput<
|
|
|
2679
2742
|
logger,
|
|
2680
2743
|
)
|
|
2681
2744
|
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2745
|
+
try {
|
|
2746
|
+
// Consume the stream — chunks pipe through middleware but are not yielded externally
|
|
2747
|
+
for await (const _chunk of engine.run()) {
|
|
2748
|
+
// intentionally empty
|
|
2749
|
+
}
|
|
2750
|
+
} finally {
|
|
2751
|
+
await mcpManager.dispose()
|
|
2685
2752
|
}
|
|
2686
2753
|
|
|
2687
2754
|
const finalizationError = engine.getFinalizationError()
|
|
@@ -2912,8 +2979,15 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
2912
2979
|
options: TextActivityOptions<AnyTextAdapter, TSchema, true, TContext>,
|
|
2913
2980
|
jsonSchema: NonNullable<ReturnType<typeof convertSchemaToJsonSchema>>,
|
|
2914
2981
|
): StructuredOutputStreamInternal<InferSchemaType<TSchema>> {
|
|
2915
|
-
const {
|
|
2916
|
-
|
|
2982
|
+
const {
|
|
2983
|
+
adapter,
|
|
2984
|
+
outputSchema,
|
|
2985
|
+
middleware,
|
|
2986
|
+
context,
|
|
2987
|
+
debug,
|
|
2988
|
+
mcp,
|
|
2989
|
+
...textOptions
|
|
2990
|
+
} = options
|
|
2917
2991
|
const model = adapter.model
|
|
2918
2992
|
const logger = resolveDebugOption(debug)
|
|
2919
2993
|
|
|
@@ -2927,6 +3001,12 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
2927
3001
|
const nativeCombined =
|
|
2928
3002
|
adapter.supportsCombinedToolsAndSchema?.(options.modelOptions) === true
|
|
2929
3003
|
|
|
3004
|
+
const mcpManager = MCPManager.from(mcp)
|
|
3005
|
+
const mcpTools = await mcpManager.discover()
|
|
3006
|
+
if (mcpTools.length > 0) {
|
|
3007
|
+
textOptions.tools = [...(textOptions.tools ?? []), ...mcpTools]
|
|
3008
|
+
}
|
|
3009
|
+
|
|
2930
3010
|
// Inputs may be UIMessages (from useChat) or ModelMessages (from server-side
|
|
2931
3011
|
// callers). TextEngine handles the conversion uniformly.
|
|
2932
3012
|
const engine = new TextEngine(
|
|
@@ -2948,8 +3028,12 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
2948
3028
|
logger,
|
|
2949
3029
|
)
|
|
2950
3030
|
|
|
2951
|
-
|
|
2952
|
-
|
|
3031
|
+
try {
|
|
3032
|
+
for await (const chunk of engine.run()) {
|
|
3033
|
+
yield chunk
|
|
3034
|
+
}
|
|
3035
|
+
} finally {
|
|
3036
|
+
await mcpManager.dispose()
|
|
2953
3037
|
}
|
|
2954
3038
|
|
|
2955
3039
|
// Schema validation for the streaming variant remains the consumer's
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ServerTool } from '../tools/tool-definition'
|
|
2
|
+
import type { ChatMCPOptions, MCPToolSource } from './types'
|
|
3
|
+
|
|
4
|
+
export class MCPDuplicateToolNameError extends Error {
|
|
5
|
+
constructor(public readonly toolName: string) {
|
|
6
|
+
super(
|
|
7
|
+
`Duplicate MCP tool name "${toolName}" in chat({ mcp.clients }). ` +
|
|
8
|
+
`Set a unique \`prefix\` on one of the MCP clients (or use a pool, ` +
|
|
9
|
+
`which auto-prefixes) to disambiguate.`,
|
|
10
|
+
)
|
|
11
|
+
this.name = 'MCPDuplicateToolNameError'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Encapsulates MCP tool discovery + connection lifecycle for chat().
|
|
17
|
+
* Built from chat()'s `mcp` option; runners only call `discover()` then
|
|
18
|
+
* `dispose()`. A manager built from `undefined` is an inert no-op
|
|
19
|
+
* (`discover()` → `[]`, `dispose()` → no-op), so runners need no branching.
|
|
20
|
+
*/
|
|
21
|
+
export class MCPManager {
|
|
22
|
+
static from(options: ChatMCPOptions | undefined): MCPManager {
|
|
23
|
+
return new MCPManager(options)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
readonly #sources: ReadonlyArray<MCPToolSource>
|
|
27
|
+
readonly #shouldClose: boolean
|
|
28
|
+
readonly #lazyTools: boolean
|
|
29
|
+
readonly #onDiscoveryError?: (
|
|
30
|
+
error: unknown,
|
|
31
|
+
source: MCPToolSource,
|
|
32
|
+
) => void | Promise<void>
|
|
33
|
+
|
|
34
|
+
private constructor(options: ChatMCPOptions | undefined) {
|
|
35
|
+
this.#sources = options?.clients ?? []
|
|
36
|
+
// default 'close'; only 'keep-alive' disables closing
|
|
37
|
+
this.#shouldClose = options ? options.connection !== 'keep-alive' : false
|
|
38
|
+
this.#lazyTools = options?.lazyTools ?? false
|
|
39
|
+
this.#onDiscoveryError = options?.onDiscoveryError
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Discover + merge tools from all sources. Throws on a fatal discovery error
|
|
44
|
+
* (no `onDiscoveryError`, or it re-threw) or a duplicate tool name; in that
|
|
45
|
+
* case it first closes any connected sources when the policy is 'close'.
|
|
46
|
+
*/
|
|
47
|
+
async discover(): Promise<Array<ServerTool>> {
|
|
48
|
+
if (this.#sources.length === 0) return []
|
|
49
|
+
try {
|
|
50
|
+
const settled = await Promise.allSettled(
|
|
51
|
+
this.#sources.map((s) => s.tools({ lazy: this.#lazyTools })),
|
|
52
|
+
)
|
|
53
|
+
const tools: Array<ServerTool> = []
|
|
54
|
+
const zipped = this.#sources.map(
|
|
55
|
+
(source, i) => [source, settled[i]] as const,
|
|
56
|
+
)
|
|
57
|
+
for (const [source, result] of zipped) {
|
|
58
|
+
if (result === undefined) continue
|
|
59
|
+
if (result.status === 'fulfilled') {
|
|
60
|
+
tools.push(...result.value)
|
|
61
|
+
} else if (this.#onDiscoveryError) {
|
|
62
|
+
// throw/reject inside handler ⇒ propagate (fail-fast); return ⇒ skip
|
|
63
|
+
await this.#onDiscoveryError(result.reason, source)
|
|
64
|
+
} else {
|
|
65
|
+
throw result.reason
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const seen = new Set<string>()
|
|
69
|
+
for (const t of tools) {
|
|
70
|
+
if (seen.has(t.name)) throw new MCPDuplicateToolNameError(t.name)
|
|
71
|
+
seen.add(t.name)
|
|
72
|
+
}
|
|
73
|
+
return tools
|
|
74
|
+
} catch (err) {
|
|
75
|
+
await this.dispose() // cleanup-on-failure (no-op if keep-alive)
|
|
76
|
+
throw err
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Close sources iff policy is 'close'. Idempotent; never throws. */
|
|
81
|
+
async dispose(): Promise<void> {
|
|
82
|
+
if (!this.#shouldClose || this.#sources.length === 0) return
|
|
83
|
+
await Promise.allSettled(this.#sources.map((s) => s.close()))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ServerTool } from '../tools/tool-definition'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal structural shape that `chat({ mcp })` needs from an MCP client.
|
|
5
|
+
*
|
|
6
|
+
* `@tanstack/ai-mcp`'s `MCPClient` and `MCPClients` satisfy this interface by
|
|
7
|
+
* shape — the core `@tanstack/ai` package does NOT import `@tanstack/ai-mcp`
|
|
8
|
+
* (ai-mcp depends on ai, not the reverse).
|
|
9
|
+
*/
|
|
10
|
+
export interface MCPToolSource {
|
|
11
|
+
// Keep the options shape in sync with ai-mcp's `ToolsOptions` — extra
|
|
12
|
+
// optional fields added there still match structurally, but chat() only
|
|
13
|
+
// forwards what is declared here.
|
|
14
|
+
tools: (options?: { lazy?: boolean }) => Promise<Array<ServerTool>>
|
|
15
|
+
close: () => Promise<void>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Controls what happens to MCP connections when the chat run ends.
|
|
20
|
+
*
|
|
21
|
+
* - `'close'` (default) — `chat()` closes each connection when the run ends
|
|
22
|
+
* (after the agent loop completes and the stream is drained), so tools can
|
|
23
|
+
* still execute throughout the run.
|
|
24
|
+
* - `'keep-alive'` — `chat()` never closes the connections; the caller owns
|
|
25
|
+
* their lifecycle (e.g. keep them warm across requests).
|
|
26
|
+
*/
|
|
27
|
+
export type MCPConnectionPolicy = 'close' | 'keep-alive'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Options controlling MCP tool discovery and lifecycle for a `chat()` call.
|
|
31
|
+
*/
|
|
32
|
+
export interface ChatMCPOptions {
|
|
33
|
+
/**
|
|
34
|
+
* The MCP clients or client pools to discover tools from and manage.
|
|
35
|
+
*/
|
|
36
|
+
clients: Array<MCPToolSource>
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Connection lifecycle policy applied to all clients when the run ends.
|
|
40
|
+
*
|
|
41
|
+
* Defaults to `'close'`.
|
|
42
|
+
*/
|
|
43
|
+
connection?: MCPConnectionPolicy
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* When `true`, tool schemas are fetched lazily (forwarded to
|
|
47
|
+
* `tools({ lazy: true })`).
|
|
48
|
+
*
|
|
49
|
+
* Defaults to `false`.
|
|
50
|
+
*/
|
|
51
|
+
lazyTools?: boolean
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Called when tool discovery fails for a single source.
|
|
55
|
+
*
|
|
56
|
+
* - Throw (or re-throw) from this handler to fail the entire chat call fast.
|
|
57
|
+
* - Return normally to skip that source and continue with remaining clients.
|
|
58
|
+
* - Omit this handler entirely to rethrow the error (fail-fast by default).
|
|
59
|
+
*
|
|
60
|
+
* Async handlers are awaited, so a rejected promise also fails fast.
|
|
61
|
+
*/
|
|
62
|
+
onDiscoveryError?: (
|
|
63
|
+
error: unknown,
|
|
64
|
+
source: MCPToolSource,
|
|
65
|
+
) => void | Promise<void>
|
|
66
|
+
}
|
|
@@ -161,10 +161,14 @@ export function updateToolCallApproval(
|
|
|
161
161
|
)
|
|
162
162
|
|
|
163
163
|
if (toolCallPart) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
const index = parts.indexOf(toolCallPart)
|
|
165
|
+
parts[index] = {
|
|
166
|
+
...toolCallPart,
|
|
167
|
+
state: 'approval-requested',
|
|
168
|
+
approval: {
|
|
169
|
+
id: approvalId,
|
|
170
|
+
needsApproval: true,
|
|
171
|
+
},
|
|
168
172
|
}
|
|
169
173
|
}
|
|
170
174
|
|
|
@@ -192,7 +196,8 @@ export function updateToolCallState(
|
|
|
192
196
|
)
|
|
193
197
|
|
|
194
198
|
if (toolCallPart) {
|
|
195
|
-
|
|
199
|
+
const index = parts.indexOf(toolCallPart)
|
|
200
|
+
parts[index] = { ...toolCallPart, state }
|
|
196
201
|
}
|
|
197
202
|
|
|
198
203
|
return { ...msg, parts }
|
|
@@ -217,8 +222,12 @@ export function updateToolCallWithOutput(
|
|
|
217
222
|
)
|
|
218
223
|
|
|
219
224
|
if (toolCallPart) {
|
|
220
|
-
|
|
221
|
-
|
|
225
|
+
const index = parts.indexOf(toolCallPart)
|
|
226
|
+
parts[index] = {
|
|
227
|
+
...toolCallPart,
|
|
228
|
+
output: errorText ? { error: errorText } : output,
|
|
229
|
+
state: state ?? (errorText ? 'error' : 'complete'),
|
|
230
|
+
}
|
|
222
231
|
}
|
|
223
232
|
|
|
224
233
|
return { ...msg, parts }
|
|
@@ -242,8 +251,12 @@ export function updateToolCallApprovalResponse(
|
|
|
242
251
|
)
|
|
243
252
|
|
|
244
253
|
if (toolCallPart && toolCallPart.approval) {
|
|
245
|
-
|
|
246
|
-
|
|
254
|
+
const index = parts.indexOf(toolCallPart)
|
|
255
|
+
parts[index] = {
|
|
256
|
+
...toolCallPart,
|
|
257
|
+
approval: { ...toolCallPart.approval, approved },
|
|
258
|
+
state: 'approval-responded',
|
|
259
|
+
}
|
|
247
260
|
}
|
|
248
261
|
|
|
249
262
|
return { ...msg, parts }
|
|
@@ -317,7 +317,7 @@ export class StreamProcessor {
|
|
|
317
317
|
this.messages,
|
|
318
318
|
toolCallId,
|
|
319
319
|
output,
|
|
320
|
-
error ? '
|
|
320
|
+
error ? 'error' : undefined,
|
|
321
321
|
error,
|
|
322
322
|
)
|
|
323
323
|
|
|
@@ -1184,7 +1184,7 @@ export class StreamProcessor {
|
|
|
1184
1184
|
this.messages,
|
|
1185
1185
|
chunk.toolCallId,
|
|
1186
1186
|
output,
|
|
1187
|
-
chunk.state === 'output-error' ? '
|
|
1187
|
+
chunk.state === 'output-error' ? 'error' : undefined,
|
|
1188
1188
|
)
|
|
1189
1189
|
|
|
1190
1190
|
// Step 2: Create/update the tool-result part (for LLM conversation history)
|
|
@@ -1240,7 +1240,7 @@ export class StreamProcessor {
|
|
|
1240
1240
|
this.messages,
|
|
1241
1241
|
chunk.toolCallId,
|
|
1242
1242
|
output,
|
|
1243
|
-
chunk.state === 'output-error' ? '
|
|
1243
|
+
chunk.state === 'output-error' ? 'error' : undefined,
|
|
1244
1244
|
)
|
|
1245
1245
|
|
|
1246
1246
|
// Step 2: Create/update the tool-result part
|
|
@@ -1690,11 +1690,22 @@ export class StreamProcessor {
|
|
|
1690
1690
|
_index: number,
|
|
1691
1691
|
toolCall: InternalToolCallState,
|
|
1692
1692
|
): void {
|
|
1693
|
+
// Finalize the internal bookkeeping: the call's input arguments ARE
|
|
1694
|
+
// complete regardless of whether execution later failed, so the call still
|
|
1695
|
+
// counts as a completed tool call in getCompletedToolCalls()/getState().
|
|
1693
1696
|
toolCall.state = 'input-complete'
|
|
1694
1697
|
|
|
1695
1698
|
// Try final parse
|
|
1696
1699
|
toolCall.parsedArguments = this.jsonParser.parse(toolCall.arguments)
|
|
1697
1700
|
|
|
1701
|
+
// Don't downgrade the rendered part of a call that already reached the
|
|
1702
|
+
// terminal 'error' state (e.g. an output-error TOOL_CALL_RESULT arrived
|
|
1703
|
+
// without a preceding TOOL_CALL_END). The RUN_FINISHED / finalizeStream
|
|
1704
|
+
// safety net must not clobber a failed call back to 'input-complete'.
|
|
1705
|
+
if (this.isToolCallPartErrored(toolCall.id)) {
|
|
1706
|
+
return
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1698
1709
|
// Update UIMessage
|
|
1699
1710
|
this.messages = updateToolCallPart(this.messages, messageId, {
|
|
1700
1711
|
id: toolCall.id,
|
|
@@ -1714,6 +1725,22 @@ export class StreamProcessor {
|
|
|
1714
1725
|
)
|
|
1715
1726
|
}
|
|
1716
1727
|
|
|
1728
|
+
/**
|
|
1729
|
+
* Whether the rendered tool-call part for the given id has reached the
|
|
1730
|
+
* terminal 'error' state. Used to prevent the completion safety net from
|
|
1731
|
+
* downgrading a failed call back to 'input-complete'.
|
|
1732
|
+
*/
|
|
1733
|
+
private isToolCallPartErrored(toolCallId: string): boolean {
|
|
1734
|
+
return this.messages.some((msg) =>
|
|
1735
|
+
msg.parts.some(
|
|
1736
|
+
(part) =>
|
|
1737
|
+
part.type === 'tool-call' &&
|
|
1738
|
+
part.id === toolCallId &&
|
|
1739
|
+
part.state === 'error',
|
|
1740
|
+
),
|
|
1741
|
+
)
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1717
1744
|
/**
|
|
1718
1745
|
* Emit pending text update for a specific message.
|
|
1719
1746
|
*
|
|
@@ -599,6 +599,7 @@ export async function* executeToolCalls<TContext = unknown>(
|
|
|
599
599
|
) => CustomEvent,
|
|
600
600
|
middlewareHooks?: ToolExecutionMiddlewareHooks,
|
|
601
601
|
userContext?: TContext,
|
|
602
|
+
abortSignal?: AbortSignal,
|
|
602
603
|
): AsyncGenerator<CustomEvent, ExecuteToolCallsResult, void> {
|
|
603
604
|
const results: Array<ToolResult> = []
|
|
604
605
|
const needsApproval: Array<ApprovalRequest> = []
|
|
@@ -679,6 +680,7 @@ export async function* executeToolCalls<TContext = unknown>(
|
|
|
679
680
|
const context = {
|
|
680
681
|
toolCallId: toolCall.id,
|
|
681
682
|
context: userContext,
|
|
683
|
+
abortSignal,
|
|
682
684
|
emitCustomEvent: (eventName: string, value: Record<string, any>) => {
|
|
683
685
|
if (createCustomEventChunk) {
|
|
684
686
|
pendingEvents.push(
|
|
@@ -15,6 +15,7 @@ import type { DebugOption } from '../../logger/types'
|
|
|
15
15
|
import type { VideoAdapter } from './adapter'
|
|
16
16
|
import type {
|
|
17
17
|
StreamChunk,
|
|
18
|
+
TokenUsage,
|
|
18
19
|
VideoJobResult,
|
|
19
20
|
VideoStatusResult,
|
|
20
21
|
VideoUrlResult,
|
|
@@ -380,6 +381,7 @@ async function* runStreamingVideoGeneration<
|
|
|
380
381
|
status: 'completed',
|
|
381
382
|
url: urlResult.url,
|
|
382
383
|
expiresAt: urlResult.expiresAt,
|
|
384
|
+
...(urlResult.usage ? { usage: urlResult.usage } : {}),
|
|
383
385
|
},
|
|
384
386
|
timestamp: Date.now(),
|
|
385
387
|
} as StreamChunk
|
|
@@ -454,6 +456,7 @@ export async function getVideoJobStatus<
|
|
|
454
456
|
progress?: number
|
|
455
457
|
url?: string
|
|
456
458
|
error?: string
|
|
459
|
+
usage?: TokenUsage
|
|
457
460
|
}> {
|
|
458
461
|
const { adapter, jobId } = options
|
|
459
462
|
const requestId = createId('video-status')
|
|
@@ -487,10 +490,19 @@ export async function getVideoJobStatus<
|
|
|
487
490
|
duration: Date.now() - startTime,
|
|
488
491
|
timestamp: Date.now(),
|
|
489
492
|
})
|
|
493
|
+
if (urlResult.usage) {
|
|
494
|
+
aiEventClient.emit('video:usage', {
|
|
495
|
+
requestId,
|
|
496
|
+
model: adapter.model,
|
|
497
|
+
usage: urlResult.usage,
|
|
498
|
+
timestamp: Date.now(),
|
|
499
|
+
})
|
|
500
|
+
}
|
|
490
501
|
return {
|
|
491
502
|
status: statusResult.status,
|
|
492
503
|
progress: statusResult.progress,
|
|
493
504
|
url: urlResult.url,
|
|
505
|
+
...(urlResult.usage ? { usage: urlResult.usage } : {}),
|
|
494
506
|
}
|
|
495
507
|
} catch (error) {
|
|
496
508
|
const errorMessage =
|