@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,307 @@
|
|
|
1
|
+
# AI agents + streaming
|
|
2
|
+
|
|
3
|
+
> Covers `@directive-run/ai` — `AgentLike`, `RunResult`, `StreamChunk`, backpressure, `createStreamingRunner`, `createSSETransport`.
|
|
4
|
+
|
|
5
|
+
Defines the `AgentLike` shape, the `RunResult` returned by every runner, the `StreamChunk` discriminated union, backpressure strategies, the streaming runner wrapper, and the SSE transport for piping tokens to a browser.
|
|
6
|
+
|
|
7
|
+
## Decision tree
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Need the complete result?
|
|
11
|
+
├── Yes → orchestrator.run(agent, prompt) → Promise<RunResult>
|
|
12
|
+
└── No, need incremental output
|
|
13
|
+
├── Async-iterator stream → orchestrator.runStream(agent, prompt) → { stream, result, abort }
|
|
14
|
+
├── Wrap a base runner → createStreamingRunner(baseRunner, opts) → StreamRunner
|
|
15
|
+
└── Server-Sent Events to HTTP → createSSETransport(config) → { toResponse, toStream }
|
|
16
|
+
|
|
17
|
+
Backpressure concern?
|
|
18
|
+
├── Consumer is slow → backpressure: "buffer" (default)
|
|
19
|
+
├── Need every token → backpressure: "block"
|
|
20
|
+
└── Real-time, can drop → backpressure: "drop"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## `AgentLike`
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
interface AgentLike {
|
|
27
|
+
name: string; // required — unique identifier
|
|
28
|
+
instructions?: string; // system prompt
|
|
29
|
+
model?: string; // adapter-specific model id
|
|
30
|
+
tools?: unknown[]; // tools the agent can call
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const agent: AgentLike = {
|
|
34
|
+
name: "analyst",
|
|
35
|
+
instructions: "You analyze data and provide insights.",
|
|
36
|
+
model: "claude-sonnet-4-5",
|
|
37
|
+
tools: [searchTool, calculatorTool],
|
|
38
|
+
};
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## `RunResult<T>`
|
|
42
|
+
|
|
43
|
+
Every runner — wrapped, mocked, real — resolves to this shape.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
interface RunResult<T = unknown> {
|
|
47
|
+
output: T; // the agent's final output
|
|
48
|
+
messages: Message[]; // full message history from this run
|
|
49
|
+
toolCalls: ToolCall[]; // tool calls executed during this run
|
|
50
|
+
totalTokens: number; // cumulative tokens
|
|
51
|
+
tokenUsage?: TokenUsage; // optional input/output breakdown when the provider supplies it
|
|
52
|
+
isCached?: boolean; // true when served from a semantic cache hit
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface TokenUsage {
|
|
56
|
+
inputTokens: number;
|
|
57
|
+
outputTokens: number;
|
|
58
|
+
// Note: NO `total` field — sum `inputTokens + outputTokens` when needed, or use `result.totalTokens`.
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## `StreamChunk` discriminated union
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
type StreamChunk =
|
|
66
|
+
| { type: "token"; data: string; tokenCount: number } // a text token from the model
|
|
67
|
+
| { type: "tool_start"; tool: string; toolCallId: string; arguments: string } // tool started executing
|
|
68
|
+
| { type: "tool_end"; tool: string; toolCallId: string; result: string } // tool finished
|
|
69
|
+
| { type: "message"; message: Message } // a complete message added to history
|
|
70
|
+
| { type: "guardrail_triggered"; guardrailName: string; reason: string; stopped: boolean } // a guardrail fired during streaming
|
|
71
|
+
| { type: "progress"; phase: "starting" | "generating" | "tool_calling" | "finishing" } // coarse progress
|
|
72
|
+
| { type: "done"; totalTokens: number; duration: number; droppedTokens: number } // stream complete
|
|
73
|
+
| { type: "error"; error: Error }; // stream aborted with an error
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Consuming `runStream`
|
|
77
|
+
|
|
78
|
+
`orchestrator.runStream(agent, input, options?)` returns **`OrchestratorStreamResult<T>`** — a `{ stream, result, abort }` triple, NOT an `AsyncIterable` directly. Destructure it before iterating.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const { stream, result, abort } = orchestrator.runStream(agent, "Write a report");
|
|
82
|
+
|
|
83
|
+
for await (const chunk of stream) {
|
|
84
|
+
switch (chunk.type) {
|
|
85
|
+
case "token":
|
|
86
|
+
process.stdout.write(chunk.data);
|
|
87
|
+
break;
|
|
88
|
+
case "tool_start":
|
|
89
|
+
console.log(`\ntool: ${chunk.tool}`);
|
|
90
|
+
break;
|
|
91
|
+
case "tool_end":
|
|
92
|
+
console.log(`result: ${chunk.result.slice(0, 100)}`);
|
|
93
|
+
break;
|
|
94
|
+
case "guardrail_triggered":
|
|
95
|
+
console.warn(`guardrail ${chunk.guardrailName}: ${chunk.reason}`);
|
|
96
|
+
if (chunk.stopped) {
|
|
97
|
+
console.error("stream stopped by guardrail");
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
case "done":
|
|
101
|
+
console.log(`\ntokens: ${chunk.totalTokens}, ${chunk.duration}ms`);
|
|
102
|
+
break;
|
|
103
|
+
case "error":
|
|
104
|
+
console.error("stream error:", chunk.error);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const final = await result; // RunResult<T>
|
|
110
|
+
|
|
111
|
+
// Cancel mid-stream
|
|
112
|
+
abort();
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Backpressure strategies
|
|
116
|
+
|
|
117
|
+
Configure how the stream behaves when the consumer can't keep up. Pass via `runStream`'s `options` (orchestrator-side) or via the `StreamRunOptions` if you're calling a `StreamRunner` directly.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const { stream, result } = orchestrator.runStream(agent, "Generate a long report", {
|
|
121
|
+
signal: abortController.signal,
|
|
122
|
+
backpressure: "buffer", // default — buffer all tokens
|
|
123
|
+
// backpressure: "block" // pause generation until consumer catches up
|
|
124
|
+
// backpressure: "drop" // drop unprocessed tokens; `done.droppedTokens` reports the count
|
|
125
|
+
bufferSize: 1000,
|
|
126
|
+
stopOnGuardrail: true,
|
|
127
|
+
guardrailCheckInterval: 100,
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
| Strategy | Behavior | Use when |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| `"buffer"` | Buffers all tokens in memory | Consumer is slightly slow; memory is available |
|
|
134
|
+
| `"block"` | Pauses model generation | Consumer must process every token |
|
|
135
|
+
| `"drop"` | Drops unprocessed tokens | Real-time display; some loss acceptable |
|
|
136
|
+
|
|
137
|
+
## `createStreamingRunner(baseRunner, options?)`
|
|
138
|
+
|
|
139
|
+
Wrap a base streaming runner (a `StreamingCallbackRunner` — the callback-based adapter interface) into a `StreamRunner` that produces the async-iterator chunks shown above. The factory is `createStreamingRunner`, NOT `createStreamingCallbackRunner` (the "callback" form is the INPUT to this wrapper, not a separate factory).
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { createStreamingRunner, type StreamingCallbackRunner } from "@directive-run/ai";
|
|
143
|
+
|
|
144
|
+
// The base runner is callback-driven. You supply this from your provider adapter.
|
|
145
|
+
const callbackBased: StreamingCallbackRunner = (agent, input, { onToken, onToolStart, onToolEnd, onComplete, signal }) => {
|
|
146
|
+
// … call your provider's streaming API; invoke the callbacks as tokens arrive
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const streamRunner = createStreamingRunner(callbackBased, {
|
|
150
|
+
streamingGuardrails: [],
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Use it directly — returns the same { stream, result, abort } shape
|
|
154
|
+
const { stream, result, abort } = streamRunner(agent, "prompt", { backpressure: "buffer" });
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## `createSSETransport(config?)`
|
|
158
|
+
|
|
159
|
+
Pipes a token stream to Server-Sent Events for browser consumption. `createSSEResponse` does NOT exist — the real factory is `createSSETransport`, and it returns `{ toResponse, toStream }`.
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { createSSETransport } from "@directive-run/ai";
|
|
163
|
+
|
|
164
|
+
const sse = createSSETransport({
|
|
165
|
+
maxResponseChars: 50_000,
|
|
166
|
+
truncationMessage: "\n\n*[Response truncated]*",
|
|
167
|
+
heartbeatIntervalMs: 15_000,
|
|
168
|
+
headers: { "X-AI-Service": "directive" },
|
|
169
|
+
errorMessages: { rate_limit: "Service busy — please retry shortly." },
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Modern web frameworks (Hono, Next.js, Bun, Deno) — return a Response directly
|
|
173
|
+
export async function POST(req: Request) {
|
|
174
|
+
const { prompt } = await req.json();
|
|
175
|
+
const { stream } = orchestrator.runStream(agent, prompt);
|
|
176
|
+
|
|
177
|
+
return sse.toResponse(stream, agent.name, prompt);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Express / Koa — write to res via the ReadableStream
|
|
181
|
+
app.post("/api/chat", async (req, res) => {
|
|
182
|
+
const { stream } = orchestrator.runStream(agent, req.body.prompt);
|
|
183
|
+
const readable = sse.toStream(stream, agent.name, req.body.prompt);
|
|
184
|
+
|
|
185
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
186
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
187
|
+
res.setHeader("Connection", "keep-alive");
|
|
188
|
+
|
|
189
|
+
for await (const chunk of readable) {
|
|
190
|
+
res.write(chunk);
|
|
191
|
+
}
|
|
192
|
+
res.end();
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Client-side consumption is plain SSE:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
const eventSource = new EventSource("/api/chat");
|
|
200
|
+
|
|
201
|
+
eventSource.addEventListener("text", (event) => {
|
|
202
|
+
const data = JSON.parse(event.data) as { type: "text"; text: string };
|
|
203
|
+
appendToDisplay(data.text);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
eventSource.addEventListener("done", () => eventSource.close());
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Anti-patterns
|
|
210
|
+
|
|
211
|
+
### Iterating `runStream` directly
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
// WRONG — runStream returns { stream, result, abort }, not the iterator itself
|
|
215
|
+
const stream = orchestrator.runStream(agent, prompt);
|
|
216
|
+
for await (const chunk of stream) { /* won't iterate the right thing */ }
|
|
217
|
+
|
|
218
|
+
// CORRECT — destructure
|
|
219
|
+
const { stream, result, abort } = orchestrator.runStream(agent, prompt);
|
|
220
|
+
for await (const chunk of stream) { /* … */ }
|
|
221
|
+
const final = await result;
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Importing `createStreamingCallbackRunner`
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// WRONG — no factory by that name
|
|
228
|
+
import { createStreamingCallbackRunner } from "@directive-run/ai";
|
|
229
|
+
|
|
230
|
+
// CORRECT — wrap a callback-based runner with createStreamingRunner
|
|
231
|
+
import { createStreamingRunner } from "@directive-run/ai";
|
|
232
|
+
const wrapped = createStreamingRunner(callbackBasedRunner, { streamingGuardrails: [] });
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Importing `createSSEResponse`
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// WRONG — no factory by that name
|
|
239
|
+
import { createSSEResponse } from "@directive-run/ai";
|
|
240
|
+
const sse = createSSEResponse(stream);
|
|
241
|
+
|
|
242
|
+
// CORRECT — createSSETransport returns { toResponse, toStream }
|
|
243
|
+
import { createSSETransport } from "@directive-run/ai";
|
|
244
|
+
const sse = createSSETransport({ heartbeatIntervalMs: 15_000 });
|
|
245
|
+
return sse.toResponse(stream, agentId, prompt);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Reading `tokenUsage.total`
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
// WRONG — TokenUsage has no `total` field
|
|
252
|
+
console.log(result.tokenUsage?.total);
|
|
253
|
+
|
|
254
|
+
// CORRECT — sum, or use the top-level totalTokens
|
|
255
|
+
console.log(result.totalTokens);
|
|
256
|
+
console.log((result.tokenUsage?.inputTokens ?? 0) + (result.tokenUsage?.outputTokens ?? 0));
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Not checking `chunk.type` before accessing fields
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// WRONG — not every chunk has .data; this is undefined for tool_start/done/etc.
|
|
263
|
+
for await (const chunk of stream) {
|
|
264
|
+
console.log(chunk.data);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// CORRECT — switch / narrow on chunk.type
|
|
268
|
+
for await (const chunk of stream) {
|
|
269
|
+
if (chunk.type === "token") {
|
|
270
|
+
process.stdout.write(chunk.data);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Ignoring `guardrail_triggered.stopped`
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
// WRONG — continuing after a stopping guardrail
|
|
279
|
+
case "guardrail_triggered":
|
|
280
|
+
console.log("guardrail fired, continuing…");
|
|
281
|
+
break;
|
|
282
|
+
|
|
283
|
+
// CORRECT — `stopped: true` means the stream was terminated by the guardrail
|
|
284
|
+
case "guardrail_triggered":
|
|
285
|
+
if (chunk.stopped) {
|
|
286
|
+
console.error(`stopped by ${chunk.guardrailName}: ${chunk.reason}`);
|
|
287
|
+
abort();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Quick reference
|
|
294
|
+
|
|
295
|
+
| Type / API | Path | Purpose |
|
|
296
|
+
|---|---|---|
|
|
297
|
+
| `AgentLike` | `@directive-run/ai` | `{ name, instructions?, model?, tools? }` — what runners receive |
|
|
298
|
+
| `RunResult<T>` | `@directive-run/ai` | `{ output, messages, toolCalls, totalTokens, tokenUsage?, isCached? }` |
|
|
299
|
+
| `StreamChunk` | `@directive-run/ai` | 8-way discriminated union for streaming output |
|
|
300
|
+
| `orchestrator.runStream(agent, input, opts?)` | instance method | Returns `{ stream, result, abort }` |
|
|
301
|
+
| `createStreamingRunner(baseRunner, opts?)` | `@directive-run/ai` | Wrap a `StreamingCallbackRunner` into a `StreamRunner` |
|
|
302
|
+
| `createSSETransport(config?)` | `@directive-run/ai` | `{ toResponse, toStream }` for piping a stream to SSE |
|
|
303
|
+
|
|
304
|
+
## See also
|
|
305
|
+
|
|
306
|
+
- [`ai-adapters.md`](./ai-adapters.md) — provider runners that produce the streams this file consumes
|
|
307
|
+
- [`ai-orchestrator.md`](./ai-orchestrator.md) — `orchestrator.runStream(agent, input)` returns the `{ stream, result, abort }` triple this file documents
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# AI inter-agent communication
|
|
2
|
+
|
|
3
|
+
> Covers `@directive-run/ai` — `createMessageBus`, `createAgentNetwork`, request/response patterns, scratchpad, cross-agent fact reads.
|
|
4
|
+
|
|
5
|
+
`createMessageBus` for typed pub/sub, `createAgentNetwork` for capability-based discovery and request/response patterns, plus the scratchpad and cross-agent state access available via a multi-agent orchestrator. Import from `@directive-run/ai`.
|
|
6
|
+
|
|
7
|
+
## Decision tree
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
What do you need?
|
|
11
|
+
├── Typed pub/sub between agents → createMessageBus(config?)
|
|
12
|
+
├── Capability lookup + request/response → createAgentNetwork({ bus, agents? })
|
|
13
|
+
├── Per-pattern ephemeral state → context.scratchpad inside tasks/agents
|
|
14
|
+
├── Reading another agent's facts → orchestrator.system.facts[agentId].x
|
|
15
|
+
└── Cross-agent derived state → orchestrator.system.derive[agentId].x
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## `createMessageBus(config?)`
|
|
19
|
+
|
|
20
|
+
Fire-and-forget pub/sub keyed by `agentId`. `publish()` is synchronous — it returns the message id before delivery completes. Use `onDelivery` / `onDeliveryError` in config to observe delivery status.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { createMessageBus, type TypedAgentMessage } from "@directive-run/ai";
|
|
24
|
+
|
|
25
|
+
const bus = createMessageBus({
|
|
26
|
+
maxHistory: 1000,
|
|
27
|
+
defaultTtlMs: 60 * 60 * 1000,
|
|
28
|
+
maxPendingPerAgent: 100,
|
|
29
|
+
onDelivery: (msg, recipients) => log("delivered", msg.id, recipients),
|
|
30
|
+
onDeliveryError: (msg, error) => log("delivery_failed", msg.id, error.message),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Subscribe — returns a Subscription object with an .unsubscribe() method
|
|
34
|
+
const sub = bus.subscribe("writer", (message: TypedAgentMessage) => {
|
|
35
|
+
console.log(`writer received: ${message.type}`);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Later
|
|
39
|
+
sub.unsubscribe();
|
|
40
|
+
|
|
41
|
+
// Publish — returns the message id (sync)
|
|
42
|
+
const id = bus.publish({
|
|
43
|
+
type: "DELEGATION",
|
|
44
|
+
from: "researcher",
|
|
45
|
+
to: "writer",
|
|
46
|
+
task: "Summarize the findings",
|
|
47
|
+
context: { sources: 12 },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Inspect
|
|
51
|
+
bus.getHistory({ from: "researcher" }, 50);
|
|
52
|
+
bus.getMessage(id);
|
|
53
|
+
bus.getPending("writer"); // messages queued for an offline subscriber
|
|
54
|
+
|
|
55
|
+
bus.clear();
|
|
56
|
+
bus.destroy();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
There is no `bus.request(...)` method — request/response lives on the AgentNetwork (next section).
|
|
60
|
+
|
|
61
|
+
## `createAgentNetwork({ bus, agents? })`
|
|
62
|
+
|
|
63
|
+
Wraps a MessageBus with registry, capability lookup, and request/response patterns. **Capability lookup returns `AgentInfo[]`, not `string[]`.** Request/response shapes — `request`, `delegate`, `query`, `broadcast`, `listen`, `send` — are all on the network, NOT on the bus.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { createAgentNetwork, type AgentInfo } from "@directive-run/ai";
|
|
67
|
+
|
|
68
|
+
const network = createAgentNetwork({
|
|
69
|
+
bus,
|
|
70
|
+
agents: {
|
|
71
|
+
researcher: { capabilities: ["search", "verify", "cite"] },
|
|
72
|
+
writer: { capabilities: ["draft", "edit", "summarize"] },
|
|
73
|
+
analyst: { capabilities: ["analyze", "chart", "report"] },
|
|
74
|
+
},
|
|
75
|
+
defaultTimeout: 30_000,
|
|
76
|
+
onAgentOnline: (id) => log(`${id} online`),
|
|
77
|
+
onAgentOffline: (id) => log(`${id} offline`),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Find — returns AgentInfo[], not string[]
|
|
81
|
+
const verifiers: AgentInfo[] = network.findByCapability("verify");
|
|
82
|
+
verifiers.forEach((info) => console.log(info.id, info.capabilities));
|
|
83
|
+
|
|
84
|
+
// Request/response with timeout
|
|
85
|
+
const reply = await network.request("coordinator", "researcher", "verify-claim", {
|
|
86
|
+
claim: "GPT-4 has 1.8T parameters",
|
|
87
|
+
}, 10_000);
|
|
88
|
+
console.log(reply.payload);
|
|
89
|
+
|
|
90
|
+
// Delegate a task and await its result
|
|
91
|
+
const result = await network.delegate(
|
|
92
|
+
"coordinator",
|
|
93
|
+
"writer",
|
|
94
|
+
"Draft a 200-word summary",
|
|
95
|
+
{ source: reply.payload },
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Question / answer
|
|
99
|
+
const answer = await network.query("coordinator", "analyst", "What's the median latency?", { window: "24h" });
|
|
100
|
+
|
|
101
|
+
// Broadcast to all agents
|
|
102
|
+
network.broadcast("coordinator", { type: "INFORM", content: "Cache cleared" });
|
|
103
|
+
|
|
104
|
+
// Plain fire-and-forget through the network (returns message id)
|
|
105
|
+
network.send("coordinator", "writer", { type: "INFORM", content: "Starting batch" });
|
|
106
|
+
|
|
107
|
+
// Subscribe an agent — same as bus.subscribe but registered through the network
|
|
108
|
+
const sub = network.listen("writer", (msg) => console.log(msg.type));
|
|
109
|
+
sub.unsubscribe();
|
|
110
|
+
|
|
111
|
+
network.destroy();
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
There is no `network.route(capability, payload)` — pick the agent yourself via `findByCapability(...)`, then call `request` / `delegate` against the chosen `agent.id`.
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
const candidates = network.findByCapability("verify");
|
|
118
|
+
if (candidates.length === 0) throw new Error("no verifier available");
|
|
119
|
+
const target = candidates[0];
|
|
120
|
+
|
|
121
|
+
const result = await network.request("coordinator", target.id, "verify", { claim });
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Cross-agent state via facts + derivations
|
|
125
|
+
|
|
126
|
+
Each agent in a multi-agent orchestrator becomes a namespaced module, so its facts and derivations are readable on `orchestrator.system`.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const orchestrator = createMultiAgentOrchestrator({
|
|
130
|
+
agents: { researcher, writer },
|
|
131
|
+
runner,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
orchestrator.start();
|
|
135
|
+
|
|
136
|
+
// Read another agent's namespaced state (read-only outside its own resolvers)
|
|
137
|
+
const status = orchestrator.system.facts.researcher.status;
|
|
138
|
+
const output = orchestrator.system.facts.writer.output;
|
|
139
|
+
const isReady = orchestrator.system.derive.researcher.isComplete;
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
For a deeper treatment of multi-module fact access + cross-module dependencies, see `multi-module.md`.
|
|
143
|
+
|
|
144
|
+
## Scratchpad — per-pattern ephemeral state
|
|
145
|
+
|
|
146
|
+
The scratchpad is a read-only context object shared across tasks/agents inside a single pattern execution. **You cannot mutate `context.scratchpad` directly** — pass updates back through the task's return value, or use `network.send` / `bus.publish` for messages that outlive the pattern.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
tasks: {
|
|
150
|
+
gather: {
|
|
151
|
+
run: async (input, signal, context) => {
|
|
152
|
+
// context.scratchpad is Readonly — do NOT do context.scratchpad.x = …
|
|
153
|
+
const seed = context.scratchpad.seed;
|
|
154
|
+
return JSON.stringify({ seed, data: await fetchData(seed) });
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
format: {
|
|
158
|
+
run: async (input, signal, context) => {
|
|
159
|
+
// Read scratchpad written by the pattern config
|
|
160
|
+
const region = context.scratchpad.region;
|
|
161
|
+
const parsed = JSON.parse(input);
|
|
162
|
+
return JSON.stringify({ ...parsed, region });
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
For full task surface, see `ai-tasks.md`.
|
|
169
|
+
|
|
170
|
+
## Message bus + orchestrator wiring
|
|
171
|
+
|
|
172
|
+
The orchestrator does NOT accept a `bus:` option directly — wire the bus alongside the orchestrator and route messages explicitly.
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { createMultiAgentOrchestrator } from "@directive-run/ai/multi-agent";
|
|
176
|
+
import { createMessageBus, createAgentNetwork } from "@directive-run/ai";
|
|
177
|
+
|
|
178
|
+
const bus = createMessageBus({ maxHistory: 1000 });
|
|
179
|
+
const network = createAgentNetwork({
|
|
180
|
+
bus,
|
|
181
|
+
agents: {
|
|
182
|
+
researcher: { capabilities: ["search", "verify"] },
|
|
183
|
+
writer: { capabilities: ["draft", "edit"] },
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const orchestrator = createMultiAgentOrchestrator({
|
|
188
|
+
agents: { researcher, writer },
|
|
189
|
+
runner,
|
|
190
|
+
hooks: {
|
|
191
|
+
onAgentStart: (e) => {
|
|
192
|
+
bus.publish({
|
|
193
|
+
type: "AGENT_START",
|
|
194
|
+
from: "orchestrator",
|
|
195
|
+
to: e.agentName,
|
|
196
|
+
input: e.input,
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
orchestrator.start();
|
|
203
|
+
|
|
204
|
+
// External systems can publish to the bus directly
|
|
205
|
+
bus.publish({
|
|
206
|
+
type: "INFORM",
|
|
207
|
+
from: "external-pipeline",
|
|
208
|
+
to: "researcher",
|
|
209
|
+
content: "New corpus available",
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Anti-patterns
|
|
214
|
+
|
|
215
|
+
### `bus.request(...)`
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// WRONG — there is no request method on MessageBus
|
|
219
|
+
const reply = await bus.request({ type: "REQUEST", from: "a", to: "b", action: "verify", timeout: 10_000 });
|
|
220
|
+
|
|
221
|
+
// CORRECT — request/response is on AgentNetwork
|
|
222
|
+
const reply = await network.request("a", "b", "verify", { /* payload */ }, 10_000);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### `const unsub = bus.subscribe(...); unsub();`
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// WRONG — subscribe returns a Subscription object, not the unsubscribe function
|
|
229
|
+
const unsub = bus.subscribe("writer", handler);
|
|
230
|
+
unsub();
|
|
231
|
+
|
|
232
|
+
// CORRECT — call .unsubscribe() on the returned Subscription
|
|
233
|
+
const sub = bus.subscribe("writer", handler);
|
|
234
|
+
sub.unsubscribe();
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### `network.findByCapability(...)` returning strings
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// WRONG — assumes the return is string[]
|
|
241
|
+
const writers = network.findByCapability("draft"); // ["writer"]?
|
|
242
|
+
network.send("coordinator", writers[0], message); // passes a string where AgentInfo was expected? No — but we lost capabilities/metadata
|
|
243
|
+
|
|
244
|
+
// CORRECT — it returns AgentInfo[]; use .id when you need the string
|
|
245
|
+
const candidates = network.findByCapability("draft");
|
|
246
|
+
network.send("coordinator", candidates[0].id, message);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### `network.route(capability, payload)`
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// WRONG — no such method
|
|
253
|
+
await network.route("verify", { claim });
|
|
254
|
+
|
|
255
|
+
// CORRECT — pick an agent via findByCapability, then request/delegate
|
|
256
|
+
const verifiers = network.findByCapability("verify");
|
|
257
|
+
if (verifiers.length === 0) throw new Error("no verifier");
|
|
258
|
+
const result = await network.request("coordinator", verifiers[0].id, "verify", { claim });
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### `createMultiAgentOrchestrator({ bus })`
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// WRONG — bus is not an option on MultiAgentOrchestratorOptions
|
|
265
|
+
createMultiAgentOrchestrator({ agents, runner, bus })
|
|
266
|
+
|
|
267
|
+
// CORRECT — wire the bus separately and publish through hooks/handlers
|
|
268
|
+
createMultiAgentOrchestrator({
|
|
269
|
+
agents,
|
|
270
|
+
runner,
|
|
271
|
+
hooks: {
|
|
272
|
+
onAgentComplete: (e) => bus.publish({ type: "AGENT_COMPLETE", from: "orchestrator", to: e.agentName, output: e.output }),
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Mutating `context.scratchpad`
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// WRONG — context.scratchpad is Readonly
|
|
281
|
+
context.scratchpad.researchData = data;
|
|
282
|
+
context.scratchpad.timestamp = Date.now();
|
|
283
|
+
|
|
284
|
+
// CORRECT — return new state from the task; or use bus.publish to broadcast
|
|
285
|
+
return JSON.stringify({ ...JSON.parse(input), researchData: data, timestamp: Date.now() });
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Quick reference
|
|
289
|
+
|
|
290
|
+
| API | Purpose | Notes |
|
|
291
|
+
|---|---|---|
|
|
292
|
+
| `createMessageBus(config?)` | Pub/sub primitive | `publish` / `subscribe` / `getHistory` / `getMessage` / `getPending` / `clear` / `destroy` |
|
|
293
|
+
| `bus.subscribe(id, handler, filter?)` | Subscribe | returns a `Subscription` — call `sub.unsubscribe()` |
|
|
294
|
+
| `createAgentNetwork({ bus, agents? })` | Capability-aware coordination | `request` / `delegate` / `query` / `broadcast` / `send` / `listen` / `findByCapability` |
|
|
295
|
+
| `network.findByCapability(cap)` | Discovery | returns `AgentInfo[]` (NOT `string[]`) |
|
|
296
|
+
| `network.request(from, to, action, payload, timeout?)` | Request/response | `Promise<ResponseMessage>` |
|
|
297
|
+
| `network.delegate(from, to, task, context)` | Delegated task with result | `Promise<DelegationResultMessage>` |
|
|
298
|
+
| `orchestrator.system.facts[agentId].x` | Cross-agent fact read | each agent is a namespaced module |
|
|
299
|
+
| `context.scratchpad` | Per-pattern ephemeral state | Readonly inside tasks/agents |
|
|
300
|
+
|
|
301
|
+
## See also
|
|
302
|
+
|
|
303
|
+
- [`ai-multi-agent.md`](./ai-multi-agent.md) — the orchestrator that owns the namespaced facts this file's primitives read
|
|
304
|
+
- [`ai-tasks.md`](./ai-tasks.md) — tasks share the scratchpad surface this file documents
|