@crewhaus/eval-runner 0.1.8 → 0.2.1

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.
@@ -1,5 +1,14 @@
1
1
  import type { SampleResult } from "./types";
2
2
  export declare function quantile(sorted: ReadonlyArray<number>, q: number): number;
3
+ /**
4
+ * Retry honesty (RunEvalOptions.retryErrors): a retried sample appears in
5
+ * `samples` exactly ONCE — the retry REPLACED the errored first attempt —
6
+ * so passRate's denominator still counts each dataset sample once. A retry
7
+ * that passes counts as a normal pass; a retry that errors again lands in
8
+ * `errorCount` (and, like every errored sample, drags passRate down while
9
+ * staying out of meanScore/latency/token aggregates). The discarded first
10
+ * attempt is counted nowhere, by design: it was infra noise, not a result.
11
+ */
3
12
  export declare function aggregate(samples: ReadonlyArray<SampleResult>): {
4
13
  passRate: number;
5
14
  meanScore: number;
package/dist/aggregate.js CHANGED
@@ -11,6 +11,15 @@ export function quantile(sorted, q) {
11
11
  const fract = idx - lo;
12
12
  return (sorted[lo] ?? 0) * (1 - fract) + (sorted[hi] ?? 0) * fract;
13
13
  }
14
+ /**
15
+ * Retry honesty (RunEvalOptions.retryErrors): a retried sample appears in
16
+ * `samples` exactly ONCE — the retry REPLACED the errored first attempt —
17
+ * so passRate's denominator still counts each dataset sample once. A retry
18
+ * that passes counts as a normal pass; a retry that errors again lands in
19
+ * `errorCount` (and, like every errored sample, drags passRate down while
20
+ * staying out of meanScore/latency/token aggregates). The discarded first
21
+ * attempt is counted nowhere, by design: it was infra noise, not a result.
22
+ */
14
23
  export function aggregate(samples) {
15
24
  const ok = samples.filter((s) => s.error === undefined);
16
25
  const total = samples.length;
package/dist/index.js CHANGED
@@ -86,6 +86,8 @@ export async function runEval(args) {
86
86
  startedAt,
87
87
  specHash,
88
88
  datasetName: dataset.name,
89
+ ...(opts.datasetHash !== undefined ? { datasetHash: opts.datasetHash } : {}),
90
+ ...(opts.gradersHash !== undefined ? { gradersHash: opts.gradersHash } : {}),
89
91
  graderNames: graders.map((g) => g.name),
90
92
  model: ir.agent.model,
91
93
  ...(opts.judgeModel !== undefined ? { judgeModel: opts.judgeModel } : {}),
@@ -117,7 +119,7 @@ export async function runEval(args) {
117
119
  throw new RunnerError(`run interrupted before sample "${sample.id}"`);
118
120
  }
119
121
  try {
120
- return await runSample({
122
+ const runOnce = () => runSample({
121
123
  sample,
122
124
  invoker,
123
125
  graders,
@@ -125,6 +127,20 @@ export async function runEval(args) {
125
127
  model: ir.agent.model,
126
128
  ...(opts.seed !== undefined ? { seed: opts.seed } : {}),
127
129
  });
130
+ const first = await runOnce();
131
+ // Noise auto-retry (failure-arbiter item 7): an errored SampleResult
132
+ // means the INVOKER failed (provider timeout, 429, sandbox blip),
133
+ // and a graderError means a GRADER threw (judge infra blip) — both
134
+ // are infra noise, not graded failures. Retry exactly once within
135
+ // the run; the retried outcome replaces the errored one wholesale
136
+ // (the second runSample rewrites the same per-sample artifact dir)
137
+ // and is tagged `retried: true` so reports and triage can tell.
138
+ // Skipped on SIGINT or when the caller opted out (`--no-retry`).
139
+ const infraNoise = first.error !== undefined || first.graderError !== undefined;
140
+ if (!infraNoise || opts.retryErrors === false || interrupted) {
141
+ return first;
142
+ }
143
+ return { ...(await runOnce()), retried: true };
128
144
  }
129
145
  finally {
130
146
  release();
@@ -163,6 +179,8 @@ export async function runEval(args) {
163
179
  config: {
164
180
  specHash,
165
181
  datasetName: dataset.name,
182
+ ...(opts.datasetHash !== undefined ? { datasetHash: opts.datasetHash } : {}),
183
+ ...(opts.gradersHash !== undefined ? { gradersHash: opts.gradersHash } : {}),
166
184
  graderNames: graders.map((g) => g.name),
167
185
  model: ir.agent.model,
168
186
  ...(opts.judgeModel !== undefined ? { judgeModel: opts.judgeModel } : {}),
@@ -79,20 +79,28 @@ export async function runSample(args) {
79
79
  latencyMs,
80
80
  };
81
81
  const perGrader = [];
82
+ // A grader throwing is grader INFRA noise (judge 429/timeout), not a graded
83
+ // verdict on the agent's output. It still lands as a failed perGrader entry
84
+ // (so `overall` honestly fails), but the structured `graderError` below
85
+ // lets the run loop retry the sample and triage classify it as noise.
86
+ const graderErrors = [];
82
87
  for (const g of graders) {
83
88
  let result;
84
89
  try {
85
90
  result = await g.grader(sample, runResult);
86
91
  }
87
92
  catch (err) {
93
+ const msg = err instanceof Error ? err.message : String(err);
94
+ graderErrors.push(`${g.name}: ${msg}`);
88
95
  result = {
89
96
  passed: false,
90
97
  score: 0,
91
- rationale: `grader threw: ${err instanceof Error ? err.message : String(err)}`,
98
+ rationale: `grader threw: ${msg}`,
92
99
  };
93
100
  }
94
101
  perGrader.push({ name: g.name, ...result });
95
102
  }
103
+ const graderError = graderErrors.length > 0 ? `grader threw: ${graderErrors.join("; ")}` : undefined;
96
104
  // Overall = AND of all graders, score = mean.
97
105
  const overall = {
98
106
  passed: error === undefined && perGrader.every((g) => g.passed),
@@ -113,6 +121,7 @@ export async function runSample(args) {
113
121
  agentOutput,
114
122
  grades: { overall, perGrader },
115
123
  ...(error !== undefined ? { error } : {}),
124
+ ...(graderError !== undefined ? { graderError } : {}),
116
125
  };
117
126
  await Bun.write(join(sampleDir, "grades.json"), JSON.stringify(sampleResult.grades, null, 2));
118
127
  await Bun.write(join(sampleDir, "meta.json"), JSON.stringify({
@@ -125,6 +134,7 @@ export async function runSample(args) {
125
134
  tokens,
126
135
  model,
127
136
  ...(error !== undefined ? { error } : {}),
137
+ ...(graderError !== undefined ? { graderError } : {}),
128
138
  ...(args.seed !== undefined ? { seed: args.seed } : {}),
129
139
  }, null, 2));
130
140
  return sampleResult;
package/dist/types.d.ts CHANGED
@@ -52,6 +52,24 @@ export type SampleResult = {
52
52
  } & GradeResult>;
53
53
  };
54
54
  readonly error?: string;
55
+ /**
56
+ * Set when one or more GRADERS threw while grading this sample (judge
57
+ * provider 429/timeout, rubric fetch failure, …) — grader infrastructure
58
+ * noise, distinct from `error` (the INVOKER failed) and from an honest
59
+ * graded failure. The thrown grader still contributes a failed
60
+ * `perGrader` entry (rationale `grader threw: …`), so `grades.overall`
61
+ * fails; this field preserves the structured evidence so the retry loop
62
+ * can retry the sample and triage can classify the failure as noise.
63
+ */
64
+ readonly graderError?: string;
65
+ /**
66
+ * True when this result replaced an ERRORED first attempt via the runner's
67
+ * bounded noise retry (see {@link RunEvalOptions.retryErrors}) — invoker
68
+ * errors and grader throws (`graderError`) alike. Set on the retried
69
+ * outcome regardless of whether the retry passed or errored again; absent
70
+ * on samples that succeeded (or failed grading) on attempt one.
71
+ */
72
+ readonly retried?: boolean;
55
73
  };
56
74
  export type EvalRunSummary = {
57
75
  readonly runId: string;
@@ -74,6 +92,10 @@ export type EvalRunSummary = {
74
92
  readonly config: {
75
93
  readonly specHash: string;
76
94
  readonly datasetName: string;
95
+ /** sha256 of the dataset file bytes, when the caller supplied one. */
96
+ readonly datasetHash?: string;
97
+ /** sha256 of the parsed GradersConfig, when the caller supplied one. */
98
+ readonly gradersHash?: string;
77
99
  readonly graderNames: ReadonlyArray<string>;
78
100
  readonly model: string;
79
101
  readonly judgeModel?: string;
@@ -90,6 +112,32 @@ export type RunEvalOptions = {
90
112
  readonly judgeModel?: string;
91
113
  readonly invoker?: AgentInvoker;
92
114
  readonly cwd?: string;
115
+ /**
116
+ * Retry a sample ONCE, within the run, when its result is an ERROR
117
+ * (`SampleResult.error` — the INVOKER failed: provider timeout, 429,
118
+ * sandbox blip) or a GRADER threw (`SampleResult.graderError` — judge
119
+ * infra noise; the agent may have answered fine). Infra noise, not a
120
+ * graded failure. The retried outcome replaces the errored one wholesale
121
+ * (per-sample artifacts included) and is tagged `retried: true`.
122
+ * Default: true. `crewhaus eval --no-retry` opts out. Interrupted runs
123
+ * (SIGINT) never retry.
124
+ */
125
+ readonly retryErrors?: boolean;
126
+ /**
127
+ * Content hash (sha256 hex) of the dataset file the samples came from.
128
+ * Purely informational — persisted into `run.json` / `results.json` so
129
+ * downstream consumers (run-history index, dataset-drift detection) can
130
+ * tell whether two runs saw byte-identical data.
131
+ */
132
+ readonly datasetHash?: string;
133
+ /**
134
+ * Content hash (sha256 hex) of the parsed GradersConfig the graders came
135
+ * from. Mirrors {@link datasetHash}: purely informational, persisted into
136
+ * `run.json` / `results.json` so downstream consumers (notably the item-30
137
+ * sentinel) can tell whether two runs graded with byte-identical config —
138
+ * a changed rubric/threshold must not be misattributed to provider drift.
139
+ */
140
+ readonly gradersHash?: string;
93
141
  };
94
142
  export type GraderEntry = {
95
143
  readonly name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/eval-runner",
3
- "version": "0.1.8",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Eval runner — per-sample isolation, concurrency, grading, persistence",
6
6
  "main": "dist/index.js",
@@ -15,35 +15,35 @@
15
15
  "test": "bun test src"
16
16
  },
17
17
  "dependencies": {
18
- "@crewhaus/agent-context-isolation": "0.1.8",
19
- "@crewhaus/compiler": "0.1.8",
20
- "@crewhaus/errors": "0.1.8",
21
- "@crewhaus/eval-dataset": "0.1.8",
22
- "@crewhaus/eval-grader": "0.1.8",
23
- "@crewhaus/eval-judge": "0.1.8",
24
- "@crewhaus/event-log": "0.1.8",
25
- "@crewhaus/hooks-engine": "0.1.8",
26
- "@crewhaus/ir": "0.1.8",
27
- "@crewhaus/logging": "0.1.8",
28
- "@crewhaus/mcp-host": "0.1.8",
29
- "@crewhaus/permission-engine": "0.1.8",
30
- "@crewhaus/run-context": "0.1.8",
31
- "@crewhaus/runtime-core": "0.1.8",
32
- "@crewhaus/skills-registry": "0.1.8",
33
- "@crewhaus/slash-commands": "0.1.8",
34
- "@crewhaus/spec": "0.1.8",
35
- "@crewhaus/sub-agent-spawner": "0.1.8",
36
- "@crewhaus/tenancy": "0.1.8",
37
- "@crewhaus/tool-bash": "0.1.8",
38
- "@crewhaus/tool-catalog": "0.1.8",
39
- "@crewhaus/tool-fetch": "0.1.8",
40
- "@crewhaus/tool-fs": "0.1.8",
41
- "@crewhaus/tool-image": "0.1.8",
42
- "@crewhaus/tool-mcp": "0.1.8",
43
- "@crewhaus/tool-task": "0.1.8",
44
- "@crewhaus/tool-todo": "0.1.8",
45
- "@crewhaus/tool-web": "0.1.8",
46
- "@crewhaus/trace-event-bus": "0.1.8",
18
+ "@crewhaus/agent-context-isolation": "0.2.1",
19
+ "@crewhaus/compiler": "0.2.1",
20
+ "@crewhaus/errors": "0.2.1",
21
+ "@crewhaus/eval-dataset": "0.2.1",
22
+ "@crewhaus/eval-grader": "0.2.1",
23
+ "@crewhaus/eval-judge": "0.2.1",
24
+ "@crewhaus/event-log": "0.2.1",
25
+ "@crewhaus/hooks-engine": "0.2.1",
26
+ "@crewhaus/ir": "0.2.1",
27
+ "@crewhaus/logging": "0.2.1",
28
+ "@crewhaus/mcp-host": "0.2.1",
29
+ "@crewhaus/permission-engine": "0.2.1",
30
+ "@crewhaus/run-context": "0.2.1",
31
+ "@crewhaus/runtime-core": "0.2.1",
32
+ "@crewhaus/skills-registry": "0.2.1",
33
+ "@crewhaus/slash-commands": "0.2.1",
34
+ "@crewhaus/spec": "0.2.1",
35
+ "@crewhaus/sub-agent-spawner": "0.2.1",
36
+ "@crewhaus/tenancy": "0.2.1",
37
+ "@crewhaus/tool-bash": "0.2.1",
38
+ "@crewhaus/tool-catalog": "0.2.1",
39
+ "@crewhaus/tool-fetch": "0.2.1",
40
+ "@crewhaus/tool-fs": "0.2.1",
41
+ "@crewhaus/tool-image": "0.2.1",
42
+ "@crewhaus/tool-mcp": "0.2.1",
43
+ "@crewhaus/tool-task": "0.2.1",
44
+ "@crewhaus/tool-todo": "0.2.1",
45
+ "@crewhaus/tool-web": "0.2.1",
46
+ "@crewhaus/trace-event-bus": "0.2.1",
47
47
  "zod": "^3.23.8"
48
48
  },
49
49
  "license": "Apache-2.0",