@directive-run/knowledge 1.19.0 → 1.19.2
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/ai/ai-sources.md +93 -64
- package/core/sources.md +22 -4
- package/package.json +3 -3
package/ai/ai-sources.md
CHANGED
|
@@ -54,7 +54,11 @@ gate execution on `facts.<lifecycle>.healthy` derivations.
|
|
|
54
54
|
- **MCP server connect/disconnect** → see "MCP Lifecycle" below.
|
|
55
55
|
- **Health checks** → poll inside the source's `attach`; publish
|
|
56
56
|
`health_changed` events as the upstream service responds.
|
|
57
|
-
- **WebSocket open / close** → `
|
|
57
|
+
- **WebSocket open / close (Cloudflare DO)** → `sourceFromWebSocketMessage()`
|
|
58
|
+
(`@directive-run/sources/cloudflare`). For raw Node/browser WebSocket
|
|
59
|
+
bridges, declare a source whose `attach` wires `addEventListener` and
|
|
60
|
+
publishes `MESSAGE` / `CLOSE` / `ERROR` events (a generic
|
|
61
|
+
`sourceFromWebSocket()` helper is queued for a follow-up RFC).
|
|
58
62
|
|
|
59
63
|
### "I want my agent's facts to update from a live external feed"
|
|
60
64
|
|
|
@@ -151,10 +155,14 @@ Two new chunk variants land on the stream:
|
|
|
151
155
|
`runStream({ liveContext })` is the dedicated subject of
|
|
152
156
|
[RFC 0005](../../docs/rfcs/0005-live-context-agent.md). Today's
|
|
153
157
|
implementation is **231 LOC in `agent-orchestrator.ts`** — under the
|
|
154
|
-
RFC's 300-LOC scope guard.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
RFC's 300-LOC scope guard. Behavior is **abort-and-emit**: when
|
|
159
|
+
`interruptWhen` returns `true`, the orchestrator aborts the in-flight
|
|
160
|
+
LLM run and emits an `interrupted` chunk with the partial output; the
|
|
161
|
+
caller resumes by issuing a fresh `runStream` against the still-live
|
|
162
|
+
subscription (or fully tears down via `result.abort()`). Automatic
|
|
163
|
+
re-invocation (the "restart" semantic the original RFC drafted) is
|
|
164
|
+
reserved for a follow-up RFC + field — the original `mode` field was
|
|
165
|
+
removed before release because the impl never read it.
|
|
158
166
|
|
|
159
167
|
**Security companion (mandatory when watched facts may carry PII):**
|
|
160
168
|
wire `createFactPIIGuardrail` on the same Directive system. Without
|
|
@@ -177,77 +185,97 @@ holder. The holder pattern lets you construct the adapter and the
|
|
|
177
185
|
source in either order — the source's mount/unmount drives which
|
|
178
186
|
publish closure (if any) receives lifecycle events.
|
|
179
187
|
|
|
188
|
+
> **Multi-tenant safety.** If you import the module that owns the
|
|
189
|
+
> holder from two places (e.g., a Worker that mounts one Directive
|
|
190
|
+
> system per tenant DO), a module-level `let publishRef` would be
|
|
191
|
+
> SHARED — the second tenant's `attach` would silently overwrite the
|
|
192
|
+
> first tenant's pipe. **Always construct BOTH the adapter AND the
|
|
193
|
+
> module inside the same factory function** so the adapter's
|
|
194
|
+
> `events` callbacks close over the same factory-local `publishRef`
|
|
195
|
+
> as the source's `attach`. Sharing an adapter across factory calls
|
|
196
|
+
> re-introduces the cross-contamination because the adapter's
|
|
197
|
+
> `events.onConnect` was bound at construction time to whichever
|
|
198
|
+
> factory's `publishRef` was in scope first.
|
|
199
|
+
|
|
180
200
|
```ts
|
|
181
201
|
import { createSystem, createModule, t } from "@directive-run/core";
|
|
182
202
|
import type { SourcePublish } from "@directive-run/core";
|
|
183
203
|
import { createMCPAdapter } from "@directive-run/ai/mcp";
|
|
184
204
|
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
let publishRef: SourcePublish | null = null;
|
|
191
|
-
|
|
192
|
-
const adapter = createMCPAdapter({
|
|
193
|
-
servers: [/* ... */],
|
|
194
|
-
events: {
|
|
195
|
-
onConnect: (name) => publishRef?.("MCP_SERVER_CONNECTED", { name }),
|
|
196
|
-
onDisconnect: (name) => publishRef?.("MCP_SERVER_DISCONNECTED", { name }),
|
|
197
|
-
},
|
|
198
|
-
});
|
|
205
|
+
// Factory closure — each call yields a fresh `(adapter, module)`
|
|
206
|
+
// pair with its own `publishRef`. Multi-tenant safe; SSR / Vitest
|
|
207
|
+
// hot-reload safe; one tenant's MCP traffic can never leak into
|
|
208
|
+
// another tenant's facts.
|
|
209
|
+
function makeOrchestrator() {
|
|
210
|
+
let publishRef: SourcePublish | null = null;
|
|
199
211
|
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
facts: {
|
|
203
|
-
mcp: t.object<{
|
|
204
|
-
servers: Record<string, "connected" | "disconnected">;
|
|
205
|
-
}>(),
|
|
206
|
-
},
|
|
212
|
+
const adapter = createMCPAdapter({
|
|
213
|
+
servers: [/* ... */],
|
|
207
214
|
events: {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
derivations: {
|
|
212
|
-
allServersHealthy: t.boolean(),
|
|
215
|
+
onConnect: (name) => publishRef?.("MCP_SERVER_CONNECTED", { name }),
|
|
216
|
+
onDisconnect: (name) =>
|
|
217
|
+
publishRef?.("MCP_SERVER_DISCONNECTED", { name }),
|
|
213
218
|
},
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const orchestrator = createModule("orchestrator", {
|
|
222
|
+
schema: {
|
|
223
|
+
facts: {
|
|
224
|
+
mcp: t.object<{
|
|
225
|
+
servers: Record<string, "connected" | "disconnected">;
|
|
226
|
+
}>(),
|
|
227
|
+
},
|
|
228
|
+
events: {
|
|
229
|
+
MCP_SERVER_CONNECTED: { name: t.string() },
|
|
230
|
+
MCP_SERVER_DISCONNECTED: { name: t.string() },
|
|
231
|
+
},
|
|
232
|
+
derivations: {
|
|
233
|
+
allServersHealthy: t.boolean(),
|
|
234
|
+
},
|
|
224
235
|
},
|
|
225
|
-
|
|
226
|
-
f.mcp = {
|
|
227
|
-
...f.mcp,
|
|
228
|
-
servers: { ...f.mcp.servers, [p.name]: "disconnected" },
|
|
229
|
-
};
|
|
236
|
+
init: (f) => {
|
|
237
|
+
f.mcp = { servers: {} };
|
|
230
238
|
},
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
void adapter.connect();
|
|
243
|
-
return async () => {
|
|
244
|
-
publishRef = null;
|
|
245
|
-
await adapter.disconnect();
|
|
239
|
+
events: {
|
|
240
|
+
MCP_SERVER_CONNECTED: (f, p) => {
|
|
241
|
+
f.mcp = {
|
|
242
|
+
...f.mcp,
|
|
243
|
+
servers: { ...f.mcp.servers, [p.name]: "connected" },
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
MCP_SERVER_DISCONNECTED: (f, p) => {
|
|
247
|
+
f.mcp = {
|
|
248
|
+
...f.mcp,
|
|
249
|
+
servers: { ...f.mcp.servers, [p.name]: "disconnected" },
|
|
246
250
|
};
|
|
247
251
|
},
|
|
248
252
|
},
|
|
249
|
-
|
|
250
|
-
|
|
253
|
+
derive: {
|
|
254
|
+
allServersHealthy: (facts) =>
|
|
255
|
+
Object.values(facts.mcp.servers).every((s) => s === "connected"),
|
|
256
|
+
},
|
|
257
|
+
sources: {
|
|
258
|
+
mcpLifecycle: {
|
|
259
|
+
attach: (publish) => {
|
|
260
|
+
publishRef = publish;
|
|
261
|
+
// Kick the adapter's connection lifecycle once the source is
|
|
262
|
+
// attached so the first `onConnect` flows into our publish.
|
|
263
|
+
void adapter.connect();
|
|
264
|
+
return async () => {
|
|
265
|
+
publishRef = null;
|
|
266
|
+
await adapter.disconnect();
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return { orchestrator, adapter };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Per-tenant / per-request usage:
|
|
277
|
+
const { orchestrator, adapter } = makeOrchestrator();
|
|
278
|
+
const system = createSystem({ module: orchestrator });
|
|
251
279
|
```
|
|
252
280
|
|
|
253
281
|
Constraints can now gate agent execution on
|
|
@@ -344,7 +372,8 @@ exports. One install, optional peerDependencies per vendor.
|
|
|
344
372
|
|---|---|---|
|
|
345
373
|
| `@directive-run/sources/supabase` | `sourceFromSupabaseChannel()` | Supabase realtime channel |
|
|
346
374
|
| `@directive-run/sources/cloudflare` | `sourceFromDOAlarm()` | Cloudflare Durable Object alarm |
|
|
347
|
-
| `@directive-run/sources/
|
|
375
|
+
| `@directive-run/sources/cloudflare` | `sourceFromWebSocketMessage()` | Cloudflare Durable Object WebSocket message stream |
|
|
376
|
+
| `@directive-run/sources/websocket` | `sourceFromWebSocket()` | (future RFC) raw browser / Node WebSocket |
|
|
348
377
|
| `@directive-run/sources/sentry` | `sourceFromSentryHook()` | (future RFC) Sentry production-error stream |
|
|
349
378
|
|
|
350
379
|
Install only the umbrella; the vendor peerDeps are optional and pull in
|
package/core/sources.md
CHANGED
|
@@ -274,20 +274,38 @@ flushes return Promises) work with both sync and async teardown:
|
|
|
274
274
|
wall-clock cutoff so the runtime can evict the isolate even if
|
|
275
275
|
some sources hang.
|
|
276
276
|
|
|
277
|
+
Use the **holder + closure** bridge pattern (the same shape the MCP
|
|
278
|
+
recipe in `ai-sources.md` documents) so the `attach` and `onEvict`
|
|
279
|
+
sibling closures share the channel handle:
|
|
280
|
+
|
|
277
281
|
```typescript
|
|
282
|
+
// Holder shared between `attach` and `onEvict`. Both closures live
|
|
283
|
+
// inside the same `sources` object; declaring the channel above them
|
|
284
|
+
// keeps the handle reachable from both.
|
|
285
|
+
let channel: RealtimeChannel | null = null;
|
|
286
|
+
|
|
278
287
|
sources: {
|
|
279
288
|
channel: {
|
|
280
289
|
attach: (publish) => {
|
|
281
|
-
|
|
282
|
-
|
|
290
|
+
channel = supabase.channel("game");
|
|
291
|
+
channel.on("postgres_changes", { event: "UPDATE" }, (p) =>
|
|
283
292
|
publish("ROW_UPDATED", p),
|
|
284
293
|
);
|
|
285
|
-
|
|
294
|
+
channel.subscribe();
|
|
295
|
+
// Per RFC 0009, the unsubscribe MAY return a Promise; awaiting
|
|
296
|
+
// it on `system.stopAsync()` ensures the broker has dropped the
|
|
297
|
+
// subscription before the next start cycle re-attaches.
|
|
298
|
+
return async () => {
|
|
299
|
+
if (channel) await supabase.removeChannel(channel);
|
|
300
|
+
channel = null;
|
|
301
|
+
};
|
|
286
302
|
},
|
|
287
303
|
onEvict: async () => {
|
|
288
304
|
// Cloudflare DO hibernate signal: close the channel actively
|
|
289
305
|
// so the broker drops the subscription before the isolate dies.
|
|
290
|
-
|
|
306
|
+
// The unsubscribe will also run, but onEvict fires first and
|
|
307
|
+
// bounds the pre-hibernation work to a deadline.
|
|
308
|
+
if (channel) await supabase.removeChannel(channel);
|
|
291
309
|
},
|
|
292
310
|
},
|
|
293
311
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directive-run/knowledge",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.2",
|
|
4
4
|
"description": "Knowledge files, examples, and validation for Directive — the constraint-driven TypeScript runtime.",
|
|
5
5
|
"license": "(MIT OR Apache-2.0)",
|
|
6
6
|
"author": "Jason Comes",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"tsx": "^4.19.2",
|
|
54
54
|
"typescript": "^5.7.2",
|
|
55
55
|
"vitest": "^3.0.0",
|
|
56
|
-
"@directive-run/core": "1.19.
|
|
57
|
-
"@directive-run/ai": "1.19.
|
|
56
|
+
"@directive-run/core": "1.19.2",
|
|
57
|
+
"@directive-run/ai": "1.19.2"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"build": "tsx scripts/generate-api-skeleton.ts && tsx scripts/generate-sitemap.ts && tsx scripts/extract-examples.ts && tsup",
|