@tanstack/ai 0.9.2 → 0.10.1
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/stream/processor.js +3 -0
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tool-registry.d.ts +81 -0
- package/dist/esm/tool-registry.js +49 -0
- package/dist/esm/tool-registry.js.map +1 -0
- package/package.json +6 -4
- package/skills/ai-core/SKILL.md +59 -0
- package/skills/ai-core/adapter-configuration/SKILL.md +283 -0
- package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +97 -0
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +102 -0
- package/skills/ai-core/adapter-configuration/references/grok-adapter.md +77 -0
- package/skills/ai-core/adapter-configuration/references/groq-adapter.md +106 -0
- package/skills/ai-core/adapter-configuration/references/ollama-adapter.md +82 -0
- package/skills/ai-core/adapter-configuration/references/openai-adapter.md +95 -0
- package/skills/ai-core/adapter-configuration/references/openrouter-adapter.md +99 -0
- package/skills/ai-core/ag-ui-protocol/SKILL.md +232 -0
- package/skills/ai-core/chat-experience/SKILL.md +506 -0
- package/skills/ai-core/custom-backend-integration/SKILL.md +463 -0
- package/skills/ai-core/media-generation/SKILL.md +471 -0
- package/skills/ai-core/middleware/SKILL.md +336 -0
- package/skills/ai-core/structured-outputs/SKILL.md +203 -0
- package/skills/ai-core/tool-calling/SKILL.md +411 -0
- package/src/activities/chat/stream/processor.ts +7 -0
- package/src/index.ts +7 -0
- package/src/tool-registry.ts +150 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ai-core/tool-calling
|
|
3
|
+
description: >
|
|
4
|
+
Isomorphic tool system: toolDefinition() with Zod schemas,
|
|
5
|
+
.server() and .client() implementations, passing tools to both
|
|
6
|
+
chat() on server and useChat/clientTools on client, tool approval
|
|
7
|
+
flows with needsApproval and addToolApprovalResponse(), lazy tool
|
|
8
|
+
discovery with lazy:true, rendering ToolCallPart and ToolResultPart
|
|
9
|
+
in UI.
|
|
10
|
+
type: sub-skill
|
|
11
|
+
library: tanstack-ai
|
|
12
|
+
library_version: '0.10.0'
|
|
13
|
+
sources:
|
|
14
|
+
- 'TanStack/ai:docs/tools/tools.md'
|
|
15
|
+
- 'TanStack/ai:docs/tools/server-tools.md'
|
|
16
|
+
- 'TanStack/ai:docs/tools/client-tools.md'
|
|
17
|
+
- 'TanStack/ai:docs/tools/tool-approval.md'
|
|
18
|
+
- 'TanStack/ai:docs/tools/lazy-tool-discovery.md'
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Tool Calling
|
|
22
|
+
|
|
23
|
+
This skill builds on ai-core. Read it first for critical rules.
|
|
24
|
+
|
|
25
|
+
## Setup
|
|
26
|
+
|
|
27
|
+
Complete end-to-end example: shared definition, server tool, client tool, server route, React client.
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// tools/definitions.ts
|
|
31
|
+
import { toolDefinition } from '@tanstack/ai'
|
|
32
|
+
import { z } from 'zod'
|
|
33
|
+
|
|
34
|
+
export const getProductsDef = toolDefinition({
|
|
35
|
+
name: 'get_products',
|
|
36
|
+
description: 'Search for products in the catalog',
|
|
37
|
+
inputSchema: z.object({
|
|
38
|
+
query: z.string().meta({ description: 'Search keyword' }),
|
|
39
|
+
limit: z.number().optional().meta({ description: 'Max results' }),
|
|
40
|
+
}),
|
|
41
|
+
outputSchema: z.object({
|
|
42
|
+
products: z.array(
|
|
43
|
+
z.object({ id: z.string(), name: z.string(), price: z.number() }),
|
|
44
|
+
),
|
|
45
|
+
}),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
export const updateCartUIDef = toolDefinition({
|
|
49
|
+
name: 'update_cart_ui',
|
|
50
|
+
description: 'Update the shopping cart UI with item count',
|
|
51
|
+
inputSchema: z.object({ itemCount: z.number(), message: z.string() }),
|
|
52
|
+
outputSchema: z.object({ displayed: z.boolean() }),
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// tools/server.ts
|
|
58
|
+
import { getProductsDef } from './definitions'
|
|
59
|
+
|
|
60
|
+
export const getProducts = getProductsDef.server(async ({ query, limit }) => {
|
|
61
|
+
const results = await db.products.search(query, { limit: limit ?? 10 })
|
|
62
|
+
return {
|
|
63
|
+
products: results.map((p) => ({ id: p.id, name: p.name, price: p.price })),
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// api/chat/route.ts
|
|
70
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
71
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
72
|
+
import { getProducts } from '@/tools/server'
|
|
73
|
+
import { updateCartUIDef } from '@/tools/definitions'
|
|
74
|
+
|
|
75
|
+
export async function POST(request: Request) {
|
|
76
|
+
const { messages } = await request.json()
|
|
77
|
+
const stream = chat({
|
|
78
|
+
adapter: openaiText('gpt-4o'),
|
|
79
|
+
messages,
|
|
80
|
+
tools: [getProducts, updateCartUIDef], // server tool + client definition
|
|
81
|
+
})
|
|
82
|
+
return toServerSentEventsResponse(stream)
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// app/chat.tsx
|
|
88
|
+
import {
|
|
89
|
+
useChat,
|
|
90
|
+
fetchServerSentEvents,
|
|
91
|
+
clientTools,
|
|
92
|
+
createChatClientOptions,
|
|
93
|
+
type InferChatMessages,
|
|
94
|
+
} from "@tanstack/ai-react";
|
|
95
|
+
import { updateCartUIDef } from "@/tools/definitions";
|
|
96
|
+
import { useState } from "react";
|
|
97
|
+
|
|
98
|
+
function ChatPage() {
|
|
99
|
+
const [cartCount, setCartCount] = useState(0);
|
|
100
|
+
|
|
101
|
+
const updateCartUI = updateCartUIDef.client((input) => {
|
|
102
|
+
setCartCount(input.itemCount);
|
|
103
|
+
return { displayed: true };
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const tools = clientTools(updateCartUI);
|
|
107
|
+
const chatOptions = createChatClientOptions({
|
|
108
|
+
connection: fetchServerSentEvents("/api/chat"),
|
|
109
|
+
tools,
|
|
110
|
+
});
|
|
111
|
+
type Messages = InferChatMessages<typeof chatOptions>;
|
|
112
|
+
|
|
113
|
+
const { messages, sendMessage } = useChat(chatOptions);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div>
|
|
117
|
+
<span>Cart: {cartCount}</span>
|
|
118
|
+
{(messages as Messages).map((msg) => (
|
|
119
|
+
<div key={msg.id}>
|
|
120
|
+
{msg.parts.map((part) => {
|
|
121
|
+
if (part.type === "text") return <p>{part.content}</p>;
|
|
122
|
+
if (part.type === "tool-call") {
|
|
123
|
+
return <div key={part.id}>Tool: {part.name} ({part.state})</div>;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
})}
|
|
127
|
+
</div>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Core Patterns
|
|
135
|
+
|
|
136
|
+
### Pattern 1: Server-Only Tool
|
|
137
|
+
|
|
138
|
+
Define with `toolDefinition()`, implement with `.server()`, pass to `chat({ tools })`.
|
|
139
|
+
The server executes it automatically. The client never runs code for this tool.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { toolDefinition } from '@tanstack/ai'
|
|
143
|
+
import { z } from 'zod'
|
|
144
|
+
|
|
145
|
+
const getUserDataDef = toolDefinition({
|
|
146
|
+
name: 'get_user_data',
|
|
147
|
+
description: 'Look up user by ID',
|
|
148
|
+
inputSchema: z.object({
|
|
149
|
+
userId: z.string().meta({ description: "The user's ID" }),
|
|
150
|
+
}),
|
|
151
|
+
outputSchema: z.object({ name: z.string(), email: z.string() }),
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const getUserData = getUserDataDef.server(async ({ userId }) => {
|
|
155
|
+
const user = await db.users.findUnique({ where: { id: userId } })
|
|
156
|
+
return { name: user.name, email: user.email }
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// In your route handler:
|
|
160
|
+
const stream = chat({
|
|
161
|
+
adapter: openaiText('gpt-4o'),
|
|
162
|
+
messages,
|
|
163
|
+
tools: [getUserData],
|
|
164
|
+
})
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Pattern 2: Client-Only Tool
|
|
168
|
+
|
|
169
|
+
Pass the bare definition (no `.server()`) to `chat({ tools })` so the LLM knows
|
|
170
|
+
about it. Pass the `.client()` implementation to `useChat` via `clientTools()`.
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { toolDefinition } from '@tanstack/ai'
|
|
174
|
+
import { z } from 'zod'
|
|
175
|
+
|
|
176
|
+
export const showNotificationDef = toolDefinition({
|
|
177
|
+
name: 'show_notification',
|
|
178
|
+
description: 'Display a toast notification to the user',
|
|
179
|
+
inputSchema: z.object({
|
|
180
|
+
message: z.string(),
|
|
181
|
+
type: z.enum(['success', 'error', 'info']),
|
|
182
|
+
}),
|
|
183
|
+
outputSchema: z.object({ shown: z.boolean() }),
|
|
184
|
+
})
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Server -- pass definition only (no execute function):
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const stream = chat({
|
|
191
|
+
adapter: openaiText('gpt-4o'),
|
|
192
|
+
messages,
|
|
193
|
+
tools: [showNotificationDef],
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Client -- pass `.client()` implementation:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import {
|
|
201
|
+
useChat,
|
|
202
|
+
fetchServerSentEvents,
|
|
203
|
+
clientTools,
|
|
204
|
+
createChatClientOptions,
|
|
205
|
+
} from "@tanstack/ai-react";
|
|
206
|
+
import { showNotificationDef } from "@/tools/definitions";
|
|
207
|
+
import { useState } from "react";
|
|
208
|
+
|
|
209
|
+
function ChatPage() {
|
|
210
|
+
const [toast, setToast] = useState<string | null>(null);
|
|
211
|
+
|
|
212
|
+
const showNotification = showNotificationDef.client((input) => {
|
|
213
|
+
setToast(input.message);
|
|
214
|
+
setTimeout(() => setToast(null), 3000);
|
|
215
|
+
return { shown: true };
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const { messages, sendMessage } = useChat(
|
|
219
|
+
createChatClientOptions({
|
|
220
|
+
connection: fetchServerSentEvents("/api/chat"),
|
|
221
|
+
tools: clientTools(showNotification),
|
|
222
|
+
})
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div>
|
|
227
|
+
{toast && <div className="toast">{toast}</div>}
|
|
228
|
+
{messages.map((msg) => (
|
|
229
|
+
<div key={msg.id}>
|
|
230
|
+
{msg.parts.map((part) =>
|
|
231
|
+
part.type === "text" ? <p>{part.content}</p> : null
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
))}
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Pattern 3: Tool with Approval Flow
|
|
241
|
+
|
|
242
|
+
Set `needsApproval: true` in the definition. Execution pauses until the client
|
|
243
|
+
calls `addToolApprovalResponse()`. The part has `state: "approval-requested"`
|
|
244
|
+
and an `approval` object with an `id`.
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { toolDefinition } from '@tanstack/ai'
|
|
248
|
+
import { z } from 'zod'
|
|
249
|
+
|
|
250
|
+
export const sendEmailDef = toolDefinition({
|
|
251
|
+
name: 'send_email',
|
|
252
|
+
description: 'Send an email to a recipient',
|
|
253
|
+
inputSchema: z.object({
|
|
254
|
+
to: z.string().email(),
|
|
255
|
+
subject: z.string(),
|
|
256
|
+
body: z.string(),
|
|
257
|
+
}),
|
|
258
|
+
outputSchema: z.object({ success: z.boolean(), messageId: z.string() }),
|
|
259
|
+
needsApproval: true,
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
export const sendEmail = sendEmailDef.server(async ({ to, subject, body }) => {
|
|
263
|
+
const result = await emailService.send({ to, subject, body })
|
|
264
|
+
return { success: true, messageId: result.id }
|
|
265
|
+
})
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Client -- render approval UI and respond:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
|
|
272
|
+
|
|
273
|
+
function ChatPage() {
|
|
274
|
+
const { messages, addToolApprovalResponse } = useChat({
|
|
275
|
+
connection: fetchServerSentEvents("/api/chat"),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<div>
|
|
280
|
+
{messages.map((msg) => (
|
|
281
|
+
<div key={msg.id}>
|
|
282
|
+
{msg.parts.map((part) => {
|
|
283
|
+
if (part.type === "text") return <p>{part.content}</p>;
|
|
284
|
+
if (
|
|
285
|
+
part.type === "tool-call" &&
|
|
286
|
+
part.state === "approval-requested" &&
|
|
287
|
+
part.approval
|
|
288
|
+
) {
|
|
289
|
+
return (
|
|
290
|
+
<div key={part.id}>
|
|
291
|
+
<p>Approve "{part.name}"?</p>
|
|
292
|
+
<pre>{part.arguments}</pre>
|
|
293
|
+
<button
|
|
294
|
+
onClick={() =>
|
|
295
|
+
addToolApprovalResponse({
|
|
296
|
+
id: part.approval!.id,
|
|
297
|
+
approved: true,
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
>
|
|
301
|
+
Approve
|
|
302
|
+
</button>
|
|
303
|
+
<button
|
|
304
|
+
onClick={() =>
|
|
305
|
+
addToolApprovalResponse({
|
|
306
|
+
id: part.approval!.id,
|
|
307
|
+
approved: false,
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
>
|
|
311
|
+
Deny
|
|
312
|
+
</button>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
})}
|
|
318
|
+
</div>
|
|
319
|
+
))}
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Pattern 4: Lazy Tool Discovery
|
|
326
|
+
|
|
327
|
+
Set `lazy: true` on rarely-needed tools. The LLM sees their names via a synthetic
|
|
328
|
+
`__lazy__tool__discovery__` tool and discovers schemas on demand. Saves tokens.
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
import {
|
|
332
|
+
toolDefinition,
|
|
333
|
+
chat,
|
|
334
|
+
toServerSentEventsResponse,
|
|
335
|
+
maxIterations,
|
|
336
|
+
} from '@tanstack/ai'
|
|
337
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
338
|
+
import { z } from 'zod'
|
|
339
|
+
|
|
340
|
+
const getProductsDef = toolDefinition({
|
|
341
|
+
name: 'getProducts',
|
|
342
|
+
description: 'List all products',
|
|
343
|
+
inputSchema: z.object({}),
|
|
344
|
+
outputSchema: z.array(
|
|
345
|
+
z.object({ id: z.number(), name: z.string(), price: z.number() }),
|
|
346
|
+
),
|
|
347
|
+
})
|
|
348
|
+
const getProducts = getProductsDef.server(async () => db.products.findMany())
|
|
349
|
+
|
|
350
|
+
const compareProductsDef = toolDefinition({
|
|
351
|
+
name: 'compareProducts',
|
|
352
|
+
description: 'Compare two or more products side by side',
|
|
353
|
+
inputSchema: z.object({ productIds: z.array(z.number()).min(2) }),
|
|
354
|
+
lazy: true, // not sent to LLM upfront
|
|
355
|
+
})
|
|
356
|
+
const compareProducts = compareProductsDef.server(async ({ productIds }) => {
|
|
357
|
+
return db.products.findMany({ where: { id: { in: productIds } } })
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
export async function POST(request: Request) {
|
|
361
|
+
const { messages } = await request.json()
|
|
362
|
+
const stream = chat({
|
|
363
|
+
adapter: openaiText('gpt-4o'),
|
|
364
|
+
messages,
|
|
365
|
+
tools: [getProducts, compareProducts],
|
|
366
|
+
agentLoopStrategy: maxIterations(20),
|
|
367
|
+
})
|
|
368
|
+
return toServerSentEventsResponse(stream)
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
The LLM sees `getProducts` and `__lazy__tool__discovery__` upfront.
|
|
373
|
+
To compare, it first calls `__lazy__tool__discovery__({ toolNames: ["compareProducts"] })`,
|
|
374
|
+
gets the full schema, then calls `compareProducts` directly.
|
|
375
|
+
Once discovered, a tool stays available for the conversation.
|
|
376
|
+
When all lazy tools are discovered, the discovery tool is removed automatically.
|
|
377
|
+
|
|
378
|
+
## Common Mistakes
|
|
379
|
+
|
|
380
|
+
### a. HIGH: Not passing tool definitions to both server and client
|
|
381
|
+
|
|
382
|
+
Server tools need `chat({ tools })`. Client tools need their definition in
|
|
383
|
+
`chat({ tools })` AND their `.client()` in `useChat({ tools: clientTools(...) })`.
|
|
384
|
+
|
|
385
|
+
Wrong -- tool only on server, client cannot execute:
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
chat({ adapter, messages, tools: [myToolDef] })
|
|
389
|
+
useChat({ connection: fetchServerSentEvents('/api/chat') }) // no tools
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Wrong -- tool only on client, LLM does not know about it:
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
chat({ adapter, messages }); // no tools
|
|
396
|
+
useChat({ ..., tools: clientTools(myToolDef.client(() => result)) });
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Correct:
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
chat({ adapter, messages, tools: [myToolDef] });
|
|
403
|
+
useChat({ ..., tools: clientTools(myToolDef.client((input) => ({ success: true }))) });
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Source: docs/tools/tools.md
|
|
407
|
+
|
|
408
|
+
## Cross-References
|
|
409
|
+
|
|
410
|
+
- See also: ai-core/chat-experience/SKILL.md -- Tools are used within chat
|
|
411
|
+
- See also: `@tanstack/ai-code-mode` package skills -- Code Mode is an alternative to tools for complex multi-step operations
|
|
@@ -967,6 +967,13 @@ export class StreamProcessor {
|
|
|
967
967
|
// Transition the tool call to input-complete (the authoritative completion signal)
|
|
968
968
|
const existingToolCall = msgState.toolCalls.get(chunk.toolCallId)
|
|
969
969
|
if (existingToolCall && existingToolCall.state !== 'input-complete') {
|
|
970
|
+
// If TOOL_CALL_END provides parsed input and no TOOL_CALL_ARGS were
|
|
971
|
+
// received, back-fill the arguments string so the UIMessage ToolCallPart
|
|
972
|
+
// carries the correct value (defensive against adapters that skip ARGS).
|
|
973
|
+
if (chunk.input !== undefined && !existingToolCall.arguments) {
|
|
974
|
+
existingToolCall.arguments = JSON.stringify(chunk.input)
|
|
975
|
+
}
|
|
976
|
+
|
|
970
977
|
const index = msgState.toolCallOrder.indexOf(chunk.toolCallId)
|
|
971
978
|
this.completeToolCall(messageId, index, existingToolCall)
|
|
972
979
|
// If TOOL_CALL_END provides parsed input, use it as the canonical parsed
|
package/src/index.ts
CHANGED
|
@@ -70,6 +70,13 @@ export {
|
|
|
70
70
|
combineStrategies,
|
|
71
71
|
} from './activities/chat/agent-loop-strategies'
|
|
72
72
|
|
|
73
|
+
// Tool registry
|
|
74
|
+
export {
|
|
75
|
+
createToolRegistry,
|
|
76
|
+
createFrozenRegistry,
|
|
77
|
+
type ToolRegistry,
|
|
78
|
+
} from './tool-registry'
|
|
79
|
+
|
|
73
80
|
// Chat middleware
|
|
74
81
|
export type {
|
|
75
82
|
ChatMiddleware,
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { Tool } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A registry that holds tools and allows dynamic tool management.
|
|
5
|
+
*
|
|
6
|
+
* The registry can be either mutable (allowing additions/removals during execution)
|
|
7
|
+
* or frozen (static tool list, for backward compatibility with tools arrays).
|
|
8
|
+
*/
|
|
9
|
+
export interface ToolRegistry {
|
|
10
|
+
/**
|
|
11
|
+
* Get all current tools in the registry.
|
|
12
|
+
* Called each agent loop iteration to get the latest tool list.
|
|
13
|
+
*/
|
|
14
|
+
getTools: () => ReadonlyArray<Tool>
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Add a tool to the registry dynamically.
|
|
18
|
+
* For frozen registries, this is a no-op.
|
|
19
|
+
*
|
|
20
|
+
* @param tool - The tool to add
|
|
21
|
+
*/
|
|
22
|
+
add: (tool: Tool) => void
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Remove a tool from the registry by name.
|
|
26
|
+
* For frozen registries, this always returns false.
|
|
27
|
+
*
|
|
28
|
+
* @param name - The name of the tool to remove
|
|
29
|
+
* @returns true if the tool was removed, false if not found or frozen
|
|
30
|
+
*/
|
|
31
|
+
remove: (name: string) => boolean
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if a tool exists in the registry.
|
|
35
|
+
*
|
|
36
|
+
* @param name - The name of the tool to check
|
|
37
|
+
*/
|
|
38
|
+
has: (name: string) => boolean
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get a tool by name.
|
|
42
|
+
*
|
|
43
|
+
* @param name - The name of the tool to get
|
|
44
|
+
* @returns The tool if found, undefined otherwise
|
|
45
|
+
*/
|
|
46
|
+
get: (name: string) => Tool | undefined
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether this registry is frozen (immutable).
|
|
50
|
+
* Frozen registries don't allow add/remove operations.
|
|
51
|
+
*/
|
|
52
|
+
readonly isFrozen: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a mutable tool registry for dynamic tool scenarios.
|
|
57
|
+
*
|
|
58
|
+
* Tools can be added and removed during chat execution, and the
|
|
59
|
+
* changes will be reflected in subsequent agent loop iterations.
|
|
60
|
+
*
|
|
61
|
+
* @param initialTools - Optional initial set of tools
|
|
62
|
+
* @returns A mutable ToolRegistry
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* const registry = createToolRegistry([toolA, toolB])
|
|
67
|
+
*
|
|
68
|
+
* const stream = chat({
|
|
69
|
+
* adapter,
|
|
70
|
+
* messages,
|
|
71
|
+
* toolRegistry: registry,
|
|
72
|
+
* })
|
|
73
|
+
*
|
|
74
|
+
* // Later, during tool execution:
|
|
75
|
+
* registry.add(newTool) // Immediately available to LLM
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function createToolRegistry(
|
|
79
|
+
initialTools: Array<Tool> = [],
|
|
80
|
+
): ToolRegistry {
|
|
81
|
+
const tools = new Map<string, Tool>()
|
|
82
|
+
|
|
83
|
+
for (const tool of initialTools) {
|
|
84
|
+
tools.set(tool.name, tool)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
getTools: () => Array.from(tools.values()),
|
|
89
|
+
|
|
90
|
+
add: (tool: Tool) => {
|
|
91
|
+
tools.set(tool.name, tool)
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
remove: (name: string) => {
|
|
95
|
+
return tools.delete(name)
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
has: (name: string) => {
|
|
99
|
+
return tools.has(name)
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
get: (name: string) => {
|
|
103
|
+
return tools.get(name)
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
isFrozen: false,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create a frozen (immutable) tool registry from a tools array.
|
|
112
|
+
*
|
|
113
|
+
* This is used internally to wrap static `tools` arrays for backward compatibility.
|
|
114
|
+
* Add and remove operations are no-ops on frozen registries.
|
|
115
|
+
*
|
|
116
|
+
* @param tools - The static array of tools
|
|
117
|
+
* @returns A frozen ToolRegistry
|
|
118
|
+
*/
|
|
119
|
+
export function createFrozenRegistry(tools: Array<Tool> = []): ToolRegistry {
|
|
120
|
+
const toolMap = new Map<string, Tool>()
|
|
121
|
+
|
|
122
|
+
for (const tool of tools) {
|
|
123
|
+
toolMap.set(tool.name, tool)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const frozenTools = Object.freeze([...tools])
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
getTools: () => frozenTools,
|
|
130
|
+
|
|
131
|
+
add: (_tool: Tool) => {
|
|
132
|
+
// No-op for frozen registry
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
remove: (_name: string) => {
|
|
136
|
+
// No-op for frozen registry
|
|
137
|
+
return false
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
has: (name: string) => {
|
|
141
|
+
return toolMap.has(name)
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
get: (name: string) => {
|
|
145
|
+
return toolMap.get(name)
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
isFrozen: true,
|
|
149
|
+
}
|
|
150
|
+
}
|