@directive-run/claude-plugin 1.19.2 → 1.19.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directive-run/claude-plugin",
3
- "version": "1.19.2",
3
+ "version": "1.19.4",
4
4
  "description": "Claude Code plugin for Directive — 12 skills covering modules, constraints, resolvers, derivations, AI orchestration, and adapters. Installable via Claude Code's plugin marketplace or consumable programmatically as an npm package.",
5
5
  "license": "(MIT OR Apache-2.0)",
6
6
  "author": "Jason Comes",
@@ -52,7 +52,7 @@
52
52
  "tsx": "^4.19.2",
53
53
  "typescript": "^5.7.2",
54
54
  "vitest": "^3.0.0",
55
- "@directive-run/knowledge": "1.19.2"
55
+ "@directive-run/knowledge": "1.19.4"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsx scripts/build-skills.ts && tsup",
@@ -82,6 +82,30 @@ above), not a source. Wire it inside the orchestrator's constructor.
82
82
 
83
83
  ---
84
84
 
85
+ ## What other agent frameworks have (and don't)
86
+
87
+ The closest comparable surfaces today, and what they cover:
88
+
89
+ | Capability | Directive `runStream({ liveContext })` | LangChain / LangGraph | Vercel AI SDK | LlamaIndex |
90
+ |---|---|---|---|---|
91
+ | **Mid-generation fact updates from an external stream** | ✅ source primitive bridges the transport; `liveContext` watches fact keys; predicate decides interrupt vs. notify | partial — `Tool` callbacks can be invoked but no "fact store the LLM sees through" surface | partial — `streamText` exposes `onChunk` for the consumer but no mid-stream re-prompt | partial — `QueryEngine` re-runs are caller-driven |
92
+ | **Declarative source / inbound subscription** | ✅ `source` primitive — engine owns mount/unmount; lifecycle observable | hand-rolled per integration (LangChain runnables don't own subscriptions) | n/a — Vercel AI is a streaming SDK, not a state engine | n/a — LlamaIndex is a retrieval framework |
93
+ | **Interrupt + resume against the same subscription** | ✅ `result.interrupt(reason?)` keeps the fact subscription alive; caller re-prompts | callback orchestration on the consumer | abort signal only — no "resume against the same context" affordance | re-query each time |
94
+ | **Tier 0 PII guard at the publish→fact boundary** | ✅ `createFactPIIGuardrail` — wired at `createSystem`; runs on every fact write | callback pattern; consumer wires per chain | input/output guardrails on the call; nothing at the fact-store / state-update boundary | Pydantic schema validation at the retrieval boundary |
95
+ | **Source × OTel out of the box** | ✅ `attachSourcesToOtel` — one long-lived span per `(sourceId, moduleId)` | manual `with_config(callbacks=...)` plumbing | consumer wires `onChunk` into their tracer | manual |
96
+ | **Multi-system reactive composition** | ✅ `liveContext.system` accepts ANY Directive system, even one not owned by the orchestrator | n/a — single-graph model | n/a | n/a |
97
+
98
+ Directive's pitch is not "we're a better LangChain" — it's **"your
99
+ state engine and your agent runtime share one fact store, so the agent
100
+ sees the world change without you wiring callbacks."** The shipped
101
+ 1.x surface delivers that for inbound sources + abort-and-emit
102
+ interruption; the queued follow-up RFC adds automatic re-prompt with
103
+ merge strategies. See
104
+ [RFC 0005's "Demo" section](../../docs/rfcs/0005-live-context-agent.md#demo-the-launch-video)
105
+ for the 6-second launch GIF concept.
106
+
107
+ ---
108
+
85
109
  ## `runStream({ liveContext })` — Reactive Agents
86
110
 
87
111
  > The killer demo: the agent is mid-generation, a source publishes a fact
@@ -366,7 +390,13 @@ that should accompany this plugin in regulated deployments.
366
390
  ## Adapter packages
367
391
 
368
392
  The canonical bridges live in **`@directive-run/sources`** as subpath
369
- exports. One install, optional peerDependencies per vendor.
393
+ exports. One install, optional peerDependencies per vendor:
394
+
395
+ ```bash
396
+ pnpm add @directive-run/sources # umbrella
397
+ pnpm add @directive-run/sources @supabase/supabase-js # + supabase
398
+ pnpm add @directive-run/sources @cloudflare/workers-types # + cloudflare (devDep)
399
+ ```
370
400
 
371
401
  | Subpath | Source factory | Wraps |
372
402
  |---|---|---|
@@ -377,7 +407,57 @@ exports. One install, optional peerDependencies per vendor.
377
407
  | `@directive-run/sources/sentry` | `sourceFromSentryHook()` | (future RFC) Sentry production-error stream |
378
408
 
379
409
  Install only the umbrella; the vendor peerDeps are optional and pull in
380
- only when the corresponding subpath is imported.
410
+ only when the corresponding subpath is imported. `package.json` marks
411
+ both `@supabase/supabase-js` and `@cloudflare/workers-types` as
412
+ optional peer dependencies — consumers using only one subpath get no
413
+ install-error nag for the other.
414
+
415
+ ---
416
+
417
+ ## Observability — pipe `source.*` events to OpenTelemetry
418
+
419
+ `attachSourcesToOtel(system, { tracer, serviceName })` bridges the
420
+ `system.observe()` source lifecycle (`source.attach` / `.publish` /
421
+ `.detach` / `.error`) into OTel spans. Pairs with `createOtelPlugin`
422
+ which exports the agent-side trace surface; the two together cover
423
+ the entire publish-to-prompt pipeline:
424
+
425
+ ```ts
426
+ import { createAgentOrchestrator, createOtelPlugin } from "@directive-run/ai";
427
+ import { attachSourcesToOtel } from "@directive-run/ai";
428
+ import { trace } from "@opentelemetry/api";
429
+
430
+ const tracer = trace.getTracer("agent-service", "1.0.0");
431
+
432
+ const orchestrator = createAgentOrchestrator({
433
+ runner: anthropic(/* ... */),
434
+ plugins: [createOtelPlugin({ tracer })],
435
+ });
436
+
437
+ // One long-lived span per (sourceId, moduleId); publishes land as span
438
+ // events on the parent attach span (cardinality-budgeted).
439
+ const unsub = attachSourcesToOtel(system, {
440
+ tracer,
441
+ serviceName: "agent-service",
442
+ // Optional: throttle high-frequency publishes (default 1 = every publish).
443
+ publishSampleRate: 1,
444
+ });
445
+
446
+ // On `system.destroy()` / `evict()`, call unsub to close any open spans.
447
+ ```
448
+
449
+ What you see in your trace backend:
450
+ - `directive.source.attached` — long-lived span; closes on `detach` with
451
+ OK status, or on `unsub()` with `directive.detached: true` attribute.
452
+ - Span events (`publish`) on the parent span with `source.event_name`
453
+ attribute and a wall-clock timestamp.
454
+ - `directive.source.error` spans with `phase: "attach" | "cleanup" |
455
+ "runtime"` and the truncated error message.
456
+
457
+ This complements `createOtelPlugin`'s agent-span coverage so an SRE
458
+ debugging "agent stopped mid-stream" can trace back through
459
+ `runStream` → `liveContext` interrupt → the watched fact → the source
460
+ publish that triggered the change — one trace tree, no manual joins.
381
461
 
382
462
  ---
383
463
 
@@ -0,0 +1,163 @@
1
+ # Choosing the right primitive
2
+
3
+ Directive ships six core primitives. Picking the wrong one — modeling
4
+ business state as a derivation, or running a side effect inside a
5
+ resolver — is the most common source of "Directive feels heavy"
6
+ complaints. The framework rewards getting this right; this page is the
7
+ decision matrix.
8
+
9
+ ## The six primitives
10
+
11
+ | Primitive | Purpose | Reads | Writes | Triggers when |
12
+ |---|---|---|---|---|
13
+ | **`facts`** | Owned, mutable state | n/a | event handlers, `init`, resolvers, plugin pre-emit | the consumer writes to it |
14
+ | **`derivations`** | Pure, cached read-only views of facts | facts + other derivations | nothing | watched dependency changes |
15
+ | **`events`** | Typed mutation entry points | facts (via `f`) | facts (via `f`) | `system.events.name(payload)` or a source `publish` |
16
+ | **`constraints`** | Declarative "when X, require Y" rules | facts + derivations | NOTHING (emits requirements) | watched dependency changes |
17
+ | **`resolvers`** | Async work that fulfills a constraint's requirement | facts + derivations + `req.payload` | facts (via the resolver's `ctx.set`) | matched requirement enters the inflight queue |
18
+ | **`effects`** | Side effects that fire on fact change | facts + derivations | nothing inside Directive (writes to the outside world) | watched dependency changes |
19
+ | **`sources`** | External event streams mounted by the runtime | nothing | facts (via `publish` → event handler) | the external transport calls `publish(...)` |
20
+
21
+ ## Decision tree
22
+
23
+ ```
24
+ Does this thing belong to my system's state?
25
+ ├── No → it's external. Inbound or outbound?
26
+ │ ├── Inbound (something happens out there, I want to react)
27
+ │ │ ├── Once per ingest event (channel.on, addEventListener,
28
+ │ │ │ setInterval poll, MCP notification) → `source`
29
+ │ │ └── Continuous LLM token stream → AsyncIterable / runStream;
30
+ │ │ NOT a source (the source owns the connection; the
31
+ │ │ iterable owns the per-generation token sequence)
32
+ │ └── Outbound (state changed, I want to tell the world)
33
+ │ ├── Async work I want to retry / cancel / dedupe →
34
+ │ │ `resolver` (paired with a `constraint`)
35
+ │ └── Fire-and-forget side effect (log, ping, animate) →
36
+ │ `effect`
37
+ └── Yes → does it change over time?
38
+ ├── Yes → `fact` (declare in `schema.facts`, mutate via
39
+ │ `events:` handlers and resolver `ctx.set`)
40
+ └── No → it's computed from other facts → `derivation`
41
+ (declare in `schema.derivations`, compute in `derive:`)
42
+
43
+ Then: does anything need to RUN when this changes?
44
+ ├── A rule must trigger and require async work → `constraint`
45
+ │ (declarative `when`; emits a requirement; a `resolver` fulfills it)
46
+ ├── A side effect that just runs → `effect`
47
+ └── Nothing — the LLM / hook / view just reads it next time → done
48
+ ```
49
+
50
+ ## Side-by-side comparisons
51
+
52
+ ### `effect` vs `source`
53
+
54
+ The most common confusion. Both wrap something that fires "when
55
+ something changes."
56
+
57
+ | | `effect` | `source` |
58
+ |---|---|---|
59
+ | Direction | **Outbound** — your state changes, code runs | **Inbound** — the world changes, your state updates |
60
+ | Trigger | watched facts / derivations change | external transport calls `publish` |
61
+ | Body | imperative side effect | call `publish(eventName, payload)` |
62
+ | Lifecycle | per fact change | mount-once at `start`, detach at `stop` |
63
+ | Owner | tied to a fact/derivation watcher | owns its external transport (channel, socket, listener) |
64
+ | Replaces | hand-rolled `useEffect(() => { /* writes elsewhere */ }, [factA])` | hand-rolled `useEffect(() => { subscribe(); return unsub; }, [])` |
65
+
66
+ If the `useEffect` body is `subscribe + return unsubscribe`, declare a
67
+ source. If the `useEffect` body is `do something external because some
68
+ fact changed`, declare an effect.
69
+
70
+ ### `derivation` vs `resolver` vs `effect`
71
+
72
+ All three "react to fact changes." Pick by what they OWN:
73
+
74
+ | | `derivation` | `resolver` | `effect` |
75
+ |---|---|---|---|
76
+ | Owns a **value** | YES (the computed result) | NO | NO |
77
+ | Owns a **promise** | NO | YES (retries, cancels, dedupes) | NO (fire-and-forget) |
78
+ | Owns a **side effect** | NO (pure) | NO (writes facts via `ctx.set`) | YES |
79
+ | Reruns on input change | always (cached) | NEVER automatically — only when the constraint emits a new requirement | always |
80
+ | Can be `await`-ed externally | n/a (read synchronously) | YES (constraints + `system.settle()`) | NO |
81
+ | Wrong-choice symptom | "I want a fact but it's always derived" → fact; "I want side effects" → effect | "I want this to retry but it doesn't" → likely should be an effect; "I want this to run every render" → likely should be a derivation | "I want to write a fact based on the change" → likely should be a resolver |
82
+
83
+ ### `event` vs `resolver`
84
+
85
+ Both can write facts. Pick by who calls them:
86
+
87
+ | | `event` | `resolver` |
88
+ |---|---|---|
89
+ | Caller | external code (`system.events.name(payload)`) OR a source's `publish` | the engine, when a constraint emits a matching requirement |
90
+ | Body | synchronous mutation via `f` accessor | async, can retry, can await |
91
+ | Best for | user actions, source-driven mutations, deterministic state transitions | async work whose result is a fact update |
92
+
93
+ A typical pattern: `event` ingests raw data, `derivation` computes a
94
+ "needs sync?" flag, `constraint` triggers when the flag is true,
95
+ `resolver` does the async work, on success writes the result back via
96
+ another `event`. The same fact pipeline doesn't mix `events` and
97
+ `resolvers` for the SAME write — events are user/source-driven; resolvers
98
+ are engine-driven.
99
+
100
+ ### `constraint` vs `derivation` (predicates)
101
+
102
+ Both can hold `when` predicates. Different jobs:
103
+
104
+ - **`derivation`** evaluates an expression against the current facts
105
+ and returns a value. It's a noun. Other constraints / effects can
106
+ watch it.
107
+ - **`constraint`** evaluates a `when` predicate and, when it matches,
108
+ emits a `requirement` object that a resolver pattern-matches and
109
+ fulfills. It's a verb. Constraints never write — they describe what
110
+ the world MUST do.
111
+
112
+ The constraint's `when` may reference a derivation; that's the canonical
113
+ pattern (compute the condition cheaply once; multiple constraints share
114
+ it).
115
+
116
+ ## Worked example
117
+
118
+ You're building a chat app that mirrors a Supabase realtime channel,
119
+ calls a moderation API on each message, and displays the result.
120
+
121
+ | Layer | Primitive | Why |
122
+ |---|---|---|
123
+ | `messages: t.array<Message>()` | `fact` | mutable list owned by this module |
124
+ | `unmoderated: t.array<Id>()` | `derivation` | filtered view of `messages` where `verdict === null` |
125
+ | `realtime` (the broker subscription) | `source` | mount-once external event stream → `publish('MESSAGE_RECEIVED', row)` |
126
+ | `MESSAGE_RECEIVED` handler | `event` | source-driven mutation; writes the row into `messages` |
127
+ | `moderateUnverified` | `constraint` + `resolver` | constraint emits `MODERATE` requirements for each `unmoderated` id; resolver calls the moderation API + writes `MODERATION_COMPLETE` |
128
+ | `MODERATION_COMPLETE` handler | `event` | engine-driven mutation; sets `verdict` on the message |
129
+ | `announceFlag` | `effect` | watches `unmoderated`, posts to #alerts when count crosses a threshold |
130
+ | `mcpHealthCheck` | `source` | the moderation API has a status SSE; mount once, publish health changes |
131
+
132
+ Every external touch is a `source` or a `resolver`. Every state field
133
+ is a `fact` or a `derivation`. Every "must-happen rule" is a
134
+ `constraint`. Every "do this when X" is an `effect`. Six primitives,
135
+ seven pieces of work, zero `useEffect` hooks.
136
+
137
+ ## When you find yourself wanting to break the model
138
+
139
+ - "I want to call `set` from inside a derivation" — you wanted a
140
+ resolver (the work has a result that's a fact update).
141
+ - "I want my effect to retry" — you wanted a resolver (resolvers
142
+ have retry config; effects don't).
143
+ - "I want my source to write multiple facts at once" — declare an
144
+ event whose handler does the batch write; the source publishes the
145
+ event with the batch payload.
146
+ - "I want to write a fact from a constraint" — you wanted a
147
+ resolver. Constraints declare; resolvers do.
148
+ - "I have a value that's derived from outside state (URL, env, time)"
149
+ — declare it as a `fact` whose `init` reads the external value,
150
+ and use a `source` if it changes (e.g. `popstate` listener).
151
+
152
+ ## Related
153
+
154
+ - [`core-patterns.md`](./core-patterns.md) — module composition and
155
+ the recommended structure of a module file.
156
+ - [`sources.md`](./sources.md) — the source primitive's full lifecycle
157
+ and recipes.
158
+ - [`constraints.md`](./constraints.md) — `when` predicates, requirement
159
+ shapes, and the `bind` / `owns` field-scoped CAS contract.
160
+ - [`resolvers.md`](./resolvers.md) — retry config, batch semantics, and
161
+ the `ctx.set` write contract.
162
+ - [`anti-patterns.md`](./anti-patterns.md) — the things to avoid that
163
+ every Directive consumer eventually tries.
@@ -194,6 +194,17 @@ const unsub = system.observe((event) => {
194
194
 
195
195
  `system.inspect().sources` lists the declared sources + `system.inspect().attachedSourceCount` reports how many are currently bound.
196
196
 
197
+ ### Pipe `source.*` to OpenTelemetry
198
+
199
+ For production observability, `@directive-run/ai` ships
200
+ `attachSourcesToOtel(system, { tracer, serviceName })` which bridges
201
+ the lifecycle into OTel spans (one long-lived span per
202
+ `(sourceId, moduleId)`; publishes as span events; errors as
203
+ `directive.source.error` spans tagged with `phase`). See
204
+ [`../ai/ai-sources.md` § Observability](../ai/ai-sources.md#observability--pipe-source-events-to-opentelemetry)
205
+ for the full recipe; nothing in core depends on OTel so the bridge
206
+ lives next to the AI orchestrator's own span integration.
207
+
197
208
  ## Error handling — runtime errors via `reportError`
198
209
 
199
210
  Per RFC 0008, `attach` receives an optional second argument: a
@@ -314,6 +325,74 @@ sources: {
314
325
  Inside a DO `alarm()` or `webSocketClose()` handler:
315
326
  `await system.evict(/* deadline */ Date.now() + 5000)`.
316
327
 
328
+ ## Runtime compatibility
329
+
330
+ The `source` primitive itself is runtime-agnostic — `attach`, `publish`,
331
+ `unsubscribe`, `onEvict`, and `coalesce` only depend on `Promise`,
332
+ `queueMicrotask`, and standard timers (all available in every modern
333
+ JS runtime). The matrix below covers the SHIPPED adapters in
334
+ `@directive-run/sources`; consumer-authored sources inherit each
335
+ runtime's standard guarantees.
336
+
337
+ | Runtime | Source primitive | `sourceFromSupabaseChannel` | `sourceFromDOAlarm` | `sourceFromWebSocketMessage` | Notes |
338
+ |---|---|---|---|---|---|
339
+ | **Cloudflare DO** | ✅ | ✅ (peerDep `@supabase/supabase-js` works in workerd) | ✅ default `onEvict` clears alarm | ✅ default `onEvict` closes socket 1001 | DO eviction recipe at top of this section; call `await system.evict(deadline)` from `alarm()` / `webSocketClose()`. |
340
+ | **Cloudflare Workers** | ✅ | ✅ | ✅ (needs DO `storage` handle) | ✅ (needs DO `WebSocket` handle) | Same recipes as DO; the 30s wall-clock budget applies to `cleanupAllAsync`. |
341
+ | **Bun** | ✅ | ✅ (Bun supports the `ws` transport supabase uses) | n/a (DO API) | n/a (DO API) | Sync `stop()` + async `stopAsync()` both work; Bun supports top-level `await`. |
342
+ | **Deno** | ✅ | ✅ via `npm:` specifier; needs `--allow-net` | n/a (DO API) | n/a (DO API) | Permissions: `--allow-net` for any transport; `--allow-env` for SUPABASE_URL / SUPABASE_KEY. |
343
+ | **Browser** | ✅ | ✅ | n/a (DO API) | partial — DOM `WebSocket` works with a thin adapter; the Cloudflare-typed helper is DO-specific | DOM `addEventListener` / `BroadcastChannel` / `visibilitychange` recipes inline above. |
344
+ | **Node** | ✅ | ✅ | n/a (DO API) | n/a (DO API) | Test target; vitest exercises every source path. |
345
+
346
+ The `@directive-run/sandbox` validator allowlists `@directive-run/sources`
347
+ and both subpaths, so the playground / `run_in_sandbox` MCP tool / docs
348
+ live runner all accept source-using snippets.
349
+
350
+ ### Polling — when a transport is request/response only
351
+
352
+ When the upstream system has no push channel (REST endpoint, a polled
353
+ status URL, a `setInterval`-driven heartbeat), declare a polling source.
354
+ The runtime owns mount/unmount, the `onEvict` hook lets you cancel any
355
+ pending fetch before hibernation, and the engine's batching keeps the
356
+ per-tick fact mutations cheap:
357
+
358
+ ```typescript
359
+ sources: {
360
+ status: {
361
+ attach: (publish, reportError) => {
362
+ const controller = new AbortController();
363
+ const tick = async () => {
364
+ try {
365
+ const res = await fetch(statusUrl, { signal: controller.signal });
366
+ const body = await res.json();
367
+ publish("STATUS_TICK", body);
368
+ } catch (err) {
369
+ // RFC 0008 — runtime errors go through reportError so they
370
+ // route to source.error with phase: "runtime"; the source
371
+ // stays attached.
372
+ if ((err as { name?: string }).name !== "AbortError") {
373
+ reportError?.(err);
374
+ }
375
+ }
376
+ };
377
+ const id = setInterval(tick, 5_000);
378
+ void tick(); // first poll, immediate.
379
+ return () => {
380
+ clearInterval(id);
381
+ controller.abort();
382
+ };
383
+ },
384
+ coalesce: "lastWriteWins", // only the most recent tick survives a flush
385
+ },
386
+ },
387
+ ```
388
+
389
+ Choose the `coalesce` strategy that matches the consumer:
390
+ - `"lastWriteWins"` — gauge / dashboard updates where stale ticks
391
+ don't matter.
392
+ - `"none"` — counters / time-series where every tick must land.
393
+ - `"all"` — currently an alias for `"none"`; reserved for future
394
+ bounded-buffer semantics (do not depend on it).
395
+
317
396
  ## Common Patterns
318
397
 
319
398
  ### Pattern: Source supplies inbound; resolver supplies outbound