@directive-run/knowledge 1.23.0 → 1.24.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/ai/ai-adapters.md CHANGED
@@ -74,6 +74,27 @@ const proxiedRunner = createAnthropicRunner({
74
74
  });
75
75
  ```
76
76
 
77
+ ### Prompt Caching (Anthropic)
78
+
79
+ Opt in with `promptCaching: "automatic"` to add a `cache_control` breakpoint on the agent's instructions. Anthropic caches that stable system prefix so repeat calls that share it read from cache (~0.1x input cost) instead of reprocessing it; the variable message suffix stays uncached. Off by default (bare-string system, unchanged prior behavior), non-breaking, and no beta header is required on `anthropic-version: 2023-06-01`. Supported on the non-streaming `createAnthropicRunner` (streaming is a follow-up).
80
+
81
+ ```typescript
82
+ const runner = createAnthropicRunner({
83
+ apiKey: process.env.ANTHROPIC_API_KEY,
84
+ promptCaching: "automatic",
85
+ });
86
+ const result = await runner(agent, prompt);
87
+
88
+ // Cache usage is surfaced on tokenUsage – present only when caching is active:
89
+ const { cacheReadTokens = 0, cacheCreationTokens = 0 } = result.tokenUsage;
90
+ // cacheCreationTokens – tokens written to cache on the first call (~1.25x cost)
91
+ // cacheReadTokens – tokens served from cache on repeat calls (~0.1x cost)
92
+ ```
93
+
94
+ `inputTokens` remains the uncached remainder; the cache fields are separate and additive, and `totalTokens` includes all four (input + output + cache-read + cache-creation).
95
+
96
+ **Minimum cacheable prefix (the #1 support gotcha).** Anthropic silently ignores `cache_control` when the cached prefix is below a per-model minimum – ~1024 tokens on Sonnet-tier models, 2048 on Sonnet-4.6 & Haiku-3.5, 4096 on Opus & Haiku-4.5. No error is raised: caching simply doesn't happen and `cacheReadTokens` stays `0` on repeat calls (that `0` is the diagnostic). Since Directive caches `agent.instructions`, short instructions commonly fall below the threshold. The `ephemeral` breakpoint also carries a 5-minute default TTL – a prefix not re-read within that window is evicted. (Cost note: `withBudget`/`estimateCost` weight all tokens equally today, so cached runs are not yet repriced for the ~0.1x read / ~1.25x write rates – cache-aware pricing is a planned follow-up.)
97
+
77
98
  ## OpenAI Adapter
78
99
 
79
100
  ```typescript
@@ -147,7 +168,7 @@ console.log(result.tokenUsage.input_tokens); // undefined!
147
168
  const result = await runner.run(agent, prompt);
148
169
  console.log(result.tokenUsage.inputTokens); // number
149
170
  console.log(result.tokenUsage.outputTokens); // number
150
- console.log(result.totalTokens); // inputTokens + outputTokens
171
+ console.log(result.totalTokens); // inputTokens + outputTokens (+ cache tokens when caching is active)
151
172
  ```
152
173
 
153
174
  All adapters normalize token usage to the same shape regardless of provider:
@@ -156,9 +177,14 @@ All adapters normalize token usage to the same shape regardless of provider:
156
177
  interface NormalizedTokenUsage {
157
178
  inputTokens: number;
158
179
  outputTokens: number;
180
+ // Populated by adapters with prompt caching active (e.g. Anthropic
181
+ // `promptCaching: "automatic"`); omitted otherwise.
182
+ cacheReadTokens?: number;
183
+ cacheCreationTokens?: number;
159
184
  }
160
185
 
161
186
  // result.totalTokens = inputTokens + outputTokens
187
+ // + cacheReadTokens + cacheCreationTokens (cache fields are 0 when absent)
162
188
  ```
163
189
 
164
190
  ## Adapter Hooks
@@ -244,7 +270,7 @@ const orchestrator = createAgentOrchestrator({ runner });
244
270
 
245
271
  | Adapter | Import Path | Key Options |
246
272
  |---|---|---|
247
- | Anthropic | `@directive-run/ai/anthropic` | `apiKey`, `defaultModel`, `maxTokens` |
273
+ | Anthropic | `@directive-run/ai/anthropic` | `apiKey`, `defaultModel`, `maxTokens`, `promptCaching` |
248
274
  | OpenAI | `@directive-run/ai/openai` | `apiKey`, `defaultModel`, `organization` |
249
275
  | Ollama | `@directive-run/ai/ollama` | `baseURL`, `defaultModel` |
250
276
  | Gemini | `@directive-run/ai/gemini` | `apiKey`, `defaultModel` |
