@directive-run/claude-plugin 1.16.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/.claude-plugin/plugin.json +6 -0
- package/LICENSE +26 -0
- package/README.md +113 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/skills/README.md +9 -0
- package/skills/building-ai-agents/SKILL.md +414 -0
- package/skills/building-ai-agents/ai-adapters.md +257 -0
- package/skills/building-ai-agents/ai-agents-streaming.md +307 -0
- package/skills/building-ai-agents/ai-communication.md +304 -0
- package/skills/building-ai-agents/api-skeleton.md +5 -0
- package/skills/building-ai-agents/examples.md +849 -0
- package/skills/building-ai-orchestrators/SKILL.md +401 -0
- package/skills/building-ai-orchestrators/ai-multi-agent.md +375 -0
- package/skills/building-ai-orchestrators/ai-orchestrator.md +466 -0
- package/skills/building-ai-orchestrators/ai-tasks.md +268 -0
- package/skills/building-ai-orchestrators/api-skeleton.md +5 -0
- package/skills/building-ai-orchestrators/examples.md +1609 -0
- package/skills/building-directive-systems/SKILL.md +499 -0
- package/skills/building-directive-systems/api-skeleton.md +5 -0
- package/skills/building-directive-systems/examples.md +4149 -0
- package/skills/building-directive-systems/multi-module.md +327 -0
- package/skills/building-directive-systems/plugins.md +361 -0
- package/skills/building-directive-systems/react-adapter.md +376 -0
- package/skills/building-directive-systems/system-api.md +463 -0
- package/skills/getting-started-with-directive/SKILL.md +198 -0
- package/skills/getting-started-with-directive/api-skeleton.md +5 -0
- package/skills/getting-started-with-directive/core-patterns.md +240 -0
- package/skills/getting-started-with-directive/examples.md +87 -0
- package/skills/getting-started-with-directive/sitemap.md +202 -0
- package/skills/hardening-ai-systems/SKILL.md +377 -0
- package/skills/hardening-ai-systems/ai-budget-resilience.md +408 -0
- package/skills/hardening-ai-systems/ai-guardrails-memory.md +370 -0
- package/skills/hardening-ai-systems/ai-security.md +400 -0
- package/skills/hardening-ai-systems/api-skeleton.md +5 -0
- package/skills/hardening-ai-systems/examples.md +628 -0
- package/skills/migrating-to-directive/SKILL.md +249 -0
- package/skills/migrating-to-directive/anti-patterns.md +382 -0
- package/skills/migrating-to-directive/api-skeleton.md +5 -0
- package/skills/migrating-to-directive/core-patterns.md +240 -0
- package/skills/migrating-to-directive/examples.md +511 -0
- package/skills/migrating-to-directive/schema-types.md +273 -0
- package/skills/reviewing-directive-code/SKILL.md +232 -0
- package/skills/reviewing-directive-code/anti-patterns.md +382 -0
- package/skills/reviewing-directive-code/api-skeleton.md +5 -0
- package/skills/reviewing-directive-code/core-patterns.md +240 -0
- package/skills/reviewing-directive-code/examples.md +463 -0
- package/skills/reviewing-directive-code/naming.md +321 -0
- package/skills/scaffolding-directive-modules/SKILL.md +218 -0
- package/skills/scaffolding-directive-modules/api-skeleton.md +5 -0
- package/skills/scaffolding-directive-modules/core-patterns.md +240 -0
- package/skills/scaffolding-directive-modules/examples.md +1386 -0
- package/skills/scaffolding-directive-modules/naming.md +321 -0
- package/skills/scaffolding-directive-modules/schema-types.md +273 -0
- package/skills/testing-ai-systems/SKILL.md +398 -0
- package/skills/testing-ai-systems/ai-debug-observability.md +329 -0
- package/skills/testing-ai-systems/ai-mcp-rag.md +293 -0
- package/skills/testing-ai-systems/ai-testing-evals.md +485 -0
- package/skills/testing-ai-systems/api-skeleton.md +5 -0
- package/skills/testing-ai-systems/examples.md +1261 -0
- package/skills/testing-directive-code/SKILL.md +479 -0
- package/skills/testing-directive-code/api-skeleton.md +5 -0
- package/skills/testing-directive-code/examples.md +156 -0
- package/skills/testing-directive-code/history.md +351 -0
- package/skills/testing-directive-code/testing.md +412 -0
- package/skills/writing-directive-constraints/SKILL.md +486 -0
- package/skills/writing-directive-constraints/api-skeleton.md +5 -0
- package/skills/writing-directive-constraints/constraints.md +274 -0
- package/skills/writing-directive-constraints/error-boundaries.md +330 -0
- package/skills/writing-directive-constraints/examples.md +1361 -0
- package/skills/writing-directive-constraints/resolvers.md +367 -0
- package/skills/writing-directive-modules/SKILL.md +342 -0
- package/skills/writing-directive-modules/anti-patterns.md +382 -0
- package/skills/writing-directive-modules/api-skeleton.md +5 -0
- package/skills/writing-directive-modules/core-patterns.md +240 -0
- package/skills/writing-directive-modules/examples.md +1639 -0
- package/skills/writing-directive-modules/naming.md +321 -0
- package/skills/writing-directive-modules/schema-types.md +273 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: building-ai-agents
|
|
3
|
+
description: "Configure AI provider runners (Anthropic, OpenAI, Ollama), stream tokens and structured output, and wire cross-agent communication. Use when setting up LLM provider connections, implementing token streaming, choosing between providers, or building agents that communicate results to each other."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Building AI Agents
|
|
7
|
+
|
|
8
|
+
## Prerequisites
|
|
9
|
+
|
|
10
|
+
This skill applies when the project uses `@directive-run/ai`. If not found in `package.json`, suggest installing it: `npm install @directive-run/ai`.
|
|
11
|
+
|
|
12
|
+
## When Claude Should Use This Skill
|
|
13
|
+
|
|
14
|
+
## Auto-Invoke Triggers
|
|
15
|
+
- User asks which LLM provider to use or how to set one up
|
|
16
|
+
- User wants to stream tokens or handle streaming responses
|
|
17
|
+
- User mentions `createAnthropicRunner`, `createOpenAIRunner`, `createOllamaRunner`
|
|
18
|
+
- User asks how agents share data or pass results between each other
|
|
19
|
+
- User needs to switch providers or implement a provider abstraction
|
|
20
|
+
|
|
21
|
+
## Exclusions
|
|
22
|
+
- Do NOT invoke for orchestrator structure (constraints/resolvers) – use `building-ai-orchestrators`
|
|
23
|
+
- Do NOT invoke for rate limits, budgets, or guardrails – use `hardening-ai-systems`
|
|
24
|
+
- Do NOT invoke for testing runners – use `testing-ai-systems`
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Quick Reference
|
|
29
|
+
|
|
30
|
+
## Decision Tree: Which Runner?
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
Choosing a provider?
|
|
34
|
+
├── Production, best quality → createAnthropicRunner (claude-opus-4-6)
|
|
35
|
+
├── Production, cost-efficient → createAnthropicRunner (claude-haiku-4-5)
|
|
36
|
+
├── OpenAI ecosystem / GPT models → createOpenAIRunner
|
|
37
|
+
├── Local / private / no API key → createOllamaRunner
|
|
38
|
+
└── Custom provider → implement AgentRunner interface
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Decision Tree: Streaming vs Non-Streaming?
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Need the response immediately as it arrives?
|
|
45
|
+
├── Yes, show tokens as they generate → runStream() + "token" events
|
|
46
|
+
├── Yes, but structured JSON → runStream() + "structured" events
|
|
47
|
+
└── No, wait for full response → run()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Provider Runners
|
|
53
|
+
|
|
54
|
+
### Anthropic
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { createAnthropicRunner } from "@directive-run/ai/anthropic";
|
|
58
|
+
|
|
59
|
+
const runner = createAnthropicRunner({
|
|
60
|
+
model: "claude-opus-4-6", // or "claude-haiku-4-5", "claude-sonnet-4-6"
|
|
61
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
62
|
+
maxTokens: 4096, // optional, default: 1024
|
|
63
|
+
temperature: 0.7, // optional, default: 1.0
|
|
64
|
+
systemPrompt: "You are helpful.", // optional default system prompt
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### OpenAI
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { createOpenAIRunner } from "@directive-run/ai/openai";
|
|
72
|
+
|
|
73
|
+
const runner = createOpenAIRunner({
|
|
74
|
+
model: "gpt-4o", // or "gpt-4o-mini", "gpt-3.5-turbo"
|
|
75
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
76
|
+
maxTokens: 4096,
|
|
77
|
+
temperature: 0.7,
|
|
78
|
+
organization: "org-...", // optional
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Ollama (Local)
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { createOllamaRunner } from "@directive-run/ai/ollama";
|
|
86
|
+
|
|
87
|
+
const runner = createOllamaRunner({
|
|
88
|
+
model: "llama3.2", // any Ollama-installed model
|
|
89
|
+
baseUrl: "http://localhost:11434", // default Ollama endpoint
|
|
90
|
+
temperature: 0.8,
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Running LLM Calls Inside Resolvers
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
resolvers: {
|
|
100
|
+
generate: {
|
|
101
|
+
requirement: "GENERATE",
|
|
102
|
+
resolve: async (req, context) => {
|
|
103
|
+
// Basic call – returns full response when complete
|
|
104
|
+
const result = await context.runner.run({
|
|
105
|
+
prompt: "Explain quantum entanglement simply.",
|
|
106
|
+
system: "You are a science communicator.",
|
|
107
|
+
maxTokens: 500,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
context.facts.output = result.text;
|
|
111
|
+
context.facts.tokenUsage = result.usage.totalTokens;
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Runner Result Shape
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
interface RunnerResult {
|
|
121
|
+
text: string; // Generated text
|
|
122
|
+
usage: {
|
|
123
|
+
inputTokens: number;
|
|
124
|
+
outputTokens: number;
|
|
125
|
+
totalTokens: number;
|
|
126
|
+
};
|
|
127
|
+
model: string; // Model used
|
|
128
|
+
stopReason: "end_turn" | "max_tokens" | "stop_sequence";
|
|
129
|
+
raw: unknown; // Raw provider response
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Streaming
|
|
136
|
+
|
|
137
|
+
### Token Streaming
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// orchestrator.runStream() returns AsyncIterable<OrchestratorEvent>
|
|
141
|
+
const stream = orchestrator.runStream({ input: "Write a story about..." });
|
|
142
|
+
|
|
143
|
+
for await (const event of stream) {
|
|
144
|
+
if (event.type === "token") {
|
|
145
|
+
// Arrives as the LLM generates – display immediately
|
|
146
|
+
process.stdout.write(event.text);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (event.type === "done") {
|
|
150
|
+
console.log("\nFinal facts:", event.facts);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (event.type === "error") {
|
|
154
|
+
console.error("Stream error:", event.error.message);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### All Stream Event Types
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
type OrchestratorEvent =
|
|
164
|
+
| { type: "token"; text: string; agentId?: string }
|
|
165
|
+
| { type: "requirement_met"; requirementType: string; agentId?: string }
|
|
166
|
+
| { type: "requirement_queued"; requirementType: string }
|
|
167
|
+
| { type: "agent_started"; agentId: string }
|
|
168
|
+
| { type: "agent_done"; agentId: string }
|
|
169
|
+
| { type: "done"; facts: Record<string, unknown> }
|
|
170
|
+
| { type: "error"; error: Error };
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Structured Output Streaming
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { z } from "zod";
|
|
177
|
+
|
|
178
|
+
// When you need typed JSON output, not raw text
|
|
179
|
+
const result = await context.runner.runStructured({
|
|
180
|
+
prompt: "Extract product info from this description: ...",
|
|
181
|
+
schema: z.object({
|
|
182
|
+
name: z.string(),
|
|
183
|
+
price: z.number(),
|
|
184
|
+
inStock: z.boolean(),
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// result.data is typed to the schema
|
|
189
|
+
context.facts.productName = result.data.name;
|
|
190
|
+
context.facts.price = result.data.price;
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Emitting Custom Stream Events from Resolvers
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
resolve: async (req, context) => {
|
|
197
|
+
// Emit custom events consumers can observe
|
|
198
|
+
context.emit({ type: "token", text: "Starting analysis..." });
|
|
199
|
+
|
|
200
|
+
const result = await context.runner.run({ prompt: req.input });
|
|
201
|
+
|
|
202
|
+
context.emit({ type: "token", text: result.text });
|
|
203
|
+
context.facts.output = result.text;
|
|
204
|
+
},
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Cross-Agent Communication
|
|
210
|
+
|
|
211
|
+
### Via Coordinator Facts (Recommended)
|
|
212
|
+
|
|
213
|
+
In `createMultiAgentOrchestrator`, agents share coordinator facts. The coordinator acts as the message bus.
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// researcher agent writes to coordinator facts
|
|
217
|
+
resolve: async (req, context) => {
|
|
218
|
+
const result = await context.runner.run({ prompt: "Research AI trends..." });
|
|
219
|
+
// Writing to shared coordinator facts signals the next agent
|
|
220
|
+
context.facts.researchComplete = true;
|
|
221
|
+
context.facts.researchNotes = result.text;
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
// writer agent reads coordinator facts set by researcher
|
|
225
|
+
constraints: {
|
|
226
|
+
startWriting: {
|
|
227
|
+
// Only activates after researcher signals completion
|
|
228
|
+
when: (facts) => facts.researchComplete && !!facts.researchNotes,
|
|
229
|
+
require: { type: "WRITE_ARTICLE" },
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Via Requirement Payload
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// Pass structured data in the requirement itself
|
|
238
|
+
constraints: {
|
|
239
|
+
routeToSpecialist: {
|
|
240
|
+
when: (facts) => facts.analysisType === "legal",
|
|
241
|
+
require: (facts) => ({
|
|
242
|
+
type: "SPECIALIST_REVIEW",
|
|
243
|
+
category: facts.analysisType,
|
|
244
|
+
priority: facts.urgency,
|
|
245
|
+
content: facts.draftOutput,
|
|
246
|
+
}),
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
resolvers: {
|
|
251
|
+
specialistReview: {
|
|
252
|
+
requirement: "SPECIALIST_REVIEW",
|
|
253
|
+
resolve: async (req, context) => {
|
|
254
|
+
// req contains { category, priority, content } from above
|
|
255
|
+
const result = await context.runner.run({
|
|
256
|
+
prompt: `Review this ${req.category} content: ${req.content}`,
|
|
257
|
+
});
|
|
258
|
+
context.facts.reviewedOutput = result.text;
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Custom Provider – Implement AgentRunner Interface
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
import type { AgentRunner, RunOptions, RunnerResult } from "@directive-run/ai";
|
|
270
|
+
|
|
271
|
+
class MyCustomRunner implements AgentRunner {
|
|
272
|
+
async run(options: RunOptions): Promise<RunnerResult> {
|
|
273
|
+
// Call your provider
|
|
274
|
+
const response = await myProviderClient.generate({
|
|
275
|
+
prompt: options.prompt,
|
|
276
|
+
system: options.system,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
text: response.text,
|
|
281
|
+
usage: {
|
|
282
|
+
inputTokens: response.usage.input,
|
|
283
|
+
outputTokens: response.usage.output,
|
|
284
|
+
totalTokens: response.usage.total,
|
|
285
|
+
},
|
|
286
|
+
model: "my-custom-model",
|
|
287
|
+
stopReason: "end_turn",
|
|
288
|
+
raw: response,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Optional: implement for streaming support
|
|
293
|
+
async *runStream(options: RunOptions): AsyncIterable<string> {
|
|
294
|
+
for await (const chunk of myProviderClient.stream(options)) {
|
|
295
|
+
yield chunk.text;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Multi-Turn Conversations
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
// Maintain conversation history in facts
|
|
307
|
+
factsSchema: {
|
|
308
|
+
messages: t.array(t.object<{ role: "user" | "assistant"; content: string }>()),
|
|
309
|
+
latestInput: t.string(),
|
|
310
|
+
latestOutput: t.string().optional(),
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
init: (facts) => {
|
|
314
|
+
facts.messages = [];
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
resolve: async (req, context) => {
|
|
318
|
+
// Add user message to history
|
|
319
|
+
context.facts.messages.push({
|
|
320
|
+
role: "user",
|
|
321
|
+
content: context.facts.latestInput,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const result = await context.runner.run({
|
|
325
|
+
messages: context.facts.messages, // Pass full history
|
|
326
|
+
system: "You are a helpful assistant.",
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Add assistant reply to history
|
|
330
|
+
context.facts.messages.push({
|
|
331
|
+
role: "assistant",
|
|
332
|
+
content: result.text,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
context.facts.latestOutput = result.text;
|
|
336
|
+
},
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Critical Anti-Patterns
|
|
342
|
+
|
|
343
|
+
## Not handling stream errors
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
// WRONG – if stream throws, it crashes uncaught
|
|
347
|
+
for await (const event of orchestrator.runStream(input)) {
|
|
348
|
+
if (event.type === "token") {
|
|
349
|
+
process.stdout.write(event.text);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// CORRECT – always handle error events
|
|
354
|
+
for await (const event of orchestrator.runStream(input)) {
|
|
355
|
+
if (event.type === "token") {
|
|
356
|
+
process.stdout.write(event.text);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (event.type === "error") {
|
|
360
|
+
console.error("Stream failed:", event.error.message);
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Blocking on stream consumption inside a resolver
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
// WRONG – consuming a stream inside a resolver blocks the engine
|
|
370
|
+
resolve: async (req, context) => {
|
|
371
|
+
const stream = orchestrator.runStream(req.input);
|
|
372
|
+
for await (const event of stream) { /* deadlock risk */ }
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
// CORRECT – use context.runner.run() inside resolvers
|
|
376
|
+
resolve: async (req, context) => {
|
|
377
|
+
const result = await context.runner.run({ prompt: req.input });
|
|
378
|
+
context.facts.output = result.text;
|
|
379
|
+
},
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Resolver parameter naming
|
|
383
|
+
Always use `(req, context)` – never `(req, ctx)` or `(request, context)`.
|
|
384
|
+
|
|
385
|
+
## Hardcoding API keys
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
// WRONG
|
|
389
|
+
const runner = createAnthropicRunner({ apiKey: "sk-ant-..." });
|
|
390
|
+
|
|
391
|
+
// CORRECT
|
|
392
|
+
const runner = createAnthropicRunner({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## Ignoring token usage
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
// WRONG – fire and forget, no budget awareness
|
|
399
|
+
const result = await context.runner.run({ prompt });
|
|
400
|
+
context.facts.output = result.text;
|
|
401
|
+
|
|
402
|
+
// CORRECT – track usage for budget management
|
|
403
|
+
const result = await context.runner.run({ prompt });
|
|
404
|
+
context.facts.output = result.text;
|
|
405
|
+
context.facts.tokensUsed = (context.facts.tokensUsed ?? 0) + result.usage.totalTokens;
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Reference Files
|
|
411
|
+
|
|
412
|
+
- `ai-agents-streaming.md` – Streaming events, AsyncIterable patterns, token buffering
|
|
413
|
+
- `ai-adapters.md` – Provider adapter interface, all runner options, custom runner guide
|
|
414
|
+
- `ai-communication.md` – Cross-agent communication, coordinator facts, requirement payloads
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# AI Adapters
|
|
2
|
+
|
|
3
|
+
> Covers `@directive-run/ai/{openai,anthropic,ollama,gemini}` — provider runners that normalize to the `AgentRunner` interface (zero SDK deps).
|
|
4
|
+
|
|
5
|
+
Adapters connect the orchestrator to LLM providers. Each adapter normalizes provider-specific APIs into Directive's `AgentRunner` interface.
|
|
6
|
+
|
|
7
|
+
## Decision Tree: "Which adapter do I need?"
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Which LLM provider?
|
|
11
|
+
├── Anthropic (Claude) → createAnthropicRunner from '@directive-run/ai/anthropic'
|
|
12
|
+
├── OpenAI (GPT) → createOpenAIRunner from '@directive-run/ai/openai'
|
|
13
|
+
├── Google (Gemini) → createGeminiRunner from '@directive-run/ai/gemini'
|
|
14
|
+
└── Ollama (local) → createOllamaRunner from '@directive-run/ai/ollama'
|
|
15
|
+
│
|
|
16
|
+
Need streaming?
|
|
17
|
+
├── Yes → create*StreamingRunner from the same subpath
|
|
18
|
+
└── No → create*Runner is sufficient
|
|
19
|
+
│
|
|
20
|
+
Need a proxy/self-hosted URL?
|
|
21
|
+
└── Yes → pass baseURL option
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## CRITICAL: Subpath Imports
|
|
25
|
+
|
|
26
|
+
Every adapter MUST be imported from its subpath. The main `@directive-run/ai` entry does NOT export adapters.
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// CORRECT – subpath imports
|
|
30
|
+
import { createAnthropicRunner } from "@directive-run/ai/anthropic";
|
|
31
|
+
import { createOpenAIRunner } from "@directive-run/ai/openai";
|
|
32
|
+
import { createOllamaRunner } from "@directive-run/ai/ollama";
|
|
33
|
+
import { createGeminiRunner } from "@directive-run/ai/gemini";
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Anti-Pattern #26: Importing adapters from the main entry
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// WRONG – adapters are NOT exported from the main package
|
|
40
|
+
import { createOpenAIRunner } from "@directive-run/ai";
|
|
41
|
+
import { createAnthropicRunner } from "@directive-run/ai";
|
|
42
|
+
|
|
43
|
+
// CORRECT – use subpath imports
|
|
44
|
+
import { createOpenAIRunner } from "@directive-run/ai/openai";
|
|
45
|
+
import { createAnthropicRunner } from "@directive-run/ai/anthropic";
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Anthropic Adapter
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import {
|
|
52
|
+
createAnthropicRunner,
|
|
53
|
+
createAnthropicStreamingRunner,
|
|
54
|
+
} from "@directive-run/ai/anthropic";
|
|
55
|
+
|
|
56
|
+
// Standard runner
|
|
57
|
+
const runner = createAnthropicRunner({
|
|
58
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
59
|
+
defaultModel: "claude-sonnet-4-5",
|
|
60
|
+
maxTokens: 4096,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Streaming runner
|
|
64
|
+
const streamingRunner = createAnthropicStreamingRunner({
|
|
65
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
66
|
+
defaultModel: "claude-sonnet-4-5",
|
|
67
|
+
maxTokens: 4096,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// With proxy URL
|
|
71
|
+
const proxiedRunner = createAnthropicRunner({
|
|
72
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
73
|
+
baseURL: "https://my-proxy.example.com/v1",
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## OpenAI Adapter
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import {
|
|
81
|
+
createOpenAIRunner,
|
|
82
|
+
createOpenAIStreamingRunner,
|
|
83
|
+
} from "@directive-run/ai/openai";
|
|
84
|
+
|
|
85
|
+
const runner = createOpenAIRunner({
|
|
86
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
87
|
+
defaultModel: "gpt-4o",
|
|
88
|
+
organization: "org-xxx", // Optional
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Azure OpenAI
|
|
92
|
+
const azureRunner = createOpenAIRunner({
|
|
93
|
+
apiKey: process.env.AZURE_OPENAI_KEY,
|
|
94
|
+
baseURL: "https://my-instance.openai.azure.com/openai/deployments/gpt-4o",
|
|
95
|
+
defaultHeaders: {
|
|
96
|
+
"api-version": "2024-02-01",
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Ollama Adapter (Local Models)
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import {
|
|
105
|
+
createOllamaRunner,
|
|
106
|
+
createOllamaStreamingRunner,
|
|
107
|
+
} from "@directive-run/ai/ollama";
|
|
108
|
+
|
|
109
|
+
const runner = createOllamaRunner({
|
|
110
|
+
// Default: http://localhost:11434
|
|
111
|
+
baseURL: "http://localhost:11434",
|
|
112
|
+
defaultModel: "llama3.1",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Remote Ollama instance
|
|
116
|
+
const remoteRunner = createOllamaRunner({
|
|
117
|
+
baseURL: "https://ollama.my-server.com",
|
|
118
|
+
defaultModel: "mistral",
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Gemini Adapter
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import {
|
|
126
|
+
createGeminiRunner,
|
|
127
|
+
createGeminiStreamingRunner,
|
|
128
|
+
} from "@directive-run/ai/gemini";
|
|
129
|
+
|
|
130
|
+
const runner = createGeminiRunner({
|
|
131
|
+
apiKey: process.env.GOOGLE_API_KEY,
|
|
132
|
+
defaultModel: "gemini-2.0-flash",
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Token Normalization
|
|
137
|
+
|
|
138
|
+
### Anti-Pattern #27: Assuming provider-specific token structure
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// WRONG – Anthropic returns { input_tokens, output_tokens } natively
|
|
142
|
+
// but adapters normalize this
|
|
143
|
+
const result = await runner.run(agent, prompt);
|
|
144
|
+
console.log(result.tokenUsage.input_tokens); // undefined!
|
|
145
|
+
|
|
146
|
+
// CORRECT – adapters normalize to camelCase
|
|
147
|
+
const result = await runner.run(agent, prompt);
|
|
148
|
+
console.log(result.tokenUsage.inputTokens); // number
|
|
149
|
+
console.log(result.tokenUsage.outputTokens); // number
|
|
150
|
+
console.log(result.totalTokens); // inputTokens + outputTokens
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
All adapters normalize token usage to the same shape regardless of provider:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
interface NormalizedTokenUsage {
|
|
157
|
+
inputTokens: number;
|
|
158
|
+
outputTokens: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// result.totalTokens = inputTokens + outputTokens
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Adapter Hooks
|
|
165
|
+
|
|
166
|
+
Every adapter supports lifecycle hooks for logging, metrics, or request modification:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const runner = createAnthropicRunner({
|
|
170
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
171
|
+
hooks: {
|
|
172
|
+
// Called before every LLM API call
|
|
173
|
+
onBeforeCall: (agent, prompt, options) => {
|
|
174
|
+
console.log(`Calling ${agent.model} for ${agent.name}`);
|
|
175
|
+
metrics.increment("llm.calls");
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
// Called after every successful LLM API call
|
|
179
|
+
onAfterCall: (agent, result) => {
|
|
180
|
+
console.log(`${agent.name}: ${result.totalTokens} tokens`);
|
|
181
|
+
metrics.histogram("llm.tokens", result.totalTokens);
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// Called on LLM API errors
|
|
185
|
+
onError: (agent, error) => {
|
|
186
|
+
console.error(`${agent.name} failed:`, error.message);
|
|
187
|
+
metrics.increment("llm.errors");
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Using Adapters with Orchestrators
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { createAgentOrchestrator } from "@directive-run/ai";
|
|
197
|
+
import { createAnthropicRunner } from "@directive-run/ai/anthropic";
|
|
198
|
+
|
|
199
|
+
const runner = createAnthropicRunner({
|
|
200
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Single-agent
|
|
204
|
+
const orchestrator = createAgentOrchestrator({ runner });
|
|
205
|
+
|
|
206
|
+
// Multi-agent – same runner shared across all agents
|
|
207
|
+
const multiOrchestrator = createMultiAgentOrchestrator({
|
|
208
|
+
agents: { /* ... */ },
|
|
209
|
+
runner,
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Switching Adapters
|
|
214
|
+
|
|
215
|
+
Adapters are interchangeable. Switch providers by changing the import and config:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// Before: Anthropic
|
|
219
|
+
import { createAnthropicRunner } from "@directive-run/ai/anthropic";
|
|
220
|
+
const runner = createAnthropicRunner({
|
|
221
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// After: OpenAI – same orchestrator, different runner
|
|
225
|
+
import { createOpenAIRunner } from "@directive-run/ai/openai";
|
|
226
|
+
const runner = createOpenAIRunner({
|
|
227
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Orchestrator code remains unchanged
|
|
231
|
+
const orchestrator = createAgentOrchestrator({ runner });
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Custom BaseURL Patterns
|
|
235
|
+
|
|
236
|
+
| Provider | Default BaseURL | Custom Use Case |
|
|
237
|
+
|---|---|---|
|
|
238
|
+
| Anthropic | `https://api.anthropic.com` | Corporate proxy, VPN relay |
|
|
239
|
+
| OpenAI | `https://api.openai.com/v1` | Azure OpenAI, LiteLLM proxy |
|
|
240
|
+
| Ollama | `http://localhost:11434` | Remote GPU server |
|
|
241
|
+
| Gemini | `https://generativelanguage.googleapis.com` | Regional endpoint |
|
|
242
|
+
|
|
243
|
+
## Quick Reference
|
|
244
|
+
|
|
245
|
+
| Adapter | Import Path | Key Options |
|
|
246
|
+
|---|---|---|
|
|
247
|
+
| Anthropic | `@directive-run/ai/anthropic` | `apiKey`, `defaultModel`, `maxTokens` |
|
|
248
|
+
| OpenAI | `@directive-run/ai/openai` | `apiKey`, `defaultModel`, `organization` |
|
|
249
|
+
| Ollama | `@directive-run/ai/ollama` | `baseURL`, `defaultModel` |
|
|
250
|
+
| Gemini | `@directive-run/ai/gemini` | `apiKey`, `defaultModel` |
|
|
251
|
+
|
|
252
|
+
All adapters support: `baseURL`, `defaultHeaders`, `hooks`, streaming variant.
|
|
253
|
+
|
|
254
|
+
## See also
|
|
255
|
+
|
|
256
|
+
- [`ai-orchestrator.md`](./ai-orchestrator.md) — where the `AgentRunner` an adapter produces gets consumed
|
|
257
|
+
- [`ai-agents-streaming.md`](./ai-agents-streaming.md) — the `StreamingCallbackRunner` shape each adapter's streaming variant satisfies
|