@tanstack/ai 0.27.0 → 0.28.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 +76 -10
- 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/stream/message-updaters.js +20 -8
- package/dist/esm/activities/chat/stream/message-updaters.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/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/types.d.ts +6 -0
- package/package.json +2 -2
- package/skills/ai-core/chat-experience/SKILL.md +71 -0
- package/skills/ai-core/tool-calling/SKILL.md +287 -0
- package/src/activities/chat/index.ts +95 -12
- package/src/activities/chat/mcp/manager.ts +85 -0
- package/src/activities/chat/mcp/types.ts +66 -0
- package/src/activities/chat/stream/message-updaters.ts +22 -9
- package/src/activities/chat/tools/tool-calls.ts +2 -0
- package/src/extend-adapter.ts +42 -24
- package/src/index.ts +10 -0
- package/src/types.ts +6 -0
|
@@ -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 ? 'input-complete' : '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 }
|
|
@@ -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(
|
package/src/extend-adapter.ts
CHANGED
|
@@ -144,6 +144,14 @@ type ExtractCustomModelNames<TDefs extends ReadonlyArray<ExtendedModelDef>> =
|
|
|
144
144
|
// Factory Type Inference
|
|
145
145
|
// ===========================
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* The widest factory shape `extendAdapter` accepts: any function taking a
|
|
149
|
+
* model as its first parameter. Parameters are contravariant, so `never`
|
|
150
|
+
* params and an `unknown` return accept every factory without resorting
|
|
151
|
+
* to `any`.
|
|
152
|
+
*/
|
|
153
|
+
type AnyAdapterFactory = (model: never, ...args: Array<never>) => unknown
|
|
154
|
+
|
|
147
155
|
/**
|
|
148
156
|
* Infer the model parameter type from an adapter factory function.
|
|
149
157
|
* For generic functions like `<T extends Union>(model: T)`, this gets `T` which
|
|
@@ -151,32 +159,44 @@ type ExtractCustomModelNames<TDefs extends ReadonlyArray<ExtendedModelDef>> =
|
|
|
151
159
|
*/
|
|
152
160
|
type InferFactoryModels<TFactory> = TFactory extends (
|
|
153
161
|
model: infer TModel,
|
|
154
|
-
...args: Array<
|
|
155
|
-
) =>
|
|
162
|
+
...args: Array<never>
|
|
163
|
+
) => unknown
|
|
156
164
|
? TModel extends string
|
|
157
165
|
? TModel
|
|
158
166
|
: string
|
|
159
167
|
: string
|
|
160
168
|
|
|
161
|
-
/**
|
|
162
|
-
* Infer the config parameter type from an adapter factory function.
|
|
163
|
-
*/
|
|
164
|
-
type InferConfig<TFactory> = TFactory extends (
|
|
165
|
-
model: any,
|
|
166
|
-
config?: infer TConfig,
|
|
167
|
-
) => any
|
|
168
|
-
? TConfig
|
|
169
|
-
: undefined
|
|
170
|
-
|
|
171
169
|
/**
|
|
172
170
|
* Infer the adapter return type from a factory function.
|
|
173
171
|
*/
|
|
174
172
|
type InferAdapterReturn<TFactory> = TFactory extends (
|
|
175
|
-
...args: Array<
|
|
173
|
+
...args: Array<never>
|
|
176
174
|
) => infer TReturn
|
|
177
175
|
? TReturn
|
|
178
176
|
: never
|
|
179
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Extracts all parameter types after the model parameter from a factory,
|
|
180
|
+
* preserving labels and optionality (e.g. `[apiKey: string, config?: C]`).
|
|
181
|
+
* Note: overloaded factories resolve against their last overload (a
|
|
182
|
+
* `Parameters` limitation).
|
|
183
|
+
*/
|
|
184
|
+
type InferRestArgs<TFactory extends AnyAdapterFactory> =
|
|
185
|
+
Parameters<TFactory> extends [unknown?, ...infer TRest] ? TRest : []
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The factory signature produced by `extendAdapter`: accepts both original
|
|
189
|
+
* and custom model names while preserving all remaining parameters and the
|
|
190
|
+
* return type of the original factory.
|
|
191
|
+
*/
|
|
192
|
+
type ExtendedFactory<
|
|
193
|
+
TFactory extends AnyAdapterFactory,
|
|
194
|
+
TDefs extends ReadonlyArray<ExtendedModelDef>,
|
|
195
|
+
> = (
|
|
196
|
+
model: InferFactoryModels<TFactory> | ExtractCustomModelNames<TDefs>,
|
|
197
|
+
...args: InferRestArgs<TFactory>
|
|
198
|
+
) => InferAdapterReturn<TFactory>
|
|
199
|
+
|
|
180
200
|
// ===========================
|
|
181
201
|
// extendAdapter Function
|
|
182
202
|
// ===========================
|
|
@@ -225,19 +245,17 @@ type InferAdapterReturn<TFactory> = TFactory extends (
|
|
|
225
245
|
* ```
|
|
226
246
|
*/
|
|
227
247
|
export function extendAdapter<
|
|
228
|
-
TFactory extends
|
|
248
|
+
TFactory extends AnyAdapterFactory,
|
|
229
249
|
const TDefs extends ReadonlyArray<ExtendedModelDef>,
|
|
230
|
-
>(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
: [config?: InferConfig<TFactory>]
|
|
238
|
-
) => InferAdapterReturn<TFactory> {
|
|
250
|
+
>(factory: TFactory, _customModels: TDefs): ExtendedFactory<TFactory, TDefs>
|
|
251
|
+
// The implementation signature stays at the honest `AnyAdapterFactory` width;
|
|
252
|
+
// the overload above performs the deliberate model-union widening.
|
|
253
|
+
export function extendAdapter(
|
|
254
|
+
factory: AnyAdapterFactory,
|
|
255
|
+
_customModels: ReadonlyArray<ExtendedModelDef>,
|
|
256
|
+
): AnyAdapterFactory {
|
|
239
257
|
// At runtime, we simply pass through to the original factory.
|
|
240
258
|
// The _customModels parameter is only used for type inference.
|
|
241
259
|
// No runtime validation - users are trusted to pass valid model names.
|
|
242
|
-
return factory
|
|
260
|
+
return factory
|
|
243
261
|
}
|
package/src/index.ts
CHANGED
|
@@ -52,6 +52,16 @@ export {
|
|
|
52
52
|
type InferToolOutput,
|
|
53
53
|
} from './activities/chat/tools/tool-definition'
|
|
54
54
|
|
|
55
|
+
// MCP chat option types
|
|
56
|
+
export type {
|
|
57
|
+
MCPToolSource,
|
|
58
|
+
ChatMCPOptions,
|
|
59
|
+
MCPConnectionPolicy,
|
|
60
|
+
} from './activities/chat/mcp/types'
|
|
61
|
+
|
|
62
|
+
// MCP error classes (value exports — usable with instanceof)
|
|
63
|
+
export { MCPDuplicateToolNameError } from './activities/chat/mcp/manager'
|
|
64
|
+
|
|
55
65
|
// Schema conversion (Standard JSON Schema compliant)
|
|
56
66
|
export {
|
|
57
67
|
convertSchemaToJsonSchema,
|
package/src/types.ts
CHANGED
|
@@ -490,6 +490,12 @@ export type ToolExecutionContext<TContext = unknown> =
|
|
|
490
490
|
RuntimeContextField<TContext> & {
|
|
491
491
|
/** The ID of the tool call being executed */
|
|
492
492
|
toolCallId?: string
|
|
493
|
+
/**
|
|
494
|
+
* Abort signal for the current chat run. Aborts when the run's
|
|
495
|
+
* `abortController` fires (or middleware aborts). Long-running tools —
|
|
496
|
+
* e.g. MCP `callTool` — should forward this to cancel in-flight work.
|
|
497
|
+
*/
|
|
498
|
+
abortSignal?: AbortSignal
|
|
493
499
|
/**
|
|
494
500
|
* Emit a custom event during tool execution.
|
|
495
501
|
* Events are streamed to the client in real-time as AG-UI CUSTOM events.
|