@directive-run/knowledge 1.13.0 → 1.15.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.
Files changed (43) hide show
  1. package/README.md +50 -15
  2. package/ai/ai-adapters.md +2 -0
  3. package/ai/ai-agents-streaming.md +182 -149
  4. package/ai/ai-budget-resilience.md +299 -132
  5. package/ai/ai-communication.md +215 -197
  6. package/ai/ai-debug-observability.md +253 -173
  7. package/ai/ai-guardrails-memory.md +185 -153
  8. package/ai/ai-mcp-rag.md +199 -199
  9. package/ai/ai-multi-agent.md +247 -153
  10. package/ai/ai-orchestrator.md +344 -114
  11. package/ai/ai-security.md +282 -180
  12. package/ai/ai-tasks.md +3 -1
  13. package/ai/ai-testing-evals.md +357 -256
  14. package/core/anti-patterns.md +9 -2
  15. package/core/constraints.md +6 -2
  16. package/core/core-patterns.md +2 -0
  17. package/core/error-boundaries.md +2 -0
  18. package/core/history.md +2 -0
  19. package/core/multi-module.md +8 -3
  20. package/core/naming.md +15 -6
  21. package/core/plugins.md +2 -0
  22. package/core/react-adapter.md +250 -174
  23. package/core/resolvers.md +2 -0
  24. package/core/schema-types.md +2 -0
  25. package/core/system-api.md +2 -0
  26. package/core/testing.md +251 -143
  27. package/examples/checkers.ts +15 -16
  28. package/examples/contact-form.ts +2 -2
  29. package/examples/counter-react.ts +1 -1
  30. package/examples/counter-svelte.ts +1 -1
  31. package/examples/counter-vue.ts +1 -1
  32. package/examples/counter.ts +1 -1
  33. package/examples/data-triggers.ts +4 -4
  34. package/examples/feature-flags.ts +2 -2
  35. package/examples/form-wizard.ts +2 -2
  36. package/examples/newsletter.ts +2 -2
  37. package/examples/server.ts +2 -2
  38. package/examples/shopping-cart.ts +1 -1
  39. package/examples/topic-guard.ts +1 -1
  40. package/package.json +3 -3
  41. package/sitemap.md +26 -3
  42. package/examples/debounce-constraints.ts +0 -95
  43. package/examples/multi-module.ts +0 -58
@@ -1,81 +1,89 @@
1
- # AI Testing and Evaluations
1
+ # AI testing and evaluations
2
2
 
3
- Mock runners, test orchestrators, assertion helpers, simulators, and an evaluation framework with LLM-as-judge and dataset-driven quality gates.
3
+ > Covers `@directive-run/ai/testing` and `@directive-run/ai/evals` `createMockAgentRunner`, `createTestOrchestrator`, simulators, `createEvalSuite` + `eval*` criteria.
4
4
 
5
- ## Decision Tree: "How do I test my AI code?"
5
+ Mock runners, test orchestrators, snapshot helpers, simulators, and a dataset-driven evaluation framework. Import all testing utilities from `@directive-run/ai/testing`; eval criteria factories come from `@directive-run/ai/evals`.
6
+
7
+ ## Decision tree
6
8
 
7
9
  ```
8
10
  What are you testing?
9
- ├── Single agent behavior → createTestOrchestrator + createMockRunner
10
- ├── Multi-agent patterns → createTestMultiAgentOrchestrator
11
- ├── Specific agent was called assertAgentCalled(mockRunner, name)
12
- ├── Token budget behavior createMockRunner with token counts
13
- ├── Guardrail logic createTestOrchestrator with guardrails
14
-
11
+ ├── Single-agent runs (unit/integration) → createTestOrchestrator
12
+ ├── Multi-agent patterns → createTestMultiAgentOrchestrator
13
+ ├── Just the runner contract createMockAgentRunner
14
+ ├── Approval workflow createApprovalSimulator (built into createTestOrchestrator)
15
+ ├── Breakpoint pauses createBreakpointSimulator
16
+ ├── DAG topology → createTestDag + assertDagExecution
17
+ ├── Failure modes (timeouts, errors) → createFailingRunner
18
+ ├── Snapshot constraint behavior → createConstraintRecorder
19
+ └── Time-dependent logic → createTimeController
20
+
15
21
  What are you evaluating?
16
- ├── Output quality createEvaluator with criteria
17
- ├── Automated gradingLLM-as-judge evaluator
18
- ├── Regression testingdataset-driven evaluation
19
- ├── CI quality gates evaluation thresholds
20
-
21
- Where do I import from?
22
- ├── Test utilities '@directive-run/ai/testing'
23
- ├── Evaluators'@directive-run/ai/testing'
24
- └── Schema builders → '@directive-run/ai' (main, for t.*())
22
+ ├── Output quality createEvalSuite + eval* criteria
23
+ ├── Token / latency budgets evalCost + evalLatency
24
+ ├── Output shape / size evalOutputLength + evalStructure
25
+ ├── Safety / PII evalSafety
26
+ ├── Reference match → evalMatch
27
+ ├── Faithfulness / relevance / coherence → evalFaithfulness / evalRelevance / evalCoherence
28
+ ├── LLM-as-judge evalJudge
29
+ └── Custom assertion evalAssert
25
30
  ```
