@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.
- package/README.md +50 -15
- package/ai/ai-adapters.md +2 -0
- package/ai/ai-agents-streaming.md +182 -149
- package/ai/ai-budget-resilience.md +299 -132
- package/ai/ai-communication.md +215 -197
- package/ai/ai-debug-observability.md +253 -173
- package/ai/ai-guardrails-memory.md +185 -153
- package/ai/ai-mcp-rag.md +199 -199
- package/ai/ai-multi-agent.md +247 -153
- package/ai/ai-orchestrator.md +344 -114
- package/ai/ai-security.md +282 -180
- package/ai/ai-tasks.md +3 -1
- package/ai/ai-testing-evals.md +357 -256
- package/core/anti-patterns.md +9 -2
- package/core/constraints.md +6 -2
- package/core/core-patterns.md +2 -0
- package/core/error-boundaries.md +2 -0
- package/core/history.md +2 -0
- package/core/multi-module.md +8 -3
- package/core/naming.md +15 -6
- package/core/plugins.md +2 -0
- package/core/react-adapter.md +250 -174
- package/core/resolvers.md +2 -0
- package/core/schema-types.md +2 -0
- package/core/system-api.md +2 -0
- package/core/testing.md +251 -143
- package/examples/checkers.ts +15 -16
- package/examples/contact-form.ts +2 -2
- package/examples/counter-react.ts +1 -1
- package/examples/counter-svelte.ts +1 -1
- package/examples/counter-vue.ts +1 -1
- package/examples/counter.ts +1 -1
- package/examples/data-triggers.ts +4 -4
- package/examples/feature-flags.ts +2 -2
- package/examples/form-wizard.ts +2 -2
- package/examples/newsletter.ts +2 -2
- package/examples/server.ts +2 -2
- package/examples/shopping-cart.ts +1 -1
- package/examples/topic-guard.ts +1 -1
- package/package.json +3 -3
- package/sitemap.md +26 -3
- package/examples/debounce-constraints.ts +0 -95
- package/examples/multi-module.ts +0 -58
package/ai/ai-testing-evals.md
CHANGED
|
@@ -1,81 +1,89 @@
|
|
|
1
|
-
# AI
|
|
1
|
+
# AI testing and evaluations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Covers `@directive-run/ai/testing` and `@directive-run/ai/evals` — `createMockAgentRunner`, `createTestOrchestrator`, simulators, `createEvalSuite` + `eval*` criteria.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
10
|
-
├── Multi-agent patterns
|
|
11
|
-
├──
|
|
12
|
-
├──
|
|
13
|
-
├──
|
|
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
|
|
17
|
-
├──
|
|
18
|
-
├──
|
|
19
|
-
├──
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
├──
|
|
23
|
-
|
|
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
|
|
32
|
+
## Mock runner
|
|
28
33
|
|
|
29
|
-
|
|
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 {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
59
|
+
For dynamic responses keyed on input, use the `generate` callback inside the config:
|
|
49
60
|
|
|
50
61
|
```typescript
|
|
51
|
-
const
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
75
|
+
## Test orchestrator
|
|
67
76
|
|
|
68
|
-
|
|
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
|
|
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
|
|
78
|
-
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
118
|
+
## Test multi-agent orchestrator
|
|
141
119
|
|
|
142
120
|
```typescript
|
|
143
121
|
import {
|
|
144
122
|
createTestMultiAgentOrchestrator,
|
|
145
|
-
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
135
|
+
const orch = createTestMultiAgentOrchestrator({
|
|
155
136
|
agents: {
|
|
156
|
-
researcher: { name: "researcher", instructions: "
|
|
157
|
-
writer:
|
|
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:
|
|
143
|
+
runner: mock.run,
|
|
163
144
|
});
|
|
164
145
|
|
|
165
|
-
|
|
166
|
-
|
|
146
|
+
orch.start();
|
|
147
|
+
await orch.runPattern("pipeline", "Write about AI");
|
|
167
148
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
completedAgents: ["researcher", "writer"],
|
|
171
|
-
activePattern: null,
|
|
149
|
+
assertMultiAgentState(orch, {
|
|
150
|
+
status: "completed",
|
|
172
151
|
});
|
|
152
|
+
```
|
|
173
153
|
|
|
174
|
-
|
|
175
|
-
|
|
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 {
|
|
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
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
215
|
+
```typescript
|
|
216
|
+
const slow = createMockAgentRunner({
|
|
217
|
+
responses: { writer: { output: "slow reply", totalTokens: 50, delay: 800 } },
|
|
218
|
+
});
|
|
219
|
+
```
|
|
204
220
|
|
|
205
|
-
###
|
|
221
|
+
### `createApprovalSimulator(options?)`
|
|
206
222
|
|
|
207
|
-
|
|
223
|
+
Drives the approval workflow deterministically — auto-approve, auto-reject, or queue.
|
|
208
224
|
|
|
209
225
|
```typescript
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
297
|
+
const results = await suite.run();
|
|
255
298
|
|
|
256
|
-
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
266
|
-
},
|
|
267
|
-
}
|
|
306
|
+
### Custom criterion
|
|
268
307
|
|
|
269
|
-
|
|
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
|
|
278
|
-
|
|
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
|
-
|
|
333
|
+
const suite = createEvalSuite({
|
|
334
|
+
agents: [coderAgent],
|
|
335
|
+
runner,
|
|
336
|
+
dataset,
|
|
337
|
+
criteria: { codeQuality },
|
|
338
|
+
});
|
|
339
|
+
```
|
|
285
340
|
|
|
286
|
-
|
|
341
|
+
### LLM-as-judge
|
|
287
342
|
|
|
288
343
|
```typescript
|
|
289
|
-
import {
|
|
344
|
+
import { evalJudge } from "@directive-run/ai/evals";
|
|
290
345
|
|
|
291
|
-
const judge =
|
|
292
|
-
runner,
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
374
|
+
## Anti-patterns
|
|
314
375
|
|
|
315
|
-
|
|
376
|
+
### Calling `createMockRunner` (singular, with input-pattern array)
|
|
316
377
|
|
|
317
378
|
```typescript
|
|
318
|
-
|
|
379
|
+
// WRONG — createMockRunner does not exist; the array-pattern form does not exist
|
|
380
|
+
createMockRunner([{ input: /analyze/, output: "…" }])
|
|
319
381
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
388
|
+
### Importing hallucinated assertions
|
|
337
389
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
###
|
|
406
|
+
### Importing `createEvaluator`, `criteria.*()`, `createLLMJudge`, `createEvaluationSuite`
|
|
344
407
|
|
|
345
|
-
|
|
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
|
-
|
|
431
|
+
// WRONG — neither exists
|
|
432
|
+
import { createErrorSimulator, createLatencySimulator } from "@directive-run/ai/testing";
|
|
349
433
|
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
|
463
|
+
## Quick reference
|
|
366
464
|
|
|
367
|
-
| API | Import
|
|
465
|
+
| API | Import path | Purpose |
|
|
368
466
|
|---|---|---|
|
|
369
|
-
| `
|
|
370
|
-
| `createTestOrchestrator` | `@directive-run/ai/testing` |
|
|
371
|
-
| `createTestMultiAgentOrchestrator` | `@directive-run/ai/testing` | Multi-agent
|
|
372
|
-
| `
|
|
373
|
-
| `
|
|
374
|
-
| `
|
|
375
|
-
| `
|
|
376
|
-
| `
|
|
377
|
-
| `
|
|
378
|
-
| `
|
|
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 |
|