@@ -55,7 +55,11 @@ interface RunResult<T = unknown> {
55
55
  interface TokenUsage {
56
56
  inputTokens: number;
57
57
  outputTokens: number;
58
- // Note: NO `total` field sum `inputTokens + outputTokens` when needed, or use `result.totalTokens`.
58
+ // Populated by adapters with prompt caching active (e.g. Anthropic
59
+ // `promptCaching: "automatic"`); omitted otherwise.
60
+ cacheReadTokens?: number;
61
+ cacheCreationTokens?: number;
62
+ // Note: NO `total` field — sum `inputTokens + outputTokens` (+ cache tokens when present) when needed, or use `result.totalTokens`.
59
63
  }
60
64
  ```
61
65
 
package/ai/ai-security.md CHANGED
@@ -388,8 +388,8 @@ source publishes PII into a fact and the agent's prompt template embeds
388
388
  that fact (`"Hello ${facts.email}..."`), the PII reaches the LLM call
389
389
  without hitting the input guardrail chain.
390
390
 
391
- This is the documented R5 audit finding (red team, privacy, AI-integration
392
- reviewers independently flagged it). The Tier 0 fix:
391
+ This is a documented gap in the input-only guardrail chain — the fix
392
+ is a fact-store-boundary guardrail that runs on every fact write:
393
393
 
394
394
  ```ts
395
395
  import { createSystem, createModule, t } from "@directive-run/core";
@@ -0,0 +1,178 @@
1
+ // Example: cloudflare-directive-ai
2
+ // Source: examples/cloudflare-directive-ai/src/module.ts
3
+ // Pure module file — no DOM wiring
4
+
5
+ import { createModule, createSystem } from "@directive-run/core";
6
+ import { sourceFromDOAlarm } from "@directive-run/sources/cloudflare";
7
+ import { schema, type PublishingStatus } from "./schema.js";
8
+ import type { AuditLog, Broadcaster } from "./deps.js";
9
+ import { createPublishingChain, type PublishingChain } from "./agents.js";
10
+
11
+ export interface PublishingDeps {
12
+ auditLog: AuditLog;
13
+ broadcaster: Broadcaster;
14
+ anthropicApiKey: string;
15
+ storage: DurableObjectStorage;
16
+ intervalMs?: number;
17
+ }
18
+
19
+ export interface PublishingModuleHandle {
20
+ alarmTick: () => void;
21
+ }
22
+
23
+ export function createPublishingSystem(deps: PublishingDeps) {
24
+ const chain = createPublishingChain(deps.anthropicApiKey);
25
+ const handle: PublishingModuleHandle = {
26
+ alarmTick: () => {},
27
+ };
28
+
29
+ const alarmSource = sourceFromDOAlarm({
30
+ storage: deps.storage,
31
+ intervalMs: deps.intervalMs ?? 5 * 60 * 1000,
32
+ eventName: "tick",
33
+ payload: () => ({}),
34
+ onTickRegistered: (tick) => {
35
+ handle.alarmTick = tick;
36
+ },
37
+ });
38
+
39
+ const publishingModule = createModule("publishing", {
40
+ schema,
41
+
42
+ init: (facts) => {
43
+ facts.status = "idle" satisfies PublishingStatus;
44
+ facts.currentArticle = {};
45
+ facts.ingestQueue = [];
46
+ facts.reviewQueue = [];
47
+ facts.errorLog = [];
48
+ },
49
+
50
+ derive: {
51
+ queueDepth: (facts) => facts.ingestQueue.length,
52
+ reviewDepth: (facts) => facts.reviewQueue.length,
53
+ isReadyToAggregate: (facts) =>
54
+ facts.ingestQueue.length >= 3 && facts.status === "idle",
55
+ },
56
+
57
+ events: {
58
+ ingest: (facts, payload) => {
59
+ facts.ingestQueue = [
60
+ ...facts.ingestQueue,
61
+ { url: payload.url, capturedAt: new Date().toISOString() },
62
+ ];
63
+ },
64
+ tick: (facts) => {
65
+ if (facts.status === "failed" || facts.status === "published") {
66
+ facts.status = "idle";
67
+ }
68
+ },
69
+ },
70
+
71
+ sources: {
72
+ alarm: alarmSource,
73
+ },
74
+
75
+ constraints: {
76
+ startAggregation: {
77
+ priority: 100,
78
+ when: (facts) =>
79
+ facts.ingestQueue.length >= 3 && facts.status === "idle",
80
+ require: (facts) => ({
81
+ type: "RUN_PUBLISHING_CHAIN",
82
+ sources: facts.ingestQueue.slice(0, 8),
83
+ }),
84
+ },
85
+
86
+ routeArticle: {
87
+ priority: 90,
88
+ when: (facts) => facts.status === "reviewing",
89
+ require: (facts) => ({
90
+ type: "ROUTE_ARTICLE",
91
+ article: facts.currentArticle,
92
+ }),
93
+ },
94
+ },
95
+
96
+ resolvers: {
97
+ runChain: {
98
+ requirement: "RUN_PUBLISHING_CHAIN",
99
+ key: (req) => `chain-${req.sources.map((s) => s.url).join("|")}`,
100
+ resolve: async (req, context) => {
101
+ context.facts.status = "aggregating";
102
+ context.facts.ingestQueue = context.facts.ingestQueue.slice(
103
+ req.sources.length,
104
+ );
105
+
106
+ try {
107
+ const result = await chain.run(req.sources, "demo");
108
+
109
+ context.facts.currentArticle = result;
110
+ context.facts.status = "reviewing";
111
+ await deps.auditLog.append({
112
+ event: "chain-completed",
113
+ at: new Date().toISOString(),
114
+ sources: req.sources.length,
115
+ confidence: result.confidence,
116
+ slug: result.slug,
117
+ });
118
+ } catch (err) {
119
+ const message = err instanceof Error ? err.message : String(err);
120
+ context.facts.errorLog = [
121
+ ...context.facts.errorLog,
122
+ {
123
+ at: new Date().toISOString(),
124
+ source: "publishing-chain",
125
+ message,
126
+ },
127
+ ];
128
+ context.facts.status = "failed";
129
+ await deps.auditLog.append({
130
+ event: "chain-failed",
131
+ at: new Date().toISOString(),
132
+ error: message,
133
+ });
134
+ }
135
+ },
136
+ },
137
+
138
+ routeArticle: {
139
+ requirement: "ROUTE_ARTICLE",
140
+ key: (req) => `route-${req.article.slug ?? "unknown"}`,
141
+ resolve: async (req, context) => {
142
+ const confidence = req.article.confidence ?? 0;
143
+ const slug = req.article.slug ?? "unknown";
144
+
145
+ if (confidence >= 0.85) {
146
+ await deps.broadcaster.send(req.article);
147
+ context.facts.status = "published";
148
+ await deps.auditLog.append({
149
+ event: "auto-published",
150
+ at: new Date().toISOString(),
151
+ slug,
152
+ confidence,
153
+ });
154
+ } else {
155
+ context.facts.reviewQueue = [
156
+ ...context.facts.reviewQueue,
157
+ { articleId: slug, confidence },
158
+ ];
159
+ context.facts.status = "idle";
160
+ await deps.auditLog.append({
161
+ event: "routed-to-review",
162
+ at: new Date().toISOString(),
163
+ slug,
164
+ confidence,
165
+ });
166
+ }
167
+ },
168
+ },
169
+ },
170
+ });
171
+
172
+ const system = createSystem({ module: publishingModule });
173
+
174
+ return { system, handle };
175
+ }
176
+
177
+ export type PublishingSystem = ReturnType<typeof createPublishingSystem>;
178
+ export type { PublishingChain };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directive-run/knowledge",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
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",
@@ -52,9 +52,9 @@
52
52
  "tsup": "^8.3.5",
53
53
  "tsx": "^4.19.2",
54
54
  "typescript": "^5.7.2",
55
- "vitest": "^3.0.0",
56
- "@directive-run/core": "1.23.0",
57
- "@directive-run/ai": "1.23.0"
55
+ "vitest": "^3.2.6",
56
+ "@directive-run/core": "1.24.0",
57
+ "@directive-run/ai": "1.24.0"
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",
package/sitemap.md CHANGED
@@ -86,6 +86,7 @@ Website: https://directive.run
86
86
  - [Persistence](https://directive.run/docs/plugins/persistence)
87
87
  - [Performance](https://directive.run/docs/plugins/performance)
88
88
  - [Circuit Breaker](https://directive.run/docs/plugins/circuit-breaker)
89
+ - [Clobber Loop Detector](https://directive.run/docs/plugins/clobber-loop)
89
90
  - [Observability](https://directive.run/docs/plugins/observability)
90
91
  - [Custom Plugins](https://directive.run/docs/plugins/custom)
91
92
 
@@ -108,6 +109,14 @@ Website: https://directive.run
108
109
  - [Resolver Binding](https://directive.run/docs/resolver-binding)
109
110
  - [Audit Ledger](https://directive.run/docs/audit-ledger)
110
111
 
112
+ ### Broadcast
113
+ - [Overview](https://directive.run/docs/broadcast)
114
+ - [GitHub Action](https://directive.run/docs/broadcast/action)
115
+ - [CLI](https://directive.run/docs/broadcast/cli)
116
+ - [Cloudflare Worker](https://directive.run/docs/broadcast/worker)
117
+ - [MCP server](https://directive.run/docs/broadcast/mcp)
118
+ - [README badge](https://directive.run/docs/broadcast/badge)
119
+
111
120
  ### Examples
112
121
  - [Number Match](https://directive.run/docs/examples/counter)
113
122
  - [Auth Flow](https://directive.run/docs/examples/auth-flow)