@crewhaus/eval-runner 0.2.3 → 0.3.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/dist/exam.d.ts +66 -0
- package/dist/exam.js +202 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +63 -4
- package/dist/run-sample.d.ts +3 -0
- package/dist/run-sample.js +13 -1
- package/dist/types.d.ts +16 -0
- package/dist/wire-once.js +6 -3
- package/package.json +36 -30
package/dist/exam.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ExamRunner, MemoryWiringFragment, ThredzConnection } from "@crewhaus/memory-service";
|
|
2
|
+
import type { RunContext } from "@crewhaus/run-context";
|
|
3
|
+
import type { AgentInvoker } from "./types";
|
|
4
|
+
/** The exam-session preamble appended to the harness instructions — the
|
|
5
|
+
* grounding contract the demo's graders (`grounded_not_bluffed`) test. */
|
|
6
|
+
export declare const EXAM_SESSION_PREAMBLE: string;
|
|
7
|
+
/** The exact runChatLoop option subset an exam session uses — a dedicated
|
|
8
|
+
* type so tests can inject a capturing `chatLoop` and pin every field
|
|
9
|
+
* (the dream-cli `DreamChatLoopOptions` pattern). */
|
|
10
|
+
export type ExamChatLoopOptions = {
|
|
11
|
+
readonly model: string;
|
|
12
|
+
readonly instructions: string;
|
|
13
|
+
readonly runContext: RunContext;
|
|
14
|
+
readonly singleTurn: true;
|
|
15
|
+
readonly seedMessages: ReadonlyArray<{
|
|
16
|
+
readonly role: "user";
|
|
17
|
+
readonly content: string;
|
|
18
|
+
}>;
|
|
19
|
+
readonly sessionName: string;
|
|
20
|
+
readonly sessionTarget: "eval";
|
|
21
|
+
readonly sessionRootDir: string;
|
|
22
|
+
readonly tools: ReadonlyArray<never>;
|
|
23
|
+
readonly hooks: ReadonlyArray<never>;
|
|
24
|
+
readonly memory?: {
|
|
25
|
+
readonly autoRecall: true;
|
|
26
|
+
readonly recallK: number;
|
|
27
|
+
readonly recallSeed: string;
|
|
28
|
+
readonly recall: (query: string, k: number) => Promise<readonly string[]>;
|
|
29
|
+
};
|
|
30
|
+
readonly spinner: false;
|
|
31
|
+
};
|
|
32
|
+
export type ExamChatLoopFn = (opts: ExamChatLoopOptions) => Promise<string>;
|
|
33
|
+
export type CreateExamRunnerOptions = {
|
|
34
|
+
readonly specName: string;
|
|
35
|
+
/** The harness's own model — exam sessions and (by default) the judge
|
|
36
|
+
* grader run on it. */
|
|
37
|
+
readonly model: string;
|
|
38
|
+
/** The harness's own instructions — the examinee is THIS harness. */
|
|
39
|
+
readonly instructions: string;
|
|
40
|
+
/** The memory fragment (`memoryFragmentFromIr(ir)`) — `wireWiki` grounds
|
|
41
|
+
* each question in the same wiki surface the harness runs with. */
|
|
42
|
+
readonly fragment: MemoryWiringFragment;
|
|
43
|
+
/** The harness working directory (stores + default artifact root). */
|
|
44
|
+
readonly cwd: string;
|
|
45
|
+
/** The live Thredz connection when the wiki backend is hosted (§4.3);
|
|
46
|
+
* null/absent → the local store (or no grounding when no wiki). */
|
|
47
|
+
readonly thredz?: ThredzConnection | null;
|
|
48
|
+
/** Judge model for `llm_judge` graders. Default: the harness model. */
|
|
49
|
+
readonly judgeModel?: string;
|
|
50
|
+
readonly concurrency?: number;
|
|
51
|
+
/** Wiki lines recalled per question. Default 6 (§9 `wiki.recallK`). */
|
|
52
|
+
readonly recallK?: number;
|
|
53
|
+
/** Artifact root override. Default `<cwd>/.crewhaus/evals/exam-<stamp>`. */
|
|
54
|
+
readonly outDir?: string;
|
|
55
|
+
/** Full invoker override (tests / bespoke examinees). */
|
|
56
|
+
readonly invoker?: AgentInvoker;
|
|
57
|
+
/** Session-runner seam under the default invoker (tests pin options). */
|
|
58
|
+
readonly chatLoop?: ExamChatLoopFn;
|
|
59
|
+
readonly now?: () => Date;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Build the injected exam runner (`wireMemory({ examRunner })`). Pure
|
|
63
|
+
* construction — nothing is read until the returned runner is invoked by
|
|
64
|
+
* the `run_exam` tool, so a harness that never sits its exam pays nothing.
|
|
65
|
+
*/
|
|
66
|
+
export declare function createExamRunner(opts: CreateExamRunnerOptions): ExamRunner;
|
package/dist/exam.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.3.0 Goal 2 (design §3.3 EXAM, PR 17) — `createExamRunner`: the
|
|
3
|
+
* reference implementation of memory-service's injected `ExamRunner` seam.
|
|
4
|
+
*
|
|
5
|
+
* The expert demo's `/exam` shelled out to `crewhaus eval` via Bash from
|
|
6
|
+
* inside the agent — the harness invoking its own CLI, requiring a Bash
|
|
7
|
+
* permission just to sit an exam. This is the first-class replacement: a
|
|
8
|
+
* PROGRAMMATIC eval invocation built from this package's own machinery
|
|
9
|
+
* (`loadDataset` + `parseGradersConfig` + `runSample`/`Semaphore`), consumed
|
|
10
|
+
* by the `run_exam` tool through `wireMemory({ examRunner })` on both the
|
|
11
|
+
* `crewhaus run` interpreter path and compiled cli bundles.
|
|
12
|
+
*
|
|
13
|
+
* Exam semantics: each dataset question runs in a FRESH single-turn session
|
|
14
|
+
* — the harness's own model + instructions plus an exam preamble — grounded
|
|
15
|
+
* in the REAL wiki through the backend-invariant `wireWiki(...).recall` seam
|
|
16
|
+
* (local store or Thredz MCP client; recall lines are injected through
|
|
17
|
+
* runChatLoop's `memory` seam, so recalled bodies still cross the boundary
|
|
18
|
+
* classifier + delimiter escaping like every other recall). No tools are
|
|
19
|
+
* exposed to the exam session: the question is "can you answer from what the
|
|
20
|
+
* wiki knows", graded by the spec's own graders — never "can you edit the
|
|
21
|
+
* wiki mid-exam". Unlike `runEval`'s per-sample fabric isolation (§7.2),
|
|
22
|
+
* the exam deliberately reads the live wiki — an isolated empty wiki would
|
|
23
|
+
* examine amnesia.
|
|
24
|
+
*
|
|
25
|
+
* Per-sample artifacts land under `.crewhaus/evals/exam-<stamp>/<sampleId>/`
|
|
26
|
+
* via `runSample` (transcript/events/grades/meta), so failed exams are
|
|
27
|
+
* triageable with the same tooling as any eval run.
|
|
28
|
+
*/
|
|
29
|
+
import { readFileSync } from "node:fs";
|
|
30
|
+
import { mkdirSync } from "node:fs";
|
|
31
|
+
import { join } from "node:path";
|
|
32
|
+
import { loadDataset } from "@crewhaus/eval-dataset";
|
|
33
|
+
import { parseGradersConfig } from "@crewhaus/eval-grader";
|
|
34
|
+
import { createJudgeGrader, loadRubric } from "@crewhaus/eval-judge";
|
|
35
|
+
import { wireWiki } from "@crewhaus/memory-service";
|
|
36
|
+
import { runChatLoop } from "@crewhaus/runtime-core";
|
|
37
|
+
import { RunnerError } from "./errors";
|
|
38
|
+
import { runSample } from "./run-sample";
|
|
39
|
+
import { Semaphore } from "./semaphore";
|
|
40
|
+
/** Exam sessions run a couple at a time — an exam is an interactive,
|
|
41
|
+
* user-initiated pass, not a benchmark sweep. */
|
|
42
|
+
const DEFAULT_EXAM_CONCURRENCY = 2;
|
|
43
|
+
/** Wiki lines recalled per question (mirrors §9's `wiki.recallK` default). */
|
|
44
|
+
const DEFAULT_EXAM_RECALL_K = 6;
|
|
45
|
+
/** The exam-session preamble appended to the harness instructions — the
|
|
46
|
+
* grounding contract the demo's graders (`grounded_not_bluffed`) test. */
|
|
47
|
+
export const EXAM_SESSION_PREAMBLE = [
|
|
48
|
+
"You are sitting your competency exam. Answer the question in this one turn:",
|
|
49
|
+
"- answer ONLY from the recalled wiki context above, citing article slugs like (topic/slug);",
|
|
50
|
+
"- numbers, ranges, and units must be exact;",
|
|
51
|
+
"- if the recalled context does not support an answer, say plainly what you do not know instead of guessing.",
|
|
52
|
+
].join("\n");
|
|
53
|
+
/**
|
|
54
|
+
* Build the injected exam runner (`wireMemory({ examRunner })`). Pure
|
|
55
|
+
* construction — nothing is read until the returned runner is invoked by
|
|
56
|
+
* the `run_exam` tool, so a harness that never sits its exam pays nothing.
|
|
57
|
+
*/
|
|
58
|
+
export function createExamRunner(opts) {
|
|
59
|
+
return async ({ datasetPath, gradersPath }) => {
|
|
60
|
+
// 1. Graders — parse the spec's graders.yaml; `llm_judge` placeholders
|
|
61
|
+
// bind to the judge model (per-grader override > opts.judgeModel > the
|
|
62
|
+
// harness model), exactly like runEval's resolution.
|
|
63
|
+
let gradersYaml;
|
|
64
|
+
try {
|
|
65
|
+
gradersYaml = readFileSync(gradersPath, "utf-8");
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
throw new RunnerError(`exam: cannot read graders "${gradersPath}" (learning.exam.graders): ${err.message}`);
|
|
69
|
+
}
|
|
70
|
+
const { compiled } = parseGradersConfig(gradersYaml);
|
|
71
|
+
const graders = compiled.map((g) => {
|
|
72
|
+
if (g.judgeSpec) {
|
|
73
|
+
const rubric = loadRubric(g.judgeSpec.rubric);
|
|
74
|
+
const model = g.judgeSpec.model ?? opts.judgeModel ?? opts.model;
|
|
75
|
+
return { name: g.name, grader: createJudgeGrader(rubric, { model }) };
|
|
76
|
+
}
|
|
77
|
+
// v0.3.0 final integration (PR 17 ∩ PR 19): `type: registry` graders
|
|
78
|
+
// resolve against RunEvalOptions.graderRegistry — a seam run_exam does
|
|
79
|
+
// not carry (the exam is invoked from inside a running harness, which
|
|
80
|
+
// has no grader registry to hand over). Reject LOUDLY at exam start,
|
|
81
|
+
// exactly like runEval does, instead of letting the compiled
|
|
82
|
+
// placeholder throw per-sample as confusing grader-infra noise.
|
|
83
|
+
if (g.registrySpec) {
|
|
84
|
+
throw new RunnerError(`exam: grader "${g.name}" is \`type: registry\` (→ "${g.registrySpec.grader}") — registry graders resolve via RunEvalOptions.graderRegistry under \`crewhaus eval\` and are not available to \`run_exam\`; use code/llm_judge graders in learning.exam.graders`);
|
|
85
|
+
}
|
|
86
|
+
return { name: g.name, grader: g.grader };
|
|
87
|
+
});
|
|
88
|
+
// 2. Dataset — materialized like runEval (exams are small by design).
|
|
89
|
+
let samples;
|
|
90
|
+
try {
|
|
91
|
+
const dataset = await loadDataset(datasetPath);
|
|
92
|
+
samples = [];
|
|
93
|
+
for await (const s of dataset.samples)
|
|
94
|
+
samples.push(s);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
throw err instanceof RunnerError
|
|
98
|
+
? err
|
|
99
|
+
: new RunnerError(`exam: cannot load dataset "${datasetPath}" (learning.exam.dataset): ${err.message}`);
|
|
100
|
+
}
|
|
101
|
+
if (samples.length === 0) {
|
|
102
|
+
throw new RunnerError(`exam: dataset "${datasetPath}" yielded zero samples`);
|
|
103
|
+
}
|
|
104
|
+
const invoker = opts.invoker ?? buildExamInvoker(opts);
|
|
105
|
+
const outDir = opts.outDir ?? join(opts.cwd, ".crewhaus", "evals", `exam-${examStamp(opts.now)}`);
|
|
106
|
+
mkdirSync(outDir, { recursive: true });
|
|
107
|
+
// 3. Run — per-sample artifacts + grading via the shared runSample path.
|
|
108
|
+
const sem = new Semaphore(opts.concurrency ?? DEFAULT_EXAM_CONCURRENCY);
|
|
109
|
+
const results = await Promise.all(samples.map(async (sample) => {
|
|
110
|
+
const release = await sem.acquire();
|
|
111
|
+
try {
|
|
112
|
+
return await runSample({
|
|
113
|
+
sample,
|
|
114
|
+
invoker,
|
|
115
|
+
graders,
|
|
116
|
+
outDir,
|
|
117
|
+
model: opts.model,
|
|
118
|
+
// PR 19's artifact seam: stamp the examinee's spec name so any
|
|
119
|
+
// artifact-reading grader addresses the right state root.
|
|
120
|
+
specName: opts.specName,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
release();
|
|
125
|
+
}
|
|
126
|
+
}));
|
|
127
|
+
const bySampleId = new Map(samples.map((s) => [s.id, s]));
|
|
128
|
+
const outcomes = results.map((r) => {
|
|
129
|
+
const sample = bySampleId.get(r.sampleId);
|
|
130
|
+
return {
|
|
131
|
+
sampleId: r.sampleId,
|
|
132
|
+
input: sample?.input ?? "",
|
|
133
|
+
passed: r.grades.overall.passed,
|
|
134
|
+
score: r.grades.overall.score,
|
|
135
|
+
rationale: r.grades.overall.rationale,
|
|
136
|
+
agentOutput: r.agentOutput,
|
|
137
|
+
...(r.error !== undefined ? { error: r.error } : {}),
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
const passed = outcomes.filter((o) => o.passed).length;
|
|
141
|
+
const report = {
|
|
142
|
+
total: outcomes.length,
|
|
143
|
+
passed,
|
|
144
|
+
failed: outcomes.length - passed,
|
|
145
|
+
passRate: outcomes.length === 0 ? 0 : passed / outcomes.length,
|
|
146
|
+
outcomes,
|
|
147
|
+
outDir,
|
|
148
|
+
};
|
|
149
|
+
await Bun.write(join(outDir, "exam.json"), JSON.stringify(report, null, 2));
|
|
150
|
+
return report;
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** The default examinee: one fresh single-turn session per question,
|
|
154
|
+
* wiki-grounded through runChatLoop's memory seam, zero tools. */
|
|
155
|
+
function buildExamInvoker(opts) {
|
|
156
|
+
const chatLoop = opts.chatLoop ?? runChatLoop;
|
|
157
|
+
const wiki = wireWiki({
|
|
158
|
+
specName: opts.fragment.specName,
|
|
159
|
+
...(opts.fragment.memory !== undefined ? { memory: opts.fragment.memory } : {}),
|
|
160
|
+
...(opts.fragment.thredz !== undefined ? { thredz: opts.fragment.thredz } : {}),
|
|
161
|
+
}, {
|
|
162
|
+
// A registrar is required by the deps type but the exam session never
|
|
163
|
+
// advertises the wiki tools — recall is the only surface it gets.
|
|
164
|
+
catalog: { register: () => undefined },
|
|
165
|
+
cwd: opts.cwd,
|
|
166
|
+
...(opts.thredz !== undefined ? { thredz: opts.thredz } : {}),
|
|
167
|
+
});
|
|
168
|
+
const recallK = opts.recallK ?? opts.fragment.memory?.wiki?.recallK ?? DEFAULT_EXAM_RECALL_K;
|
|
169
|
+
return async (req) => {
|
|
170
|
+
const agentOutput = await chatLoop({
|
|
171
|
+
model: opts.model,
|
|
172
|
+
instructions: `${opts.instructions}\n\n${EXAM_SESSION_PREAMBLE}`,
|
|
173
|
+
runContext: req.runContext,
|
|
174
|
+
singleTurn: true,
|
|
175
|
+
seedMessages: [{ role: "user", content: req.sample.input }],
|
|
176
|
+
sessionName: `${opts.specName}-exam`,
|
|
177
|
+
sessionTarget: "eval",
|
|
178
|
+
sessionRootDir: req.sessionRootDir,
|
|
179
|
+
tools: [],
|
|
180
|
+
hooks: [],
|
|
181
|
+
...(wiki !== null
|
|
182
|
+
? {
|
|
183
|
+
memory: {
|
|
184
|
+
autoRecall: true,
|
|
185
|
+
recallK,
|
|
186
|
+
recallSeed: req.sample.input,
|
|
187
|
+
recall: wiki.recall,
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
: {}),
|
|
191
|
+
spinner: false,
|
|
192
|
+
});
|
|
193
|
+
return { agentOutput };
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function examStamp(now) {
|
|
197
|
+
const d = (now ?? (() => new Date()))();
|
|
198
|
+
return d
|
|
199
|
+
.toISOString()
|
|
200
|
+
.replace(/[:.]/g, "-")
|
|
201
|
+
.replace(/-\d{3}Z$/, "Z");
|
|
202
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,11 +5,12 @@ import { aggregate } from "./aggregate";
|
|
|
5
5
|
import { RunnerError } from "./errors";
|
|
6
6
|
import { runSample } from "./run-sample";
|
|
7
7
|
import { Semaphore } from "./semaphore";
|
|
8
|
-
import type { AgentInvoker, EvalRunSummary, GraderEntry, RunEvalOptions, SampleResult } from "./types";
|
|
8
|
+
import type { AgentInvoker, EvalRunSummary, GraderEntry, GraderLookup, RunEvalOptions, SampleResult } from "./types";
|
|
9
9
|
import { type SharedAgentDeps, wireRunOnce } from "./wire-once";
|
|
10
|
-
export type { AgentInvoker, EvalRunSummary, GraderEntry, RunEvalOptions, SampleResult };
|
|
10
|
+
export type { AgentInvoker, EvalRunSummary, GraderEntry, GraderLookup, RunEvalOptions, SampleResult, };
|
|
11
11
|
export type { SharedAgentDeps };
|
|
12
12
|
export { wireRunOnce };
|
|
13
|
+
export { EXAM_SESSION_PREAMBLE, createExamRunner, type CreateExamRunnerOptions, type ExamChatLoopFn, type ExamChatLoopOptions, } from "./exam";
|
|
13
14
|
export { Semaphore };
|
|
14
15
|
export { aggregate };
|
|
15
16
|
export { RunnerError };
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
import { mkdirSync } from "node:fs";
|
|
25
25
|
import { join } from "node:path";
|
|
26
26
|
import { createJudgeGrader, loadRubric } from "@crewhaus/eval-judge";
|
|
27
|
+
import { memoryFragmentFromIr, wireMemory } from "@crewhaus/memory-service";
|
|
27
28
|
import { runChatLoop } from "@crewhaus/runtime-core";
|
|
29
|
+
import { createSkillTool } from "@crewhaus/skills-registry";
|
|
28
30
|
import { currentTenantContext } from "@crewhaus/tenancy";
|
|
29
31
|
import { aggregate } from "./aggregate";
|
|
30
32
|
import { RunnerError } from "./errors";
|
|
@@ -32,6 +34,9 @@ import { runSample } from "./run-sample";
|
|
|
32
34
|
import { Semaphore } from "./semaphore";
|
|
33
35
|
import { wireRunOnce } from "./wire-once";
|
|
34
36
|
export { wireRunOnce };
|
|
37
|
+
// v0.3.0 Goal 2 (§3.3, PR 17) — the first-class competency exam: the
|
|
38
|
+
// reference implementation of memory-service's injected `ExamRunner` seam.
|
|
39
|
+
export { EXAM_SESSION_PREAMBLE, createExamRunner, } from "./exam";
|
|
35
40
|
export { Semaphore };
|
|
36
41
|
export { aggregate };
|
|
37
42
|
export { RunnerError };
|
|
@@ -63,7 +68,9 @@ export async function runEval(args) {
|
|
|
63
68
|
const outDir = resolveEvalOutDir(runId, opts.outDir);
|
|
64
69
|
mkdirSync(outDir, { recursive: true });
|
|
65
70
|
// Resolve graders. Replace any `llm_judge` placeholder with a real judge
|
|
66
|
-
// grader bound to the runner's judgeModel (or the per-grader override)
|
|
71
|
+
// grader bound to the runner's judgeModel (or the per-grader override),
|
|
72
|
+
// and any `registry` placeholder with the named grader from the caller's
|
|
73
|
+
// grader registry (PR 19 — loud at run start, not per-sample).
|
|
67
74
|
const graders = compiledGraders.map((g) => {
|
|
68
75
|
if (g.judgeSpec) {
|
|
69
76
|
const rubric = loadRubric(g.judgeSpec.rubric);
|
|
@@ -71,6 +78,12 @@ export async function runEval(args) {
|
|
|
71
78
|
const grader = createJudgeGrader(rubric, model !== undefined ? { model } : {});
|
|
72
79
|
return { name: g.name, grader };
|
|
73
80
|
}
|
|
81
|
+
if (g.registrySpec) {
|
|
82
|
+
if (opts.graderRegistry === undefined) {
|
|
83
|
+
throw new RunnerError(`grader "${g.name}" resolves by registry name "${g.registrySpec.grader}" but no graderRegistry was supplied — pass RunEvalOptions.graderRegistry (and register the pack, e.g. registerContinuityGraders(registry))`);
|
|
84
|
+
}
|
|
85
|
+
return { name: g.name, grader: opts.graderRegistry.lookup(g.registrySpec.grader) };
|
|
86
|
+
}
|
|
74
87
|
return { name: g.name, grader: g.grader };
|
|
75
88
|
});
|
|
76
89
|
// The default invoker calls runChatLoop with the per-sample fresh runContext.
|
|
@@ -125,6 +138,7 @@ export async function runEval(args) {
|
|
|
125
138
|
graders,
|
|
126
139
|
outDir,
|
|
127
140
|
model: ir.agent.model,
|
|
141
|
+
specName: ir.name,
|
|
128
142
|
...(opts.seed !== undefined ? { seed: opts.seed } : {}),
|
|
129
143
|
});
|
|
130
144
|
const first = await runOnce();
|
|
@@ -194,17 +208,62 @@ export async function runEval(args) {
|
|
|
194
208
|
}
|
|
195
209
|
async function defaultInvoker(ir, opts) {
|
|
196
210
|
const wired = await wireRunOnce(ir, opts.cwd !== undefined ? { cwd: opts.cwd } : {});
|
|
211
|
+
// v0.3.0 §7.2 — eval/optimizer state ISOLATION. When the IR carries the
|
|
212
|
+
// memory fabric (an enabled `memory` block and/or `continuity`, which is
|
|
213
|
+
// DEFAULT-ON on the cli shape since 0.3.0), each sample gets its OWN
|
|
214
|
+
// ephemeral stores rooted under its per-sample artifact directory
|
|
215
|
+
// (`.crewhaus/evals/<runId>/<sampleId>/.crewhaus/…`): plan/focus/handoff/
|
|
216
|
+
// facts written by sample N must never leak into sample N+1 — Pillar 2
|
|
217
|
+
// assumes spec patches are the only cross-run channel. The per-sample
|
|
218
|
+
// `sessionRootDir` doubles as the fabric's session-log root so proof
|
|
219
|
+
// evidence resolves against the sample's own transcript, and `homeDir`
|
|
220
|
+
// is pinned inside the sample dir so the operator's `~/.crewhaus` skills
|
|
221
|
+
// can never bleed into a measurement.
|
|
222
|
+
const fabricOn = (ir.memory !== undefined && ir.memory.enabled !== false) || ir.continuity !== undefined;
|
|
197
223
|
return async (req) => {
|
|
224
|
+
let tools = [...wired.tools];
|
|
225
|
+
let skills = wired.skills;
|
|
226
|
+
let slashCommands = wired.slashCommands;
|
|
227
|
+
let memoryOpt;
|
|
228
|
+
let continuityOpt;
|
|
229
|
+
if (fabricOn) {
|
|
230
|
+
const memWired = await wireMemory(memoryFragmentFromIr(ir), {
|
|
231
|
+
catalog: {
|
|
232
|
+
register: (tool) => {
|
|
233
|
+
tools.push(tool);
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
cwd: req.sessionRootDir,
|
|
237
|
+
sessionRootDir: req.sessionRootDir,
|
|
238
|
+
homeDir: req.sessionRootDir,
|
|
239
|
+
});
|
|
240
|
+
memoryOpt = memWired.options.memory;
|
|
241
|
+
continuityOpt = memWired.options.continuity;
|
|
242
|
+
if (memWired.options.skills !== undefined) {
|
|
243
|
+
// The fabric owns the skill surface (builtin `continuity` skill at
|
|
244
|
+
// lowest precedence): replace any Skill tool wireRunOnce registered
|
|
245
|
+
// so the tool list never advertises two.
|
|
246
|
+
skills = memWired.options.skills;
|
|
247
|
+
tools = tools.filter((t) => t.name !== "Skill");
|
|
248
|
+
if (skills.length > 0)
|
|
249
|
+
tools.push(createSkillTool(skills));
|
|
250
|
+
}
|
|
251
|
+
if (memWired.options.slashCommands !== undefined) {
|
|
252
|
+
slashCommands = memWired.options.slashCommands;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
198
255
|
const agentOutput = await runChatLoop({
|
|
199
256
|
model: wired.model,
|
|
200
257
|
instructions: wired.instructions,
|
|
201
|
-
tools
|
|
258
|
+
tools,
|
|
202
259
|
hooks: wired.hooks,
|
|
203
|
-
skills
|
|
204
|
-
slashCommands
|
|
260
|
+
skills,
|
|
261
|
+
slashCommands,
|
|
205
262
|
...(wired.subAgents !== undefined && wired.spawnSubAgent !== undefined
|
|
206
263
|
? { subAgents: wired.subAgents, spawnSubAgent: wired.spawnSubAgent }
|
|
207
264
|
: {}),
|
|
265
|
+
...(memoryOpt !== undefined ? { memory: memoryOpt } : {}),
|
|
266
|
+
...(continuityOpt !== undefined ? { continuity: continuityOpt } : {}),
|
|
208
267
|
permissionRules: wired.permissionRules,
|
|
209
268
|
permissionMode: "auto",
|
|
210
269
|
sessionName: `${wired.sessionName}_${req.sample.id}`,
|
package/dist/run-sample.d.ts
CHANGED
|
@@ -14,6 +14,9 @@ export declare function runSample(args: {
|
|
|
14
14
|
outDir: string;
|
|
15
15
|
model: string;
|
|
16
16
|
seed?: number;
|
|
17
|
+
/** The spec name (`ir.name`) — threaded into `RunResult.artifacts` so
|
|
18
|
+
* artifact graders can address `.crewhaus/state/<specName>/` directly. */
|
|
19
|
+
specName?: string;
|
|
17
20
|
}): Promise<SampleResult>;
|
|
18
21
|
/** Helper: produce one combined grader from an array of CompiledGrader. */
|
|
19
22
|
export declare function combineGraderEntries(graders: ReadonlyArray<CompiledGrader>): Grader;
|
package/dist/run-sample.js
CHANGED
|
@@ -69,7 +69,12 @@ export async function runSample(args) {
|
|
|
69
69
|
const turns = transcript.filter((e) => e.kind === "assistant_message").length;
|
|
70
70
|
const toolCalls = extractToolCalls(finalEvents);
|
|
71
71
|
const tokens = sumTokens(finalEvents);
|
|
72
|
-
// Apply graders.
|
|
72
|
+
// Apply graders. `artifacts` is the PR-19 seam for artifact-reading
|
|
73
|
+
// graders (grader-continuity): the sample's own directory — the primary
|
|
74
|
+
// session's `transcript.jsonl`, any extra session JSONLs a multi-session
|
|
75
|
+
// invoker wrote, and the isolated `.crewhaus/` fabric root (§7.2) — so
|
|
76
|
+
// graders measure the finished sample's files, never the host's live
|
|
77
|
+
// stores.
|
|
73
78
|
const runResult = {
|
|
74
79
|
agentOutput,
|
|
75
80
|
events: finalEvents,
|
|
@@ -77,6 +82,13 @@ export async function runSample(args) {
|
|
|
77
82
|
toolCalls,
|
|
78
83
|
turns,
|
|
79
84
|
latencyMs,
|
|
85
|
+
artifacts: {
|
|
86
|
+
sampleDir,
|
|
87
|
+
sessionId: runContext.sessionId,
|
|
88
|
+
transcriptPath,
|
|
89
|
+
stateRootDir: join(sampleDir, ".crewhaus"),
|
|
90
|
+
...(args.specName !== undefined ? { specName: args.specName } : {}),
|
|
91
|
+
},
|
|
80
92
|
};
|
|
81
93
|
const perGrader = [];
|
|
82
94
|
// A grader throwing is grader INFRA noise (judge 429/timeout), not a graded
|
package/dist/types.d.ts
CHANGED
|
@@ -104,6 +104,14 @@ export type EvalRunSummary = {
|
|
|
104
104
|
};
|
|
105
105
|
readonly outDir: string;
|
|
106
106
|
};
|
|
107
|
+
/**
|
|
108
|
+
* Structural view of `@crewhaus/grader-registry`'s `GraderRegistry` — just
|
|
109
|
+
* the lookup the runner needs to resolve `type: registry` grader entries,
|
|
110
|
+
* kept structural so this package takes no grader-registry dependency.
|
|
111
|
+
*/
|
|
112
|
+
export type GraderLookup = {
|
|
113
|
+
lookup(name: string): Grader;
|
|
114
|
+
};
|
|
107
115
|
export type RunEvalOptions = {
|
|
108
116
|
readonly runId?: string;
|
|
109
117
|
readonly concurrency?: number;
|
|
@@ -112,6 +120,14 @@ export type RunEvalOptions = {
|
|
|
112
120
|
readonly judgeModel?: string;
|
|
113
121
|
readonly invoker?: AgentInvoker;
|
|
114
122
|
readonly cwd?: string;
|
|
123
|
+
/**
|
|
124
|
+
* v0.3.0 §7.3 (PR 19) — resolves `type: registry` grader entries by name.
|
|
125
|
+
* An eval config opts into registered grader packs (`continuity.*` after
|
|
126
|
+
* `registerContinuityGraders`, `twelve.*` after `register12MetricRubric`)
|
|
127
|
+
* without this package importing them; omitting the registry while the
|
|
128
|
+
* config carries a registry entry is a loud `RunnerError` at run start.
|
|
129
|
+
*/
|
|
130
|
+
readonly graderRegistry?: GraderLookup;
|
|
115
131
|
/**
|
|
116
132
|
* Retry a sample ONCE, within the run, when its result is an ERROR
|
|
117
133
|
* (`SampleResult.error` — the INVOKER failed: provider timeout, 429,
|
package/dist/wire-once.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { loadHooks } from "@crewhaus/hooks-engine";
|
|
4
4
|
import { createLogger } from "@crewhaus/logging";
|
|
5
|
-
import { McpHost } from "@crewhaus/mcp-host";
|
|
5
|
+
import { McpHost, resolveMcpServerConfig } from "@crewhaus/mcp-host";
|
|
6
6
|
import { BUILTIN_DEFAULT_RULES, PermissionConfigError, parsePermissionsConfig, tagRules, } from "@crewhaus/permission-engine";
|
|
7
7
|
import { createSkillTool, discoverSkills } from "@crewhaus/skills-registry";
|
|
8
8
|
import { loadCommands } from "@crewhaus/slash-commands";
|
|
@@ -33,8 +33,11 @@ export async function wireRunOnce(ir, opts = {}) {
|
|
|
33
33
|
if (Object.keys(ir.mcp_servers).length > 0) {
|
|
34
34
|
const host = new McpHost({ logger });
|
|
35
35
|
mcpHost = host;
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// 0.3.0 — env/header values are IrSecretRef; resolve from the eval
|
|
37
|
+
// process's environment (fail-fast, names the variable).
|
|
38
|
+
for (const [name, cfg] of Object.entries(ir.mcp_servers)) {
|
|
39
|
+
host.addServer(name, resolveMcpServerConfig(cfg, { name }));
|
|
40
|
+
}
|
|
38
41
|
const tempCatalog = new ToolCatalog();
|
|
39
42
|
for (const t of tools)
|
|
40
43
|
tempCatalog.register(t);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/eval-runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Eval runner — per-sample isolation, concurrency, grading, persistence",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,37 +15,43 @@
|
|
|
15
15
|
"test": "bun test src"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@crewhaus/agent-context-isolation": "0.
|
|
19
|
-
"@crewhaus/compiler": "0.
|
|
20
|
-
"@crewhaus/errors": "0.
|
|
21
|
-
"@crewhaus/eval-dataset": "0.
|
|
22
|
-
"@crewhaus/eval-grader": "0.
|
|
23
|
-
"@crewhaus/eval-judge": "0.
|
|
24
|
-
"@crewhaus/event-log": "0.
|
|
25
|
-
"@crewhaus/hooks-engine": "0.
|
|
26
|
-
"@crewhaus/ir": "0.
|
|
27
|
-
"@crewhaus/logging": "0.
|
|
28
|
-
"@crewhaus/mcp-host": "0.
|
|
29
|
-
"@crewhaus/
|
|
30
|
-
"@crewhaus/
|
|
31
|
-
"@crewhaus/
|
|
32
|
-
"@crewhaus/
|
|
33
|
-
"@crewhaus/
|
|
34
|
-
"@crewhaus/
|
|
35
|
-
"@crewhaus/
|
|
36
|
-
"@crewhaus/
|
|
37
|
-
"@crewhaus/
|
|
38
|
-
"@crewhaus/tool-
|
|
39
|
-
"@crewhaus/tool-
|
|
40
|
-
"@crewhaus/tool-
|
|
41
|
-
"@crewhaus/tool-
|
|
42
|
-
"@crewhaus/tool-
|
|
43
|
-
"@crewhaus/tool-
|
|
44
|
-
"@crewhaus/tool-
|
|
45
|
-
"@crewhaus/tool-
|
|
46
|
-
"@crewhaus/
|
|
18
|
+
"@crewhaus/agent-context-isolation": "0.3.0",
|
|
19
|
+
"@crewhaus/compiler": "0.3.0",
|
|
20
|
+
"@crewhaus/errors": "0.3.0",
|
|
21
|
+
"@crewhaus/eval-dataset": "0.3.0",
|
|
22
|
+
"@crewhaus/eval-grader": "0.3.0",
|
|
23
|
+
"@crewhaus/eval-judge": "0.3.0",
|
|
24
|
+
"@crewhaus/event-log": "0.3.0",
|
|
25
|
+
"@crewhaus/hooks-engine": "0.3.0",
|
|
26
|
+
"@crewhaus/ir": "0.3.0",
|
|
27
|
+
"@crewhaus/logging": "0.3.0",
|
|
28
|
+
"@crewhaus/mcp-host": "0.3.0",
|
|
29
|
+
"@crewhaus/memory-service": "0.3.0",
|
|
30
|
+
"@crewhaus/permission-engine": "0.3.0",
|
|
31
|
+
"@crewhaus/run-context": "0.3.0",
|
|
32
|
+
"@crewhaus/runtime-core": "0.3.0",
|
|
33
|
+
"@crewhaus/skills-registry": "0.3.0",
|
|
34
|
+
"@crewhaus/slash-commands": "0.3.0",
|
|
35
|
+
"@crewhaus/spec": "0.3.0",
|
|
36
|
+
"@crewhaus/sub-agent-spawner": "0.3.0",
|
|
37
|
+
"@crewhaus/tenancy": "0.3.0",
|
|
38
|
+
"@crewhaus/tool-bash": "0.3.0",
|
|
39
|
+
"@crewhaus/tool-catalog": "0.3.0",
|
|
40
|
+
"@crewhaus/tool-fetch": "0.3.0",
|
|
41
|
+
"@crewhaus/tool-fs": "0.3.0",
|
|
42
|
+
"@crewhaus/tool-image": "0.3.0",
|
|
43
|
+
"@crewhaus/tool-mcp": "0.3.0",
|
|
44
|
+
"@crewhaus/tool-task": "0.3.0",
|
|
45
|
+
"@crewhaus/tool-todo": "0.3.0",
|
|
46
|
+
"@crewhaus/tool-web": "0.3.0",
|
|
47
|
+
"@crewhaus/trace-event-bus": "0.3.0",
|
|
48
|
+
"@crewhaus/wiki-store": "0.3.0",
|
|
47
49
|
"zod": "^3.23.8"
|
|
48
50
|
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@crewhaus/grader-continuity": "0.3.0",
|
|
53
|
+
"@crewhaus/grader-registry": "0.3.0"
|
|
54
|
+
},
|
|
49
55
|
"license": "Apache-2.0",
|
|
50
56
|
"author": {
|
|
51
57
|
"name": "Max Meier",
|