26
31
 
27
- ## Mock Runners
32
+ ## Mock runner
28
33
 
29
- Create deterministic runners that return predefined responses:
34
+ `createMockAgentRunner` returns `{ run, getCalls, getCallsFor, clearCalls, setResponse, setDefaultResponse }`. The factory takes a SINGLE options object — responses are keyed by **agent name**, NOT input pattern. (For input-pattern matching, use `generate` inside a response.) **The function is named `createMockAgentRunner`, not `createMockRunner`**.
30
35
 
31
36
  ```typescript
32
- import { createMockRunner } from "@directive-run/ai/testing";
33
-
34
- // Pattern-matched responses
35
- const mockRunner = createMockRunner([
36
- { input: /analyze/, output: "Analysis complete: positive trend", tokens: 100 },
37
- { input: /summarize/, output: "Summary: key findings are...", tokens: 50 },
38
- { input: /translate/, output: "Translation: Hola mundo", tokens: 30 },
39
- ]);
40
-
41
- // Catch-all default response
42
- const mockRunner = createMockRunner([
43
- { input: /specific/, output: "Matched specific" },
44
- { input: /.*/, output: "Default response", tokens: 10 },
45
- ]);
37
+ import { createMockAgentRunner } from "@directive-run/ai/testing";
38
+
39
+ const mock = createMockAgentRunner({
40
+ responses: {
41
+ analyst: { output: "Analysis: positive trend", totalTokens: 100 },
42
+ writer: { output: "Draft article…", totalTokens: 200, delay: 50 },
43
+ failing: { output: "n/a", error: new Error("simulated 503") },
44
+ },
45
+ defaultResponse: { output: "mock response", totalTokens: 10 },
46
+ recordCalls: true,
47
+ });
48
+
49
+ const orchestrator = createAgentOrchestrator({ runner: mock.run });
50
+
51
+ await orchestrator.run({ name: "analyst", instructions: "…", model: "x" }, "Analyze this");
52
+
53
+ mock.getCalls(); // RecordedCall[] — every recorded run
54
+ mock.getCallsFor("analyst"); // RecordedCall[] — only this agent
55
+ mock.setResponse("analyst", { output: "different reply", totalTokens: 80 });
56
+ mock.clearCalls();
46
57
  ```
47
58
 
48
- ### Mock Runner with Side Effects
59
+ For dynamic responses keyed on input, use the `generate` callback inside the config:
49
60
 
50
61
  ```typescript
51
- const mockRunner = createMockRunner([
52
- {
53
- input: /analyze/,
54
- output: "Analysis complete",
55
- tokens: 100,
56
- // Simulate tool calls
57
- toolCalls: [
58
- { name: "search", arguments: '{"query":"data"}', result: "found 5 items" },
59
- ],
60
- // Simulate latency
61
- delayMs: 200,
62
+ const mock = createMockAgentRunner({
63
+ responses: {
64
+ classifier: {
65
+ output: "fallback",
66
+ totalTokens: 10,
67
+ generate: (input) => /summarize/i.test(input)
68
+ ? { output: "Summary: …", totalTokens: 50 }
69
+ : { output: "Default reply", totalTokens: 10 },
70
+ },
62
71
  },
63
- ]);
72
+ });
64
73
  ```
65
74
 
66
- ## Test Orchestrator
75
+ ## Test orchestrator
67
76
 
68
- Lightweight orchestrator for unit testing:
77
+ `createTestOrchestrator` extends `AgentOrchestrator` with mock-runner wiring, an approval simulator, and call recording. Pass `mockResponses` and Directive constraints/resolvers exactly as you would to the production factory.
69
78
 
70
79
  ```typescript
71
- import { createTestOrchestrator, createMockRunner, t } from "@directive-run/ai/testing";
72
-
73
- const mockRunner = createMockRunner([
74
- { input: /analyze/, output: "Analysis: positive", tokens: 100 },
75
- ]);
80
+ import { createTestOrchestrator } from "@directive-run/ai/testing";
81
+ import { t } from "@directive-run/core";
76
82
 
77
- const orchestrator = createTestOrchestrator({
78
- runner: mockRunner,
83
+ const test = createTestOrchestrator({
84
+ mockResponses: {
85
+ analyst: { output: "Analysis: positive trend", totalTokens: 100 },
86
+ },
79
87
  factsSchema: {
80
88
  result: t.string(),
81
89
  confidence: t.number(),
@@ -92,287 +100,380 @@ const orchestrator = createTestOrchestrator({
92
100
  },
93
101
  resolvers: {
94
102
  reAnalyze: {
95
- requirement: "RE_ANALYZE",
96
- resolve: async (req, context) => {
97
- context.facts.confidence = 0.8;
98
- },
103
+ requirement: (r): r is { type: "RE_ANALYZE" } => r.type === "RE_ANALYZE",
104
+ resolve: async (req, context) => { context.facts.confidence = 0.8; },
99
105
  },
100
106
  },
101
107
  });
102
108
 
103
- const agent = {
104
- name: "analyst",
105
- instructions: "You are a data analyst.",
106
- model: "claude-sonnet-4-5",
107
- };
108
-
109
- const result = await orchestrator.run(agent, "Analyze this dataset");
110
- ```
111
-
112
- ## Assertion Helpers
113
-
114
- Verify agent behavior after a test run:
109
+ const result = await test.run({ name: "analyst", instructions: "…", model: "x" }, "Analyze");
115
110
 
116
- ```typescript
117
- import {
118
- assertAgentCalled,
119
- assertAgentNotCalled,
120
- assertTokensUsed,
121
- assertGuardrailPassed,
122
- assertGuardrailBlocked,
123
- } from "@directive-run/ai/testing";
124
-
125
- // Assert an agent was called with a matching input
126
- assertAgentCalled(mockRunner, "analyst");
127
- assertAgentCalled(mockRunner, "analyst", /analyze/);
128
-
129
- // Assert an agent was NOT called
130
- assertAgentNotCalled(mockRunner, "editor");
131
-
132
- // Assert token usage within bounds
133
- assertTokensUsed(result, { min: 50, max: 200 });
134
-
135
- // Assert guardrail behavior
136
- assertGuardrailPassed(result, "pii-detection");
137
- assertGuardrailBlocked(result, "content-filter");
111
+ test.getCalls(); // RecordedCall[]
112
+ test.getApprovalRequests(); // ApprovalRequest[] from the built-in simulator
113
+ test.mockRunner; // the underlying MockAgentRunner
114
+ test.approvalSimulator; // the built-in ApprovalSimulator
115
+ test.resetAll(); // reset orchestrator + clear calls + clear approvals
138
116
  ```
139
117
 
140
- ## Test Multi-Agent Orchestrator
118
+ ## Test multi-agent orchestrator
141
119
 
142
120
  ```typescript
143
121
  import {
144
122
  createTestMultiAgentOrchestrator,
145
- createMockRunner,
123
+ createMockAgentRunner,
146
124
  assertMultiAgentState,
147
125
  } from "@directive-run/ai/testing";
126
+ import { sequential } from "@directive-run/ai/multi-agent";
148
127
 
149
- const mockRunner = createMockRunner([
150
- { input: /research/, output: "Research findings: ...", tokens: 150 },
151
- { input: /write/, output: "Draft article: ...", tokens: 200 },
152
- ]);
128
+ const mock = createMockAgentRunner({
129
+ responses: {
130
+ researcher: { output: "research notes", totalTokens: 150 },
131
+ writer: { output: "draft article", totalTokens: 200 },
132
+ },
133
+ });
153
134
 
154
- const orchestrator = createTestMultiAgentOrchestrator({
135
+ const orch = createTestMultiAgentOrchestrator({
155
136
  agents: {
156
- researcher: { name: "researcher", instructions: "Research.", model: "claude-sonnet-4-5" },
157
- writer: { name: "writer", instructions: "Write.", model: "claude-sonnet-4-5" },
137
+ researcher: { name: "researcher", instructions: "research", model: "x" },
138
+ writer: { name: "writer", instructions: "write", model: "x" },
158
139
  },
159
140
  patterns: {
160
141
  pipeline: sequential(["researcher", "writer"]),
161
142
  },
162
- runner: mockRunner,
143
+ runner: mock.run,
163
144
  });
164
145
 
165
- orchestrator.start();
166
- const result = await orchestrator.runPattern("pipeline", "Write about AI");
146
+ orch.start();
147
+ await orch.runPattern("pipeline", "Write about AI");
167
148
 
168
- // Assert multi-agent state
169
- assertMultiAgentState(orchestrator, {
170
- completedAgents: ["researcher", "writer"],
171
- activePattern: null,
149
+ assertMultiAgentState(orch, {
150
+ status: "completed",
172
151
  });
152
+ ```
173
153
 
174
- assertAgentCalled(mockRunner, "researcher");
175
- assertAgentCalled(mockRunner, "writer");
154
+ ## Snapshot / introspection helpers
155
+
156
+ | Helper | Purpose |
157
+ |---|---|
158
+ | `createConstraintRecorder()` | Records every constraint evaluation as a `ConstraintSnapshot`; pair with `assertOrchestratorState`. |
159
+ | `assertOrchestratorState(orch, expected)` | Diffs facts, status, and constraint state against an expected shape. |
160
+ | `assertMultiAgentState(orch, expected)` | Same shape, for multi-agent. |
161
+ | `assertScratchpadState(orch, expected)` | Checks the multi-agent scratchpad contents. |
162
+ | `assertDerivedValues(orch, expected)` | Diffs derivation values. |
163
+ | `assertTimelineEvents(timeline, expected)` | Asserts a sequence of timeline event types/payloads. |
164
+ | `assertDagExecution(events, expected)` | Asserts DAG node execution order. |
165
+ | `assertBreakpointHit(events, expected)` | Asserts a breakpoint was hit with given metadata. |
166
+ | `assertRerouted(events, expected)` | Asserts a self-healing reroute fired (when configured). |
167
+ | `assertAgentHealth(monitor, agentId, expected)` | Asserts a health score range / passing failed counts. |
168
+ | `assertCheckpoint(cp, expected)` | Asserts checkpoint shape (state version, conversation length, etc.). |
169
+ | `assertMultiplexedStream(stream, expected)` | Asserts a multiplexed stream produced expected per-agent chunks. |
170
+
171
+ There is no `assertAgentCalled` / `assertAgentNotCalled` / `assertTokensUsed` / `assertGuardrailPassed` / `assertGuardrailBlocked` — those don't exist. Use `mock.getCalls()` + `expect()` from your test framework, and inspect `result.tokenUsage` and `result.guardrailEvents` directly.
172
+
173
+ ```typescript
174
+ import { expect } from "vitest";
175
+
176
+ const calls = mock.getCallsFor("analyst");
177
+ expect(calls).toHaveLength(1);
178
+ expect(calls[0].input).toMatch(/analyze/i);
179
+
180
+ expect(result.tokenUsage).toBeLessThan(500);
176
181
  ```
177
182
 
178
183
  ## Simulators
179
184
 
180
- Simulate specific conditions for testing edge cases:
181
-
182
185
  ```typescript
183
- import { createErrorSimulator, createLatencySimulator } from "@directive-run/ai/testing";
186
+ import {
187
+ createFailingRunner,
188
+ createApprovalSimulator,
189
+ createBreakpointSimulator,
190
+ createTimeController,
191
+ createTestCheckpointStore,
192
+ createTestReflectionEvaluator,
193
+ createTestEmbedder,
194
+ } from "@directive-run/ai/testing";
195
+ ```
196
+
197
+ ### `createFailingRunner(error?, options?)`
198
+
199
+ Returns an `AgentRunner` that throws (or fails after N successes). Replaces the imagined `createErrorSimulator` / `createLatencySimulator`.
184
200
 
185
- // Simulate errors on specific calls
186
- const errorRunner = createErrorSimulator(baseRunner, {
187
- failOnCall: [2, 5], // Fail on 2nd and 5th calls
188
- error: new Error("Rate limit exceeded"),
201
+ ```typescript
202
+ const failing = createFailingRunner(new Error("rate_limit"), {
203
+ delay: 100,
204
+ failAfter: 2, // first 2 calls succeed, then it starts throwing
189
205
  });
190
206
 
191
- // Simulate variable latency
192
- const slowRunner = createLatencySimulator(baseRunner, {
193
- minDelay: 100,
194
- maxDelay: 2000,
195
- distribution: "normal", // "uniform" | "normal"
207
+ const orchestrator = createAgentOrchestrator({
208
+ runner: failing,
209
+ selfHealing: { fallbackRunners: [backupRunner] },
196
210
  });
197
211
  ```
198
212
 
199
- ---
200
-
201
- ## Evaluation Framework
213
+ For latency-only simulation, set `delay` on a `MockAgentConfig`:
202
214
 
203
- Measure and gate AI output quality with structured evaluations.
215
+ ```typescript
216
+ const slow = createMockAgentRunner({
217
+ responses: { writer: { output: "slow reply", totalTokens: 50, delay: 800 } },
218
+ });
219
+ ```
204
220
 
205
- ### Built-In Criteria
221
+ ### `createApprovalSimulator(options?)`
206
222
 
207
- 10+ evaluation criteria available out of the box:
223
+ Drives the approval workflow deterministically auto-approve, auto-reject, or queue.
208
224
 
209
225
  ```typescript
210
- import { createEvaluator, criteria } from "@directive-run/ai/testing";
211
-
212
- const evaluator = createEvaluator({
213
- criteria: [
214
- criteria.relevance(), // Is the output relevant to the input?
215
- criteria.coherence(), // Is the output logically coherent?
216
- criteria.completeness(), // Does it fully address the prompt?
217
- criteria.accuracy(), // Is the information correct?
218
- criteria.conciseness(), // Is it free of unnecessary content?
219
- criteria.helpfulness(), // Is it useful to the user?
220
- criteria.harmlessness(), // Is it free of harmful content?
221
- criteria.factuality(), // Are claims factually supported?
222
- criteria.creativity(), // Does it show original thinking?
223
- criteria.instructionFollow(),// Does it follow the prompt instructions?
224
- ],
226
+ const sim = createApprovalSimulator({ defaultAction: "approve" });
227
+
228
+ const orchestrator = createAgentOrchestrator({
229
+ runner,
230
+ autoApproveToolCalls: false,
231
+ onApprovalRequest: (req) => sim.handle(req),
225
232
  });
233
+
234
+ const requests = sim.getRequests();
235
+ sim.clearRequests();
226
236
  ```
227
237
 
228
- ### Custom Criteria
238
+ ### `createBreakpointSimulator(options?)`
239
+
240
+ Resolves breakpoints automatically, with optional modifications. Pair with the orchestrator's `breakpoints:` config.
241
+
242
+ ### `createTimeController(startTime?)`
243
+
244
+ Drives `Date.now()` and `setTimeout` deterministically — required when testing budget windows, retry backoffs, or memory expiry.
245
+
246
+ ### `createTestCheckpointStore()`
247
+
248
+ In-memory checkpoint store for round-tripping `checkpoint()` + `restore()` without disk IO.
249
+
250
+ ### `createTestReflectionEvaluator(options?)`
251
+
252
+ Deterministic evaluator for `reflect()` patterns — scores can be hard-coded per iteration.
253
+
254
+ ### `createTestEmbedder()`
255
+
256
+ Deterministic embedder (no network calls) for `createSemanticCache` testing.
257
+
258
+ ## Evaluation framework
259
+
260
+ `createEvalSuite` runs a dataset of test cases through one or more agents, scores each output with one or more `EvalCriterion`, and returns aggregate results. Criteria are NOT a `criteria.*()` namespace — they're top-level `eval*` factories.
229
261
 
230
262
  ```typescript
231
- const evaluator = createEvaluator({
232
- criteria: [
233
- {
234
- name: "code-quality",
235
- description: "Does the output contain valid, well-structured code?",
236
- scorer: (input, output) => {
237
- const hasCode = output.includes("function") || output.includes("const ");
238
- const hasExplanation = output.length > 100;
239
-
240
- if (hasCode && hasExplanation) {
241
- return { score: 1.0, reason: "Contains code with explanation" };
242
- }
243
- if (hasCode) {
244
- return { score: 0.7, reason: "Code present but no explanation" };
245
- }
246
-
247
- return { score: 0.2, reason: "No code block found" };
248
- },
249
- },
263
+ import {
264
+ createEvalSuite,
265
+ evalCost,
266
+ evalLatency,
267
+ evalOutputLength,
268
+ evalSafety,
269
+ evalStructure,
270
+ evalMatch,
271
+ evalJudge,
272
+ evalFaithfulness,
273
+ evalRelevance,
274
+ evalCoherence,
275
+ evalAssert,
276
+ } from "@directive-run/ai/evals";
277
+
278
+ const suite = createEvalSuite({
279
+ agents: [analystAgent, writerAgent],
280
+ runner,
281
+ dataset: [
282
+ { id: "ts-basics", input: "What is TypeScript?", expected: "TypeScript is…", tags: ["basics"] },
283
+ { id: "monads", input: "Explain monads", expected: "A monad is…", tags: ["advanced"] },
284
+ { id: "ssr-tradeoffs", input: "Server vs client rendering", expected: "Tradeoffs…", tags: ["advanced"] },
250
285
  ],
286
+ criteria: {
287
+ cost: evalCost({ maxTokensPerRun: 500 }),
288
+ latency: evalLatency({ maxMs: 5000 }),
289
+ safety: evalSafety(),
290
+ relevance: evalRelevance({ embedder: embedderFn, minSimilarity: 0.7 }),
291
+ judge: evalJudge({ runner, judge: judgeAgent }),
292
+ },
293
+ concurrency: 4,
294
+ onCaseComplete: (caseResult) => console.log(`✓ ${caseResult.caseId} — ${caseResult.score.toFixed(2)}`),
251
295
  });
252
- ```
253
296
 
254
- ### Anti-Pattern #32: Side effects in evaluator scorer
297
+ const results = await suite.run();
255
298
 
256
- ```typescript
257
- // WRONG – scorers must be pure functions
258
- {
259
- name: "quality",
260
- scorer: (input, output) => {
261
- // Side effects: writing files, calling APIs, mutating state
262
- fs.writeFileSync("eval.log", output);
263
- metrics.increment("evals");
299
+ console.log(results.summary.averageScore); // 0.82
300
+ console.log(results.summary.passRate); // 0.90
301
+ console.log(results.cases); // EvalCaseResult[]
302
+ console.log(results.agentSummaries); // EvalAgentSummary[] (per-agent breakdown)
303
+ console.log(results.totalTokens, results.durationMs);
304
+ ```
264
305
 
265
- return { score: 0.8, reason: "OK" };
266
- },
267
- }
306
+ ### Custom criterion
268
307
 
269
- // CORRECT scorers are pure, return score + reason only
270
- {
271
- name: "quality",
272
- scorer: (input, output) => {
273
- const wordCount = output.split(/\s+/).length;
274
- const isDetailed = wordCount > 50;
308
+ A criterion is `{ name, fn, threshold?, weight? }` where `fn` returns `{ score, passed?, reason?, durationMs? }`. **Scorers must be pure** — no side effects.
275
309
 
310
+ ```typescript
311
+ const codeQuality = {
312
+ name: "code-quality",
313
+ threshold: 0.6,
314
+ weight: 1.0,
315
+ fn: (ctx) => {
316
+ const start = Date.now();
317
+ const output = String(ctx.result.output);
318
+ const hasCode = /\b(function|const|class)\b/.test(output);
319
+ const hasExplanation = output.length > 100;
320
+
321
+ const score = hasCode && hasExplanation ? 1.0 : hasCode ? 0.7 : 0.2;
276
322
  return {
277
- score: isDetailed ? 1.0 : 0.5,
278
- reason: isDetailed ? "Detailed response" : "Too brief",
323
+ score,
324
+ passed: score >= 0.6,
325
+ reason: hasCode && hasExplanation ? "code with explanation"
326
+ : hasCode ? "code only"
327
+ : "no code",
328
+ durationMs: Date.now() - start,
279
329
  };
280
330
  },
281
- }
282
- ```
331
+ };
283
332
 
284
- ### LLM-as-Judge
333
+ const suite = createEvalSuite({
334
+ agents: [coderAgent],
335
+ runner,
336
+ dataset,
337
+ criteria: { codeQuality },
338
+ });
339
+ ```
285
340
 
286
- Use an LLM to evaluate output quality:
341
+ ### LLM-as-judge
287
342
 
288
343
  ```typescript
289
- import { createLLMJudge } from "@directive-run/ai/testing";
344
+ import { evalJudge } from "@directive-run/ai/evals";
290
345
 
291
- const judge = createLLMJudge({
292
- runner,
293
- model: "claude-sonnet-4-5",
294
- criteria: ["relevance", "accuracy", "completeness"],
295
- rubric: `
296
- Score 1.0: Fully addresses the prompt with accurate, complete information.
297
- Score 0.7: Mostly accurate but missing some details.
298
- Score 0.3: Partially relevant, significant gaps.
299
- Score 0.0: Irrelevant or incorrect.
300
- `,
346
+ const judge = evalJudge({
347
+ runner, // any AgentRunner — typically a smaller/cheaper model
348
+ judge: judgeAgent, // the AgentLike to invoke
349
+ promptTemplate: `…`, // optional — defaults to a JSON-output rubric
350
+ timeoutMs: 30_000,
301
351
  });
352
+ ```
302
353
 
303
- const evalResult = await judge.evaluate({
304
- input: "Explain quantum computing",
305
- output: agentOutput,
306
- reference: "Optional reference answer for comparison",
307
- });
354
+ The judge returns `{ score: number, reason?: string }` parsed from JSON. The default prompt enforces "Respond with ONLY a JSON object: …" — override `promptTemplate` if you need a different rubric.
355
+
356
+ ### CI quality gates
357
+
358
+ ```typescript
359
+ const results = await suite.run();
360
+
361
+ if (results.summary.averageScore < 0.75) {
362
+ console.error(`Quality gate failed: ${results.summary.averageScore} < 0.75`);
363
+ process.exit(1);
364
+ }
308
365
 
309
- console.log(evalResult.score); // 0.85
310
- console.log(evalResult.reason); // "Accurate explanation with good examples..."
366
+ for (const [criterionName, summary] of Object.entries(results.summary.byCriterion)) {
367
+ if (summary.averageScore < 0.6) {
368
+ console.error(`${criterionName} regressed: ${summary.averageScore}`);
369
+ process.exit(1);
370
+ }
371
+ }
311
372
  ```
312
373
 
313
- ### Dataset-Driven Evaluation
374
+ ## Anti-patterns
314
375
 
315
- Run evaluations against a dataset for regression testing:
376
+ ### Calling `createMockRunner` (singular, with input-pattern array)
316
377
 
317
378
  ```typescript
318
- import { createEvaluationSuite } from "@directive-run/ai/testing";
379
+ // WRONG createMockRunner does not exist; the array-pattern form does not exist
380
+ createMockRunner([{ input: /analyze/, output: "…" }])
319
381
 
320
- const suite = createEvaluationSuite({
321
- evaluator,
322
- dataset: [
323
- {
324
- input: "What is TypeScript?",
325
- expectedOutput: "TypeScript is a typed superset of JavaScript...",
326
- tags: ["basics"],
327
- },
328
- {
329
- input: "Explain monads",
330
- expectedOutput: "A monad is a design pattern...",
331
- tags: ["advanced"],
332
- },
333
- ],
334
- });
382
+ // CORRECT keyed by agent name
383
+ createMockAgentRunner({
384
+ responses: { analyst: { output: "…", totalTokens: 100 } },
385
+ })
386
+ ```
335
387
 
336
- const report = await suite.run(agent, runner);
388
+ ### Importing hallucinated assertions
337
389
 
338
- console.log(report.averageScore); // 0.82
339
- console.log(report.passRate); // 0.90 (90% above threshold)
340
- console.log(report.failedCases); // Cases that scored below threshold
390
+ ```typescript
391
+ // WRONG none of these exist
392
+ import {
393
+ assertAgentCalled,
394
+ assertAgentNotCalled,
395
+ assertTokensUsed,
396
+ assertGuardrailPassed,
397
+ assertGuardrailBlocked,
398
+ } from "@directive-run/ai/testing";
399
+
400
+ // CORRECT — use the mock's recorder + your test framework's assertions
401
+ const calls = mock.getCallsFor("analyst");
402
+ expect(calls).toHaveLength(1);
403
+ expect(result.tokenUsage).toBeLessThan(200);
341
404
  ```
342
405
 
343
- ### CI Quality Gates
406
+ ### Importing `createEvaluator`, `criteria.*()`, `createLLMJudge`, `createEvaluationSuite`
344
407
 
345
- Fail CI pipelines when quality drops below a threshold:
408
+ ```typescript
409
+ // WRONG — these are all hallucinated names
410
+ import { createEvaluator, criteria, createLLMJudge, createEvaluationSuite } from "@directive-run/ai/testing";
411
+ const evaluator = createEvaluator({ criteria: [criteria.relevance(), criteria.coherence()] });
412
+ const judge = createLLMJudge({ runner, model: "…", criteria: ["accuracy"], rubric: "…" });
413
+
414
+ // CORRECT — top-level eval factories + createEvalSuite
415
+ import { createEvalSuite, evalRelevance, evalCoherence, evalJudge } from "@directive-run/ai/evals";
416
+ const suite = createEvalSuite({
417
+ agents: [agent],
418
+ runner,
419
+ dataset,
420
+ criteria: {
421
+ relevance: evalRelevance({ embedder, minSimilarity: 0.7 }),
422
+ coherence: evalCoherence({ embedder, minSimilarity: 0.5 }),
423
+ judge: evalJudge({ runner, judge: judgeAgent }),
424
+ },
425
+ });
426
+ ```
427
+
428
+ ### Importing `createErrorSimulator` / `createLatencySimulator`
346
429
 
347
430
  ```typescript
348
- const report = await suite.run(agent, runner);
431
+ // WRONG neither exists
432
+ import { createErrorSimulator, createLatencySimulator } from "@directive-run/ai/testing";
349
433
 
350
- // Threshold-based gate
351
- if (report.averageScore < 0.75) {
352
- console.error(`Quality gate failed: ${report.averageScore} < 0.75`);
353
- process.exit(1);
354
- }
434
+ // CORRECT — createFailingRunner for errors; delay in MockAgentConfig for latency
435
+ import { createFailingRunner, createMockAgentRunner } from "@directive-run/ai/testing";
436
+ const failing = createFailingRunner(new Error("503"), { failAfter: 2, delay: 100 });
437
+ const slow = createMockAgentRunner({ responses: { writer: { output: "…", delay: 800 } } });
438
+ ```
355
439
 
356
- // Per-criteria gates
357
- for (const criterion of report.criteria) {
358
- if (criterion.averageScore < 0.6) {
359
- console.error(`${criterion.name} failed: ${criterion.averageScore}`);
360
- process.exit(1);
361
- }
440
+ ### Side-effecting scorers
441
+
442
+ ```typescript
443
+ // WRONG — scorers run inside the eval suite and must be pure
444
+ {
445
+ name: "quality",
446
+ fn: (ctx) => {
447
+ fs.writeFileSync("eval.log", String(ctx.result.output)); // ← side effect
448
+ metrics.increment("evals"); // ← side effect
449
+ return { score: 0.8 };
450
+ },
362
451
  }
452
+
453
+ // CORRECT — log/metrics in onCaseComplete or after suite.run()
454
+ const suite = createEvalSuite({
455
+ …,
456
+ onCaseComplete: (caseResult) => {
457
+ metrics.increment("evals");
458
+ fs.appendFileSync("eval.log", JSON.stringify(caseResult) + "\n");
459
+ },
460
+ });
363
461
  ```
364
462
 
365
- ## Quick Reference
463
+ ## Quick reference
366
464
 
367
- | API | Import Path | Purpose |
465
+ | API | Import path | Purpose |
368
466
  |---|---|---|
369
- | `createMockRunner` | `@directive-run/ai/testing` | Deterministic test runner |
370
- | `createTestOrchestrator` | `@directive-run/ai/testing` | Lightweight test orchestrator |
371
- | `createTestMultiAgentOrchestrator` | `@directive-run/ai/testing` | Multi-agent test orchestrator |
372
- | `assertAgentCalled` | `@directive-run/ai/testing` | Verify agent was invoked |
373
- | `assertMultiAgentState` | `@directive-run/ai/testing` | Verify multi-agent state |
374
- | `createEvaluator` | `@directive-run/ai/testing` | Rule-based evaluation |
375
- | `createLLMJudge` | `@directive-run/ai/testing` | LLM-as-judge evaluation |
376
- | `createEvaluationSuite` | `@directive-run/ai/testing` | Dataset-driven evaluation |
377
- | `createErrorSimulator` | `@directive-run/ai/testing` | Simulate failures |
378
- | `createLatencySimulator` | `@directive-run/ai/testing` | Simulate latency |
467
+ | `createMockAgentRunner` | `@directive-run/ai/testing` | Deterministic mock runner with call recording |
468
+ | `createTestOrchestrator` | `@directive-run/ai/testing` | Single-agent orchestrator with mock + approval simulator |
469
+ | `createTestMultiAgentOrchestrator` | `@directive-run/ai/testing` | Multi-agent orchestrator wired to a mock runner |
470
+ | `createFailingRunner` | `@directive-run/ai/testing` | Runner that throws (or fails after N successes) |
471
+ | `createApprovalSimulator` | `@directive-run/ai/testing` | Deterministic approve/reject driver |
472
+ | `createBreakpointSimulator` | `@directive-run/ai/testing` | Deterministic breakpoint resolver |
473
+ | `createTimeController` | `@directive-run/ai/testing` | Virtual clock for budgets / backoffs / TTLs |
474
+ | `createTestCheckpointStore` | `@directive-run/ai/testing` | In-memory checkpoint store |
475
+ | `createTestReflectionEvaluator` | `@directive-run/ai/testing` | Deterministic evaluator for `reflect()` |
476
+ | `createTestEmbedder` | `@directive-run/ai/testing` | Deterministic embedder for cache tests |
477
+ | `assertOrchestratorState` / `assertMultiAgentState` / `assertScratchpadState` / `assertDerivedValues` / `assertTimelineEvents` / `assertDagExecution` / `assertBreakpointHit` / `assertRerouted` / `assertAgentHealth` / `assertCheckpoint` / `assertMultiplexedStream` | `@directive-run/ai/testing` | State/topology/event assertions |
478
+ | `createEvalSuite` | `@directive-run/ai/evals` | Dataset-driven multi-criterion eval runner |
479
+ | `evalCost` / `evalLatency` / `evalOutputLength` / `evalSafety` / `evalStructure` / `evalMatch` / `evalJudge` / `evalFaithfulness` / `evalRelevance` / `evalCoherence` / `evalAssert` | `@directive-run/ai/evals` | Built-in criterion factories |