@rudderjs/ai 0.0.1 → 0.0.3
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/README.md +89 -12
- package/boost/skills/ai-agents/SKILL.md +233 -0
- package/boost/skills/ai-tools/SKILL.md +244 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +110 -0
- package/dist/agent.js.map +1 -1
- package/dist/commands/make-agent.d.ts +3 -0
- package/dist/commands/make-agent.d.ts.map +1 -0
- package/dist/commands/make-agent.js +23 -0
- package/dist/commands/make-agent.js.map +1 -0
- package/dist/facade.d.ts +28 -1
- package/dist/facade.d.ts.map +1 -1
- package/dist/facade.js +25 -0
- package/dist/facade.js.map +1 -1
- package/dist/fake.d.ts +31 -1
- package/dist/fake.d.ts.map +1 -1
- package/dist/fake.js +98 -0
- package/dist/fake.js.map +1 -1
- package/dist/files.d.ts +27 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +44 -0
- package/dist/files.js.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/observers.d.ts +84 -0
- package/dist/observers.d.ts.map +1 -0
- package/dist/observers.js +40 -0
- package/dist/observers.js.map +1 -0
- package/dist/provider.d.ts +8 -6
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +86 -81
- package/dist/provider.js.map +1 -1
- package/dist/providers/anthropic.d.ts +2 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +63 -0
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/cohere.d.ts +13 -0
- package/dist/providers/cohere.d.ts.map +1 -0
- package/dist/providers/cohere.js +87 -0
- package/dist/providers/cohere.js.map +1 -0
- package/dist/providers/google.d.ts +2 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +53 -0
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/jina.d.ts +13 -0
- package/dist/providers/jina.d.ts.map +1 -0
- package/dist/providers/jina.js +90 -0
- package/dist/providers/jina.js.map +1 -0
- package/dist/providers/openai.d.ts +2 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +62 -0
- package/dist/providers/openai.js.map +1 -1
- package/dist/registry.d.ts +5 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +19 -0
- package/dist/registry.js.map +1 -1
- package/dist/rerank.d.ts +20 -0
- package/dist/rerank.d.ts.map +1 -0
- package/dist/rerank.js +40 -0
- package/dist/rerank.js.map +1 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +18 -4
package/README.md
CHANGED
|
@@ -14,7 +14,8 @@ Install the provider SDK(s) you need:
|
|
|
14
14
|
pnpm add @anthropic-ai/sdk # Anthropic (Claude)
|
|
15
15
|
pnpm add openai # OpenAI (GPT)
|
|
16
16
|
pnpm add @google/genai # Google (Gemini)
|
|
17
|
-
|
|
17
|
+
pnpm add cohere-ai # Cohere (reranking + embeddings)
|
|
18
|
+
# Ollama, Jina — no extra package needed
|
|
18
19
|
```
|
|
19
20
|
|
|
20
21
|
## Setup
|
|
@@ -28,6 +29,8 @@ export default {
|
|
|
28
29
|
openai: { driver: 'openai', apiKey: process.env.OPENAI_API_KEY! },
|
|
29
30
|
google: { driver: 'google', apiKey: process.env.GOOGLE_API_KEY! },
|
|
30
31
|
ollama: { driver: 'ollama', baseUrl: 'http://localhost:11434' },
|
|
32
|
+
cohere: { driver: 'cohere', apiKey: process.env.COHERE_API_KEY! },
|
|
33
|
+
jina: { driver: 'jina', apiKey: process.env.JINA_API_KEY! },
|
|
31
34
|
},
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -332,6 +335,55 @@ const agent = AI.agent({
|
|
|
332
335
|
})
|
|
333
336
|
```
|
|
334
337
|
|
|
338
|
+
### Reranking
|
|
339
|
+
|
|
340
|
+
Reorder documents by relevance to a query — useful for RAG pipelines:
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
import { AI } from '@rudderjs/ai'
|
|
344
|
+
|
|
345
|
+
// One-shot
|
|
346
|
+
const result = await AI.rerank('search query', documents, {
|
|
347
|
+
model: 'cohere/rerank-v3.5',
|
|
348
|
+
topK: 5,
|
|
349
|
+
})
|
|
350
|
+
// result.results → [{ index, relevanceScore, document }, ...]
|
|
351
|
+
|
|
352
|
+
// Fluent builder
|
|
353
|
+
const result = await AI.rerank('how to deploy', docs)
|
|
354
|
+
.model('jina/jina-reranker-v2-base-multilingual')
|
|
355
|
+
.topK(10)
|
|
356
|
+
.rank()
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Supported providers: **Cohere** (`cohere-ai` SDK) and **Jina** (direct HTTP, no SDK).
|
|
360
|
+
|
|
361
|
+
### File Management
|
|
362
|
+
|
|
363
|
+
Upload, list, and delete files on provider platforms — needed for large document context and assistant APIs:
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
import { AI } from '@rudderjs/ai'
|
|
367
|
+
|
|
368
|
+
const files = AI.files('openai')
|
|
369
|
+
|
|
370
|
+
// Upload
|
|
371
|
+
const uploaded = await files.upload('./report.pdf', { purpose: 'assistants' })
|
|
372
|
+
// uploaded → { id, filename, bytes, purpose }
|
|
373
|
+
|
|
374
|
+
// List
|
|
375
|
+
const { files: allFiles } = await files.list()
|
|
376
|
+
|
|
377
|
+
// Delete
|
|
378
|
+
await files.delete(uploaded.id)
|
|
379
|
+
|
|
380
|
+
// Retrieve content (OpenAI, Anthropic)
|
|
381
|
+
const content = await files.retrieve(uploaded.id)
|
|
382
|
+
// content → { data: Buffer, mimeType }
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Supported providers: **OpenAI** (full CRUD + retrieve), **Anthropic** (full CRUD + retrieve), **Google** (upload, list, delete — no retrieve).
|
|
386
|
+
|
|
335
387
|
### Embeddings
|
|
336
388
|
|
|
337
389
|
```ts
|
|
@@ -439,19 +491,43 @@ fake.assertPrompted(input => input.includes('Hello'))
|
|
|
439
491
|
fake.restore()
|
|
440
492
|
```
|
|
441
493
|
|
|
494
|
+
Fakes cover every modality:
|
|
495
|
+
|
|
496
|
+
```ts
|
|
497
|
+
fake.respondWith('text') // text generation
|
|
498
|
+
fake.respondWithImage('base64...') // image generation
|
|
499
|
+
fake.respondWithAudio(Buffer.from('')) // TTS
|
|
500
|
+
fake.respondWithTranscription('text') // STT
|
|
501
|
+
fake.respondWithEmbedding([[0.1, 0.2]]) // embeddings
|
|
502
|
+
fake.respondWithRanking([ // reranking
|
|
503
|
+
{ index: 0, relevanceScore: 0.95, document: 'most relevant' },
|
|
504
|
+
])
|
|
505
|
+
fake.respondWithFileUpload({ // file upload
|
|
506
|
+
id: 'file-123', filename: 'report.pdf', bytes: 1024,
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
// Assertions
|
|
510
|
+
fake.assertPrompted() fake.assertImageGenerated()
|
|
511
|
+
fake.assertAudioGenerated() fake.assertTranscribed()
|
|
512
|
+
fake.assertEmbedded() fake.assertReranked()
|
|
513
|
+
fake.assertFileUploaded()
|
|
514
|
+
```
|
|
515
|
+
|
|
442
516
|
## Providers
|
|
443
517
|
|
|
444
|
-
| Provider | SDK | Model String | Embeddings | Images | TTS/STT |
|
|
445
|
-
|
|
446
|
-
| Anthropic | `@anthropic-ai/sdk` | `anthropic/claude-sonnet-4-5` | | | |
|
|
447
|
-
| OpenAI | `openai` | `openai/gpt-4o` | ✓ | ✓ | ✓ |
|
|
448
|
-
| Google | `@google/genai` | `google/gemini-2.5-pro` | ✓ | ✓ | |
|
|
449
|
-
|
|
|
450
|
-
|
|
|
451
|
-
|
|
|
452
|
-
|
|
|
453
|
-
|
|
|
454
|
-
|
|
|
518
|
+
| Provider | SDK | Model String | Text | Embeddings | Images | TTS/STT | Reranking | Files |
|
|
519
|
+
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|
|
|
520
|
+
| Anthropic | `@anthropic-ai/sdk` | `anthropic/claude-sonnet-4-5` | ✓ | | | | | ✓ |
|
|
521
|
+
| OpenAI | `openai` | `openai/gpt-4o` | ✓ | ✓ | ✓ | ✓ | | ✓ |
|
|
522
|
+
| Google | `@google/genai` | `google/gemini-2.5-pro` | ✓ | ✓ | ✓ | | | ✓ |
|
|
523
|
+
| Cohere | `cohere-ai` | `cohere/rerank-v3.5` | | ✓ | | | ✓ | |
|
|
524
|
+
| Jina | *(none)* | `jina/jina-reranker-v2-base-multilingual` | | ✓ | | | ✓ | |
|
|
525
|
+
| Ollama | *(none)* | `ollama/llama3` | ✓ | | | | | |
|
|
526
|
+
| Groq | *(none)* | `groq/llama-3.3-70b` | ✓ | | | | | |
|
|
527
|
+
| DeepSeek | *(none)* | `deepseek/deepseek-chat` | ✓ | | | | | |
|
|
528
|
+
| xAI | *(none)* | `xai/grok-3` | ✓ | | | | | |
|
|
529
|
+
| Mistral | *(none)* | `mistral/mistral-large` | ✓ | ✓ | | | | |
|
|
530
|
+
| Azure OpenAI | `openai` | `azure/gpt-4o` | ✓ | | | | | |
|
|
455
531
|
|
|
456
532
|
## Notes
|
|
457
533
|
|
|
@@ -459,3 +535,4 @@ fake.restore()
|
|
|
459
535
|
- `exactOptionalPropertyTypes` compatible
|
|
460
536
|
- All adapters lazy-load their SDK on first use
|
|
461
537
|
- Ollama, Groq, DeepSeek, xAI, Mistral reuse the OpenAI adapter (OpenAI-compatible API)
|
|
538
|
+
- Cohere requires `cohere-ai` SDK; Jina uses direct HTTP (no SDK needed)
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ai-agents
|
|
3
|
+
description: Building AI agents with tools, streaming, conversation memory, approval flows, and middleware in RudderJS
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# AI Agents
|
|
7
|
+
|
|
8
|
+
## When to use this skill
|
|
9
|
+
|
|
10
|
+
Load this skill when you need to build an AI agent, run prompts with tool loops, stream responses, persist conversations, use approval gates, or queue agent work for background execution.
|
|
11
|
+
|
|
12
|
+
## Key concepts
|
|
13
|
+
|
|
14
|
+
- **Agent base class**: Extend `Agent` and implement `instructions()`. Optionally override `model()`, `tools()`, `maxSteps()`, `stopWhen()`, `temperature()`, `middleware()`.
|
|
15
|
+
- **Anonymous agents**: Use the `agent()` function for inline, one-off agents without a class.
|
|
16
|
+
- **Tool loop**: The agent runs a loop: prompt model -> execute tool calls -> feed results back -> repeat until stop condition.
|
|
17
|
+
- **Streaming**: `agent.stream()` returns `{ stream: AsyncIterable<StreamChunk>, response: Promise<AgentResponse> }`.
|
|
18
|
+
- **Conversations**: `agent.forUser(id).prompt()` or `agent.continue(conversationId).prompt()` for persistent memory.
|
|
19
|
+
- **Provider/model string**: Format is `'provider/model'` (e.g. `'anthropic/claude-sonnet-4-5'`, `'openai/gpt-4o'`).
|
|
20
|
+
- **Finish reasons**: `'stop'`, `'tool_calls'`, `'length'`, `'client_tool_calls'`, `'tool_approval_required'`.
|
|
21
|
+
|
|
22
|
+
## Step-by-step
|
|
23
|
+
|
|
24
|
+
### 1. Create an agent class
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// app/Agents/ResearchAgent.ts
|
|
28
|
+
import { Agent } from '@rudderjs/ai'
|
|
29
|
+
import type { HasTools, AnyTool } from '@rudderjs/ai'
|
|
30
|
+
|
|
31
|
+
export class ResearchAgent extends Agent implements HasTools {
|
|
32
|
+
instructions(): string {
|
|
33
|
+
return `You are a research assistant. Use the search tool to find
|
|
34
|
+
information and summarize your findings clearly.`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
model(): string {
|
|
38
|
+
return 'anthropic/claude-sonnet-4-5'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
tools(): AnyTool[] {
|
|
42
|
+
return [searchTool, summarizeTool]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
maxSteps(): number {
|
|
46
|
+
return 10
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Run a prompt (non-streaming)
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const agent = new ResearchAgent()
|
|
55
|
+
|
|
56
|
+
const response = await agent.prompt('What is RudderJS?')
|
|
57
|
+
console.log(response.text) // final text output
|
|
58
|
+
console.log(response.steps) // array of AgentStep
|
|
59
|
+
console.log(response.usage) // { promptTokens, completionTokens, totalTokens }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3. Stream a response
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const { stream, response } = agent.stream('Explain TypeScript decorators')
|
|
66
|
+
|
|
67
|
+
for await (const chunk of stream) {
|
|
68
|
+
switch (chunk.type) {
|
|
69
|
+
case 'text-delta':
|
|
70
|
+
process.stdout.write(chunk.text ?? '')
|
|
71
|
+
break
|
|
72
|
+
case 'tool-call':
|
|
73
|
+
console.log(`Calling tool: ${chunk.toolCall?.name}`)
|
|
74
|
+
break
|
|
75
|
+
case 'tool-result':
|
|
76
|
+
console.log(`Tool result:`, chunk.result)
|
|
77
|
+
break
|
|
78
|
+
case 'tool-update':
|
|
79
|
+
console.log(`Progress:`, chunk.update)
|
|
80
|
+
break
|
|
81
|
+
case 'finish':
|
|
82
|
+
console.log(`Done: ${chunk.finishReason}`)
|
|
83
|
+
break
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const finalResponse = await response
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 4. Use anonymous agents (inline)
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { agent } from '@rudderjs/ai'
|
|
94
|
+
|
|
95
|
+
// Simple string instructions
|
|
96
|
+
const response = await agent('You are a helpful assistant.').prompt('Hello')
|
|
97
|
+
|
|
98
|
+
// With tools and model
|
|
99
|
+
const response = await agent({
|
|
100
|
+
instructions: 'You are a search assistant.',
|
|
101
|
+
tools: [searchTool],
|
|
102
|
+
model: 'anthropic/claude-sonnet-4-5',
|
|
103
|
+
}).prompt('Find users named John')
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 5. Conversation persistence
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const myAgent = new ResearchAgent()
|
|
110
|
+
|
|
111
|
+
// Start a new conversation for a user
|
|
112
|
+
const response1 = await myAgent.forUser('user-123').prompt('What is TypeScript?')
|
|
113
|
+
const convId = response1.conversationId!
|
|
114
|
+
|
|
115
|
+
// Continue the same conversation
|
|
116
|
+
const response2 = await myAgent.continue(convId).prompt('Tell me more about generics')
|
|
117
|
+
// The agent sees the full conversation history
|
|
118
|
+
|
|
119
|
+
// Streaming with conversations
|
|
120
|
+
const { stream, response } = myAgent.forUser('user-123').stream('Explain async/await')
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
A `ConversationStore` must be registered. The built-in `MemoryConversationStore` works for dev; implement the `ConversationStore` interface for production (database-backed).
|
|
124
|
+
|
|
125
|
+
### 6. Stop conditions
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { Agent, stepCountIs, hasToolCall } from '@rudderjs/ai'
|
|
129
|
+
|
|
130
|
+
class MyAgent extends Agent {
|
|
131
|
+
instructions() { return 'You are helpful.' }
|
|
132
|
+
|
|
133
|
+
stopWhen() {
|
|
134
|
+
return [
|
|
135
|
+
stepCountIs(5), // stop after 5 iterations
|
|
136
|
+
hasToolCall('final_answer'), // stop when this tool is called
|
|
137
|
+
]
|
|
138
|
+
// Multiple conditions use OR logic -- stops when any is true
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 7. Per-step control (prepareStep)
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
class AdaptiveAgent extends Agent {
|
|
147
|
+
instructions() { return 'You are helpful.' }
|
|
148
|
+
|
|
149
|
+
prepareStep(ctx: { stepNumber: number; steps: AgentStep[]; messages: AiMessage[] }) {
|
|
150
|
+
if (ctx.stepNumber > 3) {
|
|
151
|
+
return { model: 'anthropic/claude-haiku-3' } // cheaper model for later steps
|
|
152
|
+
}
|
|
153
|
+
return {}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 8. Middleware
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import type { AiMiddleware } from '@rudderjs/ai'
|
|
162
|
+
|
|
163
|
+
const loggingMiddleware: AiMiddleware = {
|
|
164
|
+
name: 'logging',
|
|
165
|
+
onStart(ctx) { console.log(`Agent started, model: ${ctx.model}`) },
|
|
166
|
+
onChunk(ctx, chunk) {
|
|
167
|
+
if (chunk.type === 'text-delta') process.stdout.write(chunk.text ?? '')
|
|
168
|
+
return chunk // return null to suppress the chunk
|
|
169
|
+
},
|
|
170
|
+
onBeforeToolCall(ctx, toolName, args) {
|
|
171
|
+
console.log(`Calling ${toolName}`, args)
|
|
172
|
+
// Return { type: 'skip', result: 'mocked' } to skip execution
|
|
173
|
+
// Return { type: 'abort', reason: 'blocked' } to abort the loop
|
|
174
|
+
},
|
|
175
|
+
onAfterToolCall(ctx, toolName, args, result) {
|
|
176
|
+
console.log(`${toolName} returned`, result)
|
|
177
|
+
},
|
|
178
|
+
onUsage(ctx, usage) {
|
|
179
|
+
console.log(`Tokens: ${usage.totalTokens}`)
|
|
180
|
+
},
|
|
181
|
+
onError(ctx, error) {
|
|
182
|
+
console.error('Agent error:', error)
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
class MyAgent extends Agent implements HasMiddleware {
|
|
187
|
+
instructions() { return 'You are helpful.' }
|
|
188
|
+
middleware() { return [loggingMiddleware] }
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### 9. Queue for background execution
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
const myAgent = new ResearchAgent()
|
|
196
|
+
|
|
197
|
+
// Queue for async processing (requires @rudderjs/queue)
|
|
198
|
+
myAgent.queue('Analyze this dataset').dispatch()
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### 10. Failover providers
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
class ResilientAgent extends Agent {
|
|
205
|
+
instructions() { return 'You are helpful.' }
|
|
206
|
+
model() { return 'anthropic/claude-sonnet-4-5' }
|
|
207
|
+
failover() { return ['openai/gpt-4o', 'google/gemini-2.0-flash'] }
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 11. Attachments (images/documents)
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const response = await agent('Describe this image.').prompt('What do you see?', {
|
|
215
|
+
attachments: [
|
|
216
|
+
{ type: 'image', data: base64String, mimeType: 'image/png' },
|
|
217
|
+
{ type: 'document', data: pdfBase64, mimeType: 'application/pdf', name: 'report.pdf' },
|
|
218
|
+
],
|
|
219
|
+
})
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Examples
|
|
223
|
+
|
|
224
|
+
See `playground/app/Agents/ResearchAgent.ts` for a working agent class.
|
|
225
|
+
|
|
226
|
+
## Common pitfalls
|
|
227
|
+
|
|
228
|
+
- **Provider SDK not installed**: Each provider's SDK is an optional peer dependency. Install only what you use: `@anthropic-ai/sdk`, `openai`, `@google/genai`.
|
|
229
|
+
- **No default model**: If `model()` returns `undefined`, the agent uses the registry default from `config/ai.ts`. Make sure one is configured.
|
|
230
|
+
- **ConversationStore missing**: `forUser()` / `continue()` throw if no `ConversationStore` is registered. Register one via `setConversationStore()` or through the AI service provider.
|
|
231
|
+
- **maxSteps exhaustion**: Default is 20 iterations. If the agent hits `maxSteps`, it stops with whatever text it has. Override `maxSteps()` for agents that need more iterations.
|
|
232
|
+
- **Streaming vs non-streaming tool updates**: `yield` from an `async function*` tool execute emits `tool-update` chunks during streaming. In non-streaming `prompt()`, yields are silently drained.
|
|
233
|
+
- **Client tools**: Tools without an `execute` function are client tools. The loop pauses with `finishReason: 'client_tool_calls'` and returns `pendingClientToolCalls` for browser-side execution.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ai-tools
|
|
3
|
+
description: Defining server and client tools with Zod schemas, approval gates, streaming yields, and modelOutput for RudderJS AI agents
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# AI Tools
|
|
7
|
+
|
|
8
|
+
## When to use this skill
|
|
9
|
+
|
|
10
|
+
Load this skill when you need to define tools for AI agents -- server-side executors, client-side browser tools, streaming generator tools, approval gates, or tools with custom model output formatting.
|
|
11
|
+
|
|
12
|
+
## Key concepts
|
|
13
|
+
|
|
14
|
+
- **toolDefinition()**: Builder function that creates a typed tool from a Zod input schema. Call `.server()` to attach a handler, or leave as-is for a client tool.
|
|
15
|
+
- **Server tools**: Have an `execute` function that runs on the server. Can be a regular async function or an `async function*` generator.
|
|
16
|
+
- **Client tools**: No `execute` -- the agent loop pauses and returns pending tool calls for browser-side execution.
|
|
17
|
+
- **Approval gates**: `needsApproval: true` (or a predicate function) pauses the loop with `tool_approval_required` finish reason.
|
|
18
|
+
- **modelOutput()**: Transform the tool's structured result into a shorter string for the model's context, while the UI still gets the full result.
|
|
19
|
+
- **Tool updates (streaming)**: Generator tools can `yield` progress payloads that surface as `tool-update` stream chunks.
|
|
20
|
+
|
|
21
|
+
## Step-by-step
|
|
22
|
+
|
|
23
|
+
### 1. Basic server tool
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { toolDefinition } from '@rudderjs/ai'
|
|
27
|
+
import { z } from 'zod'
|
|
28
|
+
|
|
29
|
+
const weatherTool = toolDefinition({
|
|
30
|
+
name: 'get_weather',
|
|
31
|
+
description: 'Get current weather for a location',
|
|
32
|
+
inputSchema: z.object({
|
|
33
|
+
location: z.string().describe('City name'),
|
|
34
|
+
units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
|
|
35
|
+
}),
|
|
36
|
+
}).server(async ({ location, units }) => {
|
|
37
|
+
const data = await fetchWeather(location, units)
|
|
38
|
+
return { temp: data.temperature, conditions: data.conditions, unit: units }
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Client tool (browser-side execution)
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// No .server() call -- this is a client tool
|
|
46
|
+
const readClipboardTool = toolDefinition({
|
|
47
|
+
name: 'read_clipboard',
|
|
48
|
+
description: 'Read the contents of the user clipboard',
|
|
49
|
+
inputSchema: z.object({}),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// When the agent calls this tool, the loop pauses with:
|
|
53
|
+
// finishReason: 'client_tool_calls'
|
|
54
|
+
// pendingClientToolCalls: [{ id, name: 'read_clipboard', arguments: {} }]
|
|
55
|
+
// The caller executes it browser-side and resumes with tool results.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Tool with approval gate
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const deleteUserTool = toolDefinition({
|
|
62
|
+
name: 'delete_user',
|
|
63
|
+
description: 'Permanently delete a user account',
|
|
64
|
+
inputSchema: z.object({ userId: z.string() }),
|
|
65
|
+
needsApproval: true, // always requires approval
|
|
66
|
+
}).server(async ({ userId }) => {
|
|
67
|
+
await User.forceDelete(userId)
|
|
68
|
+
return { deleted: true }
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Conditional approval
|
|
72
|
+
const sendEmailTool = toolDefinition({
|
|
73
|
+
name: 'send_email',
|
|
74
|
+
description: 'Send an email to a user',
|
|
75
|
+
inputSchema: z.object({
|
|
76
|
+
to: z.string(),
|
|
77
|
+
subject: z.string(),
|
|
78
|
+
body: z.string(),
|
|
79
|
+
}),
|
|
80
|
+
needsApproval: (input) => input.to.endsWith('@external.com'),
|
|
81
|
+
}).server(async (input) => {
|
|
82
|
+
await sendEmail(input)
|
|
83
|
+
return { sent: true }
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
When approval is required, the loop stops with:
|
|
88
|
+
- `finishReason: 'tool_approval_required'`
|
|
89
|
+
- `pendingApprovalToolCall: { toolCall, isClientTool: false }`
|
|
90
|
+
|
|
91
|
+
Resume by passing `approvedToolCallIds` or `rejectedToolCallIds` in the next prompt options.
|
|
92
|
+
|
|
93
|
+
### 4. Streaming tool with progress yields
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const analyzeDataTool = toolDefinition({
|
|
97
|
+
name: 'analyze_data',
|
|
98
|
+
description: 'Analyze a dataset and return insights',
|
|
99
|
+
inputSchema: z.object({ datasetId: z.string() }),
|
|
100
|
+
}).server(async function* ({ datasetId }) {
|
|
101
|
+
const dataset = await loadDataset(datasetId)
|
|
102
|
+
|
|
103
|
+
yield { progress: 25, message: 'Loading data...' }
|
|
104
|
+
|
|
105
|
+
const cleaned = cleanData(dataset)
|
|
106
|
+
yield { progress: 50, message: 'Cleaning data...' }
|
|
107
|
+
|
|
108
|
+
const analysis = runAnalysis(cleaned)
|
|
109
|
+
yield { progress: 75, message: 'Running analysis...' }
|
|
110
|
+
|
|
111
|
+
const insights = summarize(analysis)
|
|
112
|
+
yield { progress: 100, message: 'Complete' }
|
|
113
|
+
|
|
114
|
+
return { insights, recordCount: dataset.length }
|
|
115
|
+
// Each yield surfaces as a 'tool-update' StreamChunk
|
|
116
|
+
// The return value is the final 'tool-result'
|
|
117
|
+
})
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 5. modelOutput() -- control what the model sees
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const searchTool = toolDefinition({
|
|
124
|
+
name: 'search_documents',
|
|
125
|
+
description: 'Search the document database',
|
|
126
|
+
inputSchema: z.object({ query: z.string() }),
|
|
127
|
+
}).server(async ({ query }) => {
|
|
128
|
+
const results = await searchDb(query)
|
|
129
|
+
return {
|
|
130
|
+
results, // full structured data for the UI
|
|
131
|
+
totalCount: results.length,
|
|
132
|
+
metadata: { /* ... */ },
|
|
133
|
+
}
|
|
134
|
+
}).modelOutput((result) => {
|
|
135
|
+
// The MODEL only sees this condensed string on its next step
|
|
136
|
+
// The UI still receives the full structured result above
|
|
137
|
+
return `Found ${result.totalCount} results: ${result.results.map(r => r.title).join(', ')}`
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 6. Dynamic tools (runtime-defined schemas)
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { dynamicTool } from '@rudderjs/ai'
|
|
145
|
+
|
|
146
|
+
// When the schema isn't known at compile time
|
|
147
|
+
const tool = dynamicTool({
|
|
148
|
+
name: agentDef.slug,
|
|
149
|
+
description: agentDef.description,
|
|
150
|
+
inputSchema: z.object({}),
|
|
151
|
+
}).server(async () => {
|
|
152
|
+
return await agentDef.run()
|
|
153
|
+
})
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 7. Tool with ToolCallContext
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const myTool = toolDefinition({
|
|
160
|
+
name: 'my_tool',
|
|
161
|
+
description: 'A tool that needs its call ID',
|
|
162
|
+
inputSchema: z.object({ data: z.string() }),
|
|
163
|
+
}).server(async (input, ctx) => {
|
|
164
|
+
// ctx.toolCallId is the unique ID the model assigned to this call
|
|
165
|
+
console.log(`Tool call ID: ${ctx?.toolCallId}`)
|
|
166
|
+
return { processed: true }
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 8. Lazy tools (not advertised until needed)
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const secretTool = toolDefinition({
|
|
174
|
+
name: 'admin_panel',
|
|
175
|
+
description: 'Access admin functions',
|
|
176
|
+
inputSchema: z.object({ action: z.string() }),
|
|
177
|
+
lazy: true, // not included in the tool list sent to the model
|
|
178
|
+
}).server(async ({ action }) => {
|
|
179
|
+
// Only callable if the model explicitly names it
|
|
180
|
+
return { result: await adminAction(action) }
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### 9. Pause for client tools (from inside a server tool)
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { pauseForClientTools } from '@rudderjs/ai'
|
|
188
|
+
|
|
189
|
+
const runSubAgentTool = toolDefinition({
|
|
190
|
+
name: 'run_sub_agent',
|
|
191
|
+
description: 'Run a sub-agent that may need browser tools',
|
|
192
|
+
inputSchema: z.object({ task: z.string() }),
|
|
193
|
+
}).server(async function* ({ task }, ctx) {
|
|
194
|
+
const subResponse = await runSubAgent(task)
|
|
195
|
+
|
|
196
|
+
if (subResponse.pendingClientToolCalls?.length) {
|
|
197
|
+
// Pause the parent loop -- surface client tool calls to the browser
|
|
198
|
+
yield pauseForClientTools(subResponse.pendingClientToolCalls, subResponse.resumeId)
|
|
199
|
+
return undefined as never // unreachable after pause
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return subResponse.text
|
|
203
|
+
})
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 10. Using tools with an agent
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { Agent } from '@rudderjs/ai'
|
|
210
|
+
import type { HasTools, AnyTool } from '@rudderjs/ai'
|
|
211
|
+
|
|
212
|
+
class MyAgent extends Agent implements HasTools {
|
|
213
|
+
instructions() { return 'You are a helpful assistant with access to tools.' }
|
|
214
|
+
|
|
215
|
+
tools(): AnyTool[] {
|
|
216
|
+
return [
|
|
217
|
+
weatherTool,
|
|
218
|
+
searchTool,
|
|
219
|
+
analyzeDataTool,
|
|
220
|
+
deleteUserTool,
|
|
221
|
+
]
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Or with the anonymous agent
|
|
226
|
+
const response = await agent({
|
|
227
|
+
instructions: 'You are helpful.',
|
|
228
|
+
tools: [weatherTool, searchTool],
|
|
229
|
+
}).prompt('What is the weather in Paris?')
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Examples
|
|
233
|
+
|
|
234
|
+
Tools are typically defined in `app/Tools/` or co-located with the agent that uses them. See `packages/ai/src/tool.ts` for the full builder API.
|
|
235
|
+
|
|
236
|
+
## Common pitfalls
|
|
237
|
+
|
|
238
|
+
- **Zod schemas required**: Tool input schemas must be Zod objects. They are converted to JSON Schema for each provider automatically.
|
|
239
|
+
- **Generator vs async function**: Use `async function*` only when you need streaming progress yields. For simple tools, use a regular `async` function.
|
|
240
|
+
- **modelOutput is optional**: Only use `.modelOutput()` when the tool returns large structured data that would waste model context. The default behavior is `JSON.stringify` of the result.
|
|
241
|
+
- **Approval flow is two-step**: When a tool needs approval, the loop stops. You must resume with `approvedToolCallIds` or `rejectedToolCallIds` in the next `prompt()` call's options.
|
|
242
|
+
- **Client tool placeholder mode**: By default, client tools without `execute` get a placeholder result and the loop continues. Pass `toolCallStreamingMode: 'stop-on-client-tool'` to pause instead.
|
|
243
|
+
- **exactOptionalPropertyTypes**: If your tsconfig has this enabled, do not pass `undefined` for optional tool parameters -- omit the key entirely.
|
|
244
|
+
- **Tool name conventions**: Use `snake_case` for tool names (e.g. `get_weather`, `search_documents`). This matches what AI models expect.
|
package/dist/agent.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAYpD,OAAO,KAAK,EACV,kBAAkB,EAClB,SAAS,EACT,YAAY,EAEZ,aAAa,EACb,SAAS,EACT,mBAAmB,EACnB,OAAO,EAEP,iBAAiB,EAEjB,aAAa,EACb,QAAQ,EAER,iBAAiB,EAEjB,aAAa,EAQd,MAAM,YAAY,CAAA;AA2BnB,yBAAyB;AACzB,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,aAAa,CAEpD;AAED,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,CAK3D;AAID,8BAAsB,KAAK;IACzB,yCAAyC;IACzC,QAAQ,CAAC,YAAY,IAAI,MAAM;IAE/B,uFAAuF;IACvF,KAAK,IAAI,MAAM,GAAG,SAAS;IAE3B,sCAAsC;IACtC,QAAQ,IAAI,MAAM,EAAE;IAEpB,yDAAyD;IACzD,QAAQ,IAAI,MAAM;IAElB,uEAAuE;IACvE,WAAW,CAAC,CAAC,IAAI,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,EAAE,CAAC;QAAC,QAAQ,EAAE,SAAS,EAAE,CAAA;KAAE,GAAG,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAErI,sDAAsD;IACtD,QAAQ,IAAI,aAAa,GAAG,aAAa,EAAE;IAI3C,wBAAwB;IACxB,WAAW,IAAI,MAAM,GAAG,SAAS;IAEjC,8BAA8B;IAC9B,SAAS,IAAI,MAAM,GAAG,SAAS;IAE/B,kDAAkD;IAC5C,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC;IAIjF,8CAA8C;IAC9C,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,mBAAmB;IAIxE,gDAAgD;IAChD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,mBAAmB;IAIvE,sDAAsD;IACtD,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB;IAIzC,wCAAwC;IACxC,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,gBAAgB;CAGnD;AAID;;;GAGG;AACH,qBAAa,gBAAgB;IAIf,OAAO,CAAC,QAAQ,CAAC,KAAK;IAHlC,OAAO,CAAC,OAAO,CAAoB;IACnC,OAAO,CAAC,eAAe,CAAoB;gBAEd,KAAK,EAAE,KAAK;IAEzC,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAK7B,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;IAKhC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC;IAgCjF,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,mBAAmB;CA0DzE;AA6BD;;;;;;;;;;;;GAYG;AACH,wBAAgB,KAAK,CACnB,qBAAqB,EAAE,MAAM,GAAG;IAC9B,YAAY,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,OAAO,EAAE,GAAG,SAAS,CAAA;IAC7B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,UAAU,CAAC,EAAE,YAAY,EAAE,GAAG,SAAS,CAAA;CACxC,GACA,KAAK,GAAG,QAAQ,GAAG,aAAa,CAKlC;AAQD,iFAAiF;AACjF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAEnE"}
|