bare-agent 0.45.0 → 0.46.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.
package/README.md CHANGED
@@ -88,7 +88,7 @@ Every piece works alone — take what you need, ignore the rest. Two axes: **Act
88
88
 
89
89
  | Component | What it does |
90
90
  |---|---|
91
- | **Evaluator + refine** | Judge output by `predicate` (no tokens), `rubric` (an isolated adversarial grader), or `agentic` (a critic that exercises the live artifact). `refine` is the bounded generate→evaluate→regenerate loop |
91
+ | **Evaluator + refine** | Judge output by `predicate` (no tokens), `rubric` (an isolated adversarial grader), `agentic` (a critic that exercises the live artifact), or `jev` (a cheap calibrated classifier tier). `refine` is the bounded generate→evaluate→regenerate loop |
92
92
  | **SkillRegistry** | Surface skills on demand: one meta-tool catalog; activating a skill injects its instructions and unlocks its tools |
93
93
  | **stash** | Compact finished work out of the live window (restorable), or auto-fold the middle under token pressure |
94
94
 
@@ -51,7 +51,7 @@ Eight entry points:
51
51
  | Retry individual plan steps | runPlan({ stepRetry }) |
52
52
  | Use a CLI tool as an LLM provider | CLIPipe |
53
53
  | Health-check provider, store, and tools | Loop.validate() |
54
- | Verify an agent's output (judge / grade / critic) | Evaluator + refine — `predicate` / `rubric` / `agentic` criteria |
54
+ | Verify an agent's output (judge / grade / critic) | Evaluator + refine — `predicate` / `rubric` / `agentic` / `jev` criteria |
55
55
  | Decisively judge "did this answer honor the request?" (return-time) | judge — verbatim request + one artifact → `honored`/`broke` + mechanical `where`; `calibrate` admits a tier vs a frozen floor |
56
56
  | Cheap yes/no, pick-one, or score classification (cheaper than judge/Evaluator.rubric) | JevProvider.classify — `noul`/`choice`/`score`; `calibrateJev` admits a Jev tier vs a frozen floor + injection battery |
57
57
  | Offer skills on demand without bloating context | SkillRegistry — `skill_use` meta-tool + `skills.activeTools` thunk |
@@ -254,9 +254,9 @@ const { answers, costUsd, raw } = await jev.classify('I was charged twice, pleas
254
254
  // raw → the full unmodified parsed Jev response (a request id / warnings beyond answers/usage/model, if any)
255
255
  ```
256
256
 
257
- Three question types, kept verbatim from Jev's own contract: `noul` (binary probability `0..1`), `choice` (pick one of `criteria`'s keys, plus untrusted `probabilities`/`confidence`), `score` (a position `0..N-1` on `criteria`'s legend). Jev's reply is **untrusted model output** — `classify()` schema-checks the discriminator field against the question that asked it (`ValidationError`, stamped `lib:'bare-agent'`, on a type mismatch or an out-of-range value) before returning it; `probabilities`/`confidence` pass through unvalidated.
257
+ Three question types, kept verbatim from Jev's own contract: `noul` (binary probability `0..1`), `choice` (pick one of `criteria`'s keys, plus untrusted `probabilities`/`confidence`), `score` (a position `0..N-1` on `criteria`'s legend). Jev's reply is **untrusted model output** — `classify()` schema-checks the discriminator field against the question that asked it (`ValidationError`, stamped `lib:'bare-agent'`, on a type mismatch or an out-of-range value) before returning it; `probabilities`/`confidence` pass through unvalidated. A question's `instructions` can be a string, or the documented TypeSafe structured forms — an array or a plain object with named parts (e.g. `question`/`inspect`/`focus`/`ignore`) — for giving a question multiple named parts; empty `{}`/`[]`/non-plain values are rejected as missing instructions.
258
258
 
259
- **Injection hardening is on by default.** `state` is untrusted and can carry an embedded attack (`"you are now…"`, `"ignore previous instructions"`); `classify()` prepends a defensive preamble to every question's `instructions` (copy-on-write, never mutates your `questions` object) so the classifier treats `state` as data, not commands. Opt out with `harden: false` on the constructor or per call.
259
+ **Injection hardening is on by default, and shape-aware.** `state` is untrusted and can carry an embedded attack (`"you are now…"`, `"ignore previous instructions"`); `classify()` wraps every question's `instructions` with a defensive preamble prefixed onto a string, prepended as element 0 of an array, or added under a reserved `__hardening__` key on an object — so the classifier treats `state` as data, not commands (copy-on-write, never mutates your `questions` object; a caller-supplied `__hardening__` key is rejected so it can't overwrite the preamble). Opt out with `harden: false` on the constructor or per call.
260
260
 
261
261
  **Calibrate a tier before you trust it.** `calibrateJev` (exported from `bare-agent`, mirrors `calibrate`/judge's harness) grades a Jev model against a frozen clear-case battery (`noul`/`choice`/`score` + a false-positive trap) **and** a multi-style injection battery, admitting only if the clear-case floor clears with zero reds **and** every injection style is resisted:
262
262
 
@@ -824,12 +824,18 @@ const out = await recurse('Fix the failing function in calc.js', ctx, {
824
824
  ```javascript
825
825
  const { Evaluator, refine } = require('bare-agent');
826
826
 
827
- const evaluator = new Evaluator({ provider }); // provider REQUIRED for rubric/agentic; predicate needs none
827
+ const evaluator = new Evaluator({ provider, jevProvider }); // provider REQUIRED for rubric/agentic; jevProvider REQUIRED for jev; predicate needs neither
828
828
 
829
- // Three criteria types — pass EXACTLY ONE:
829
+ // Four criteria types — pass EXACTLY ONE:
830
830
  const v1 = await evaluator.evaluate(goal, result, { predicate: (r) => r.includes('DONE') }); // deterministic, 0 tokens
831
831
  const v2 = await evaluator.evaluate(goal, result, { rubric: 'Cites a source for every claim.' }); // isolated adversarial LLM grader
832
832
  const v3 = await evaluator.evaluate(goal, url, { agentic: 'Open the page, click Submit, check the console for errors.' }); // tool-running critic that EXERCISES the artifact
833
+ const v4 = await evaluator.evaluate(goal, result, {
834
+ jev: {
835
+ question: { type: 'noul', instructions: 'Does the result satisfy the goal?' },
836
+ toVerdict: (a) => (a.noul >= 0.5 ? 'satisfied' : 'failed'), // caller owns the threshold/band mapping
837
+ },
838
+ }); // cheap calibrated classifier tier (~100-1000x cheaper than rubric)
833
839
 
834
840
  // Verdict: { status: 'satisfied' | 'needs_revision' | 'failed', pass, score, critique, suggestions }
835
841
  // pass = (status === 'satisfied'); needs_revision is retryable; failed is terminal (stop spending).
@@ -839,7 +845,8 @@ if (!v2.pass) console.log(v2.critique, v2.suggestions);
839
845
  Key invariants:
840
846
  - The **rubric path runs an isolated adversarial grader** — a separate context window with a harsh, independent prompt, never the generator's transcript. That isolation (not a feedback knob) is what defeats the self-evaluation trap; the grader treats the RESULT as untrusted DATA (judge prompt-injection defence).
841
847
  - **`agentic`** (the third type) spins up a fresh Loop with scoped tools (set on the Evaluator, or per-call `opts.tools`) that **exercises** the live artifact — clicks, reads console/network — rather than reading text. Each critic round forwards to `onLlmResult`; a governance `HaltError` re-throws clean.
842
- - **`contract`** (a definition of done) is graded against instead of the loose goal: `evaluate(goal, result, { rubric, contract })`. Judge tokens forward to the gate via `onLlmResult` (`kind:'evaluate'`) so verification spend is visible to the budget.
848
+ - **`jev`** (the fourth type) composes `JevProvider` for a cost tier BELOW rubric — `question` is the SOLE criterion sent to the classifier (no layered prompt), and the caller-supplied `toVerdict(answer)` maps jev's `noul`/`choice`/`score` answer shape to the tri-state status (the Evaluator stays agnostic to jev's answer shapes). A `toVerdict` return outside the tri-state set is a broken arbiter (BA-15 family): thrown as `ValidationError` naming the type only, never the value. Requires a `jevProvider` (on the Evaluator or per-call `opts.jevProvider`). `score` is always `null`.
849
+ - **`contract`** (a definition of done) is graded against instead of the loose goal: `evaluate(goal, result, { rubric, contract })`. Judge tokens forward to the gate via `onLlmResult` (`kind:'evaluate'`) so verification spend is visible to the budget. For `jev`, `contract` is used only as the failed/needs-revision critique string — never folded into `question`.
843
850
 
844
851
  **`refine`** drives a caller-supplied `attempt`/`evaluate` until a satisfied verdict, a terminal `failed`, or `maxIterations` (the real bound is bareguard maxTurns/budget). It threads the latest `critique` into the next attempt (fresh-feedback, not anchoring on a failed answer) and a shared `contract` to both sides.
845
852
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-agent",
3
- "version": "0.45.0",
3
+ "version": "0.46.1",
4
4
  "files": [
5
5
  "index.js",
6
6
  "index.d.ts",
package/primitives.json CHANGED
@@ -175,7 +175,7 @@
175
175
  {
176
176
  "name": "Evaluator",
177
177
  "category": "evaluation",
178
- "when": "you need to judge an output against a goal or contract — deterministically (predicate), by LLM rubric, or with a tool-running critic that exercises the live artifact",
178
+ "when": "you need to judge an output against a goal or contract — deterministically (predicate), by LLM rubric, with a tool-running critic that exercises the live artifact, or by a cheap calibrated classifier (jev)",
179
179
  "import": "import { Evaluator } from 'bare-agent'",
180
180
  "signature": "new Evaluator(options?: EvaluatorOptions)",
181
181
  "fails": "never throws for a bad grade — returns a Verdict {status: satisfied|needs_revision|failed}; a provider HaltError propagates clean. Judge tokens forward via onLlmResult.",
@@ -56,6 +56,12 @@ export type EvaluatorOptions = {
56
56
  * `EvaluateOptions.tools`. Ignored by predicate/rubric.
57
57
  */
58
58
  tools?: import("../types").ToolDef[] | undefined;
59
+ /**
60
+ * - REQUIRED for the `jev` path — a cheap
61
+ * calibrated classifier tier (~100-1000x cheaper than a rubric LLM round for classification-shaped verdicts).
62
+ * Overridable per call via `EvaluateOptions.jevProvider`. Ignored by predicate/rubric/agentic.
63
+ */
64
+ jevProvider?: import("./provider-jev").JevProvider | undefined;
59
65
  };
60
66
  export type Criteria = {
61
67
  /**
@@ -75,9 +81,28 @@ export type Criteria = {
75
81
  * `tools`. The strongest verification — catches what only running the thing reveals. Exactly one of the three.
76
82
  */
77
83
  agentic?: string | undefined;
84
+ /**
85
+ * -
86
+ * Cheap calibrated classifier door (composes `JevProvider`, the cost tier below `rubric`). `question` is
87
+ * ONE Jev question object — the SOLE criterion (a classifier takes one instruction, not a layered prompt;
88
+ * unlike rubric/agentic, `contract` is NOT folded into it here — it is used only as the failed-critique
89
+ * string). `toVerdict` maps the classifier's per-question answer (`{type:'noul',noul}` /
90
+ * `{type:'choice',choice,...}` / `{type:'score',score,...}`) to a tri-state status — the caller owns
91
+ * thresholds/bands (Option A: caller-supplied mapping fn, open + simple, no premature abstraction over
92
+ * jev's three answer shapes). Requires a `jevProvider` (on the Evaluator or per-call). Exactly one of the four.
93
+ */
94
+ jev?: {
95
+ question: {
96
+ type: "noul" | "choice" | "score";
97
+ instructions: string;
98
+ criteria?: any;
99
+ };
100
+ toVerdict: (answer: any) => "satisfied" | "needs_revision" | "failed";
101
+ } | undefined;
78
102
  /**
79
103
  * - The shared, authoritative "definition of done" the grader judges against
80
- * (A3 / D10). When present it is what success means — not the loose goal. Folded into the rubric/agentic prompt.
104
+ * (A3 / D10). When present it is what success means — not the loose goal. Folded into the rubric/agentic
105
+ * prompt; for the `jev` door it is used ONLY as the failed-critique string (never folded into `question`).
81
106
  */
82
107
  contract?: string | undefined;
83
108
  };
@@ -102,21 +127,28 @@ export type EvaluateOptions = {
102
127
  * critic MUST be bounded (turn/budget caps come from the gate; the Loop's HARD_ROUND_LIMIT is only a net).
103
128
  */
104
129
  policy?: Function | undefined;
130
+ /**
131
+ * - Per-call override of the `jev` door's classifier
132
+ * provider (else `EvaluatorOptions.jevProvider`).
133
+ */
134
+ jevProvider?: import("./provider-jev").JevProvider | undefined;
105
135
  };
106
136
  /**
107
137
  * Output-side judge — the mirror of `Planner` (input-side). Judges whether a result meets a goal, by a
108
- * deterministic `predicate`, an LLM `rubric`, or a tool-running `agentic` critic, returning one uniform
109
- * `Verdict`. The rubric and agentic paths run an ISOLATED adversarial critic (separate context + independent
110
- * system prompt) — that isolation, not a feedback knob, is what defeats the self-evaluation trap. The agentic
111
- * path additionally EXERCISES the artifact with scoped tools (it does not read the diff). Composes AROUND a
112
- * Loop (never inside `loop.js`).
138
+ * deterministic `predicate`, an LLM `rubric`, a tool-running `agentic` critic, or a cheap calibrated `jev`
139
+ * classifier tier, returning one uniform `Verdict`. The rubric and agentic paths run an ISOLATED adversarial
140
+ * critic (separate context + independent system prompt) — that isolation, not a feedback knob, is what
141
+ * defeats the self-evaluation trap. The agentic path additionally EXERCISES the artifact with scoped tools
142
+ * (it does not read the diff). The `jev` path composes `JevProvider` (a single classification call, ~100-1000x
143
+ * cheaper than a rubric round) for classification-shaped verdicts — the caller supplies a `toVerdict` mapping
144
+ * fn from the classifier's answer to the tri-state status. Composes AROUND a Loop (never inside `loop.js`).
113
145
  *
114
146
  * Built flagged-and-deletable per D11 — opt-in by import; calibrate the rubric/prompt from execution traces.
115
147
  */
116
148
  export class Evaluator {
117
149
  /**
118
150
  * @param {EvaluatorOptions} [options]
119
- * @when you need to judge an output against a goal or contract — deterministically (predicate), by LLM rubric, or with a tool-running critic that exercises the live artifact
151
+ * @when you need to judge an output against a goal or contract — deterministically (predicate), by LLM rubric, with a tool-running critic that exercises the live artifact, or by a cheap calibrated classifier (jev)
120
152
  * @fails never throws for a bad grade — returns a Verdict {status: satisfied|needs_revision|failed}; a provider HaltError propagates clean. Judge tokens forward via onLlmResult.
121
153
  * @example
122
154
  * const evaluator = new Evaluator({ provider });
@@ -128,14 +160,15 @@ export class Evaluator {
128
160
  prompt: string;
129
161
  agenticPrompt: string;
130
162
  tools: import("../types").ToolDef[];
163
+ jevProvider: import("./provider-jev").JevProvider | null;
131
164
  /**
132
165
  * Judge `result` against `goal` by exactly one criteria type.
133
166
  * @param {string} goal - The objective the result is judged against.
134
167
  * @param {any} result - The output under judgment.
135
- * @param {Criteria} criteria - Exactly one of `predicate` | `rubric` | `agentic` (none/more-than-one throws).
168
+ * @param {Criteria} criteria - Exactly one of `predicate` | `rubric` | `agentic` | `jev` (none/more-than-one throws).
136
169
  * @param {EvaluateOptions} [opts]
137
170
  * @returns {Promise<Verdict>}
138
- * @throws {ValidationError} not-exactly-one criteria supplied, or rubric/agentic requested with no provider.
171
+ * @throws {ValidationError} not-exactly-one criteria supplied, or rubric/agentic/jev requested with no provider.
139
172
  */
140
173
  evaluate(goal: string, result: any, criteria: Criteria, opts?: EvaluateOptions): Promise<Verdict>;
141
174
  /**
@@ -155,6 +188,29 @@ export class Evaluator {
155
188
  * @throws {HaltError} a governance cap halted the critic mid-run.
156
189
  */
157
190
  _evaluateAgentic(goal: string, result: any, instructions: string, contract: string | null, opts: EvaluateOptions): Promise<Verdict>;
191
+ /**
192
+ * Jev path — a cheap calibrated classifier verdict tier (the cost tier below `rubric`; ~100-1000x cheaper
193
+ * for classification-shaped verdicts). Unlike rubric/agentic there is NO layered prompt: `question.instructions`
194
+ * is the SOLE criterion sent to the classifier (a classifier takes one instruction, not goal+contract+rubric).
195
+ * `toVerdict` — caller-supplied (Option A: open + simple, no premature abstraction over jev's three answer
196
+ * shapes) — maps the classifier's answer to a tri-state status; the caller owns thresholds/bands.
197
+ * @param {any} result
198
+ * @param {{question: {type: 'noul'|'choice'|'score', instructions: string, criteria?: any}, toVerdict: (answer: any) => any}} jevCriteria
199
+ * @param {string|null} contract - Used ONLY as the failed-critique string (never folded into the question).
200
+ * @param {EvaluateOptions} opts
201
+ * @returns {Promise<Verdict>}
202
+ * @throws {ValidationError} no jevProvider, `toVerdict` isn't a function, or it returns something other
203
+ * than a valid tri-state status (BA-15 family — a broken arbiter is named loudly, never coerced; the
204
+ * error names the TYPE only, never the returned VALUE, F16/BA-1 audit-safety).
205
+ */
206
+ _evaluateJev(result: any, jevCriteria: {
207
+ question: {
208
+ type: "noul" | "choice" | "score";
209
+ instructions: string;
210
+ criteria?: any;
211
+ };
212
+ toVerdict: (answer: any) => any;
213
+ }, contract: string | null, opts: EvaluateOptions): Promise<Verdict>;
158
214
  /**
159
215
  * Defensive JSON parse of a grader response into a `Verdict` (mirrors `Planner._parse`).
160
216
  * @param {string} text
package/src/evaluator.js CHANGED
@@ -34,6 +34,9 @@ const { Loop } = require('./loop');
34
34
  * @property {ToolDef[]} [tools] - The critic's SCOPED functional tools (`barebrowse`/`baremobile`) for the
35
35
  * agentic path — what lets it exercise the live artifact rather than read text. Overridable per call via
36
36
  * `EvaluateOptions.tools`. Ignored by predicate/rubric.
37
+ * @property {import('./provider-jev').JevProvider} [jevProvider] - REQUIRED for the `jev` path — a cheap
38
+ * calibrated classifier tier (~100-1000x cheaper than a rubric LLM round for classification-shaped verdicts).
39
+ * Overridable per call via `EvaluateOptions.jevProvider`. Ignored by predicate/rubric/agentic.
37
40
  */
38
41
 
39
42
  /**
@@ -46,8 +49,17 @@ const { Loop } = require('./loop');
46
49
  * @property {string} [agentic] - Instructions for a tool-running critic (D9): how to EXERCISE the live artifact
47
50
  * (open it, click, read console/network) and what would make it fail. Runs an ISOLATED Loop with the scoped
48
51
  * `tools`. The strongest verification — catches what only running the thing reveals. Exactly one of the three.
52
+ * @property {{question: {type: 'noul'|'choice'|'score', instructions: string, criteria?: any}, toVerdict: (answer: any) => 'satisfied'|'needs_revision'|'failed'}} [jev] -
53
+ * Cheap calibrated classifier door (composes `JevProvider`, the cost tier below `rubric`). `question` is
54
+ * ONE Jev question object — the SOLE criterion (a classifier takes one instruction, not a layered prompt;
55
+ * unlike rubric/agentic, `contract` is NOT folded into it here — it is used only as the failed-critique
56
+ * string). `toVerdict` maps the classifier's per-question answer (`{type:'noul',noul}` /
57
+ * `{type:'choice',choice,...}` / `{type:'score',score,...}`) to a tri-state status — the caller owns
58
+ * thresholds/bands (Option A: caller-supplied mapping fn, open + simple, no premature abstraction over
59
+ * jev's three answer shapes). Requires a `jevProvider` (on the Evaluator or per-call). Exactly one of the four.
49
60
  * @property {string} [contract] - The shared, authoritative "definition of done" the grader judges against
50
- * (A3 / D10). When present it is what success means — not the loose goal. Folded into the rubric/agentic prompt.
61
+ * (A3 / D10). When present it is what success means — not the loose goal. Folded into the rubric/agentic
62
+ * prompt; for the `jev` door it is used ONLY as the failed-critique string (never folded into `question`).
51
63
  */
52
64
 
53
65
  /**
@@ -59,6 +71,8 @@ const { Loop } = require('./loop');
59
71
  * @property {ToolDef[]} [tools] - Per-call override of the agentic critic's scoped tools (else `EvaluatorOptions.tools`).
60
72
  * @property {Function} [policy] - bareguard `policy` forwarded to the agentic critic's Loop — a tool-running
61
73
  * critic MUST be bounded (turn/budget caps come from the gate; the Loop's HARD_ROUND_LIMIT is only a net).
74
+ * @property {import('./provider-jev').JevProvider} [jevProvider] - Per-call override of the `jev` door's classifier
75
+ * provider (else `EvaluatorOptions.jevProvider`).
62
76
  */
63
77
 
64
78
  // The adversarial grader system prompt — the anti-sycophancy core (A1, "Self-Evaluation is a Trap"). The
@@ -99,18 +113,20 @@ Output your FINAL answer as ONLY this JSON, no markdown, no prose:
99
113
 
100
114
  /**
101
115
  * Output-side judge — the mirror of `Planner` (input-side). Judges whether a result meets a goal, by a
102
- * deterministic `predicate`, an LLM `rubric`, or a tool-running `agentic` critic, returning one uniform
103
- * `Verdict`. The rubric and agentic paths run an ISOLATED adversarial critic (separate context + independent
104
- * system prompt) — that isolation, not a feedback knob, is what defeats the self-evaluation trap. The agentic
105
- * path additionally EXERCISES the artifact with scoped tools (it does not read the diff). Composes AROUND a
106
- * Loop (never inside `loop.js`).
116
+ * deterministic `predicate`, an LLM `rubric`, a tool-running `agentic` critic, or a cheap calibrated `jev`
117
+ * classifier tier, returning one uniform `Verdict`. The rubric and agentic paths run an ISOLATED adversarial
118
+ * critic (separate context + independent system prompt) — that isolation, not a feedback knob, is what
119
+ * defeats the self-evaluation trap. The agentic path additionally EXERCISES the artifact with scoped tools
120
+ * (it does not read the diff). The `jev` path composes `JevProvider` (a single classification call, ~100-1000x
121
+ * cheaper than a rubric round) for classification-shaped verdicts — the caller supplies a `toVerdict` mapping
122
+ * fn from the classifier's answer to the tri-state status. Composes AROUND a Loop (never inside `loop.js`).
107
123
  *
108
124
  * Built flagged-and-deletable per D11 — opt-in by import; calibrate the rubric/prompt from execution traces.
109
125
  */
110
126
  class Evaluator {
111
127
  /**
112
128
  * @param {EvaluatorOptions} [options]
113
- * @when you need to judge an output against a goal or contract — deterministically (predicate), by LLM rubric, or with a tool-running critic that exercises the live artifact
129
+ * @when you need to judge an output against a goal or contract — deterministically (predicate), by LLM rubric, with a tool-running critic that exercises the live artifact, or by a cheap calibrated classifier (jev)
114
130
  * @fails never throws for a bad grade — returns a Verdict {status: satisfied|needs_revision|failed}; a provider HaltError propagates clean. Judge tokens forward via onLlmResult.
115
131
  * @example
116
132
  * const evaluator = new Evaluator({ provider });
@@ -122,23 +138,25 @@ class Evaluator {
122
138
  this.prompt = options.prompt || GRADER_PROMPT;
123
139
  this.agenticPrompt = options.agenticPrompt || AGENTIC_PROMPT;
124
140
  this.tools = Array.isArray(options.tools) ? options.tools : [];
141
+ this.jevProvider = options.jevProvider || null;
125
142
  }
126
143
 
127
144
  /**
128
145
  * Judge `result` against `goal` by exactly one criteria type.
129
146
  * @param {string} goal - The objective the result is judged against.
130
147
  * @param {any} result - The output under judgment.
131
- * @param {Criteria} criteria - Exactly one of `predicate` | `rubric` | `agentic` (none/more-than-one throws).
148
+ * @param {Criteria} criteria - Exactly one of `predicate` | `rubric` | `agentic` | `jev` (none/more-than-one throws).
132
149
  * @param {EvaluateOptions} [opts]
133
150
  * @returns {Promise<Verdict>}
134
- * @throws {ValidationError} not-exactly-one criteria supplied, or rubric/agentic requested with no provider.
151
+ * @throws {ValidationError} not-exactly-one criteria supplied, or rubric/agentic/jev requested with no provider.
135
152
  */
136
153
  async evaluate(goal, result, criteria, opts = {}) {
137
154
  const predicate = typeof criteria?.predicate === 'function' ? criteria.predicate : null;
138
155
  const rubric = typeof criteria?.rubric === 'string' && criteria.rubric.length > 0 ? criteria.rubric : null;
139
156
  const agentic = typeof criteria?.agentic === 'string' && criteria.agentic.length > 0 ? criteria.agentic : null;
140
- if ([predicate, rubric, agentic].filter(Boolean).length !== 1) {
141
- throw new ValidationError('[Evaluator] criteria must supply exactly one of { predicate } | { rubric } | { agentic }');
157
+ const jev = criteria && typeof criteria.jev === 'object' && criteria.jev !== null ? criteria.jev : null;
158
+ if ([predicate, rubric, agentic, jev].filter(Boolean).length !== 1) {
159
+ throw new ValidationError('[Evaluator] criteria must supply exactly one of { predicate } | { rubric } | { agentic } | { jev }');
142
160
  }
143
161
 
144
162
  if (predicate) {
@@ -184,6 +202,12 @@ class Evaluator {
184
202
  return this._evaluateAgentic(goal, result, agentic, contract, opts);
185
203
  }
186
204
 
205
+ // Jev path — a cheap calibrated classifier tier (the cost tier below rubric). No layered prompt: the
206
+ // question's `instructions` is the sole criterion; `contract` (if any) is used only as the failed-critique.
207
+ if (jev) {
208
+ return this._evaluateJev(result, jev, contract, opts);
209
+ }
210
+
187
211
  // Rubric path — isolated adversarial grader.
188
212
  if (!this.provider) {
189
213
  throw new ValidationError('[Evaluator] rubric criteria requires a provider on the Evaluator');
@@ -275,6 +299,63 @@ class Evaluator {
275
299
  return this._parse(out.text);
276
300
  }
277
301
 
302
+ /**
303
+ * Jev path — a cheap calibrated classifier verdict tier (the cost tier below `rubric`; ~100-1000x cheaper
304
+ * for classification-shaped verdicts). Unlike rubric/agentic there is NO layered prompt: `question.instructions`
305
+ * is the SOLE criterion sent to the classifier (a classifier takes one instruction, not goal+contract+rubric).
306
+ * `toVerdict` — caller-supplied (Option A: open + simple, no premature abstraction over jev's three answer
307
+ * shapes) — maps the classifier's answer to a tri-state status; the caller owns thresholds/bands.
308
+ * @param {any} result
309
+ * @param {{question: {type: 'noul'|'choice'|'score', instructions: string, criteria?: any}, toVerdict: (answer: any) => any}} jevCriteria
310
+ * @param {string|null} contract - Used ONLY as the failed-critique string (never folded into the question).
311
+ * @param {EvaluateOptions} opts
312
+ * @returns {Promise<Verdict>}
313
+ * @throws {ValidationError} no jevProvider, `toVerdict` isn't a function, or it returns something other
314
+ * than a valid tri-state status (BA-15 family — a broken arbiter is named loudly, never coerced; the
315
+ * error names the TYPE only, never the returned VALUE, F16/BA-1 audit-safety).
316
+ */
317
+ async _evaluateJev(result, jevCriteria, contract, opts) {
318
+ const jevProvider = opts.jevProvider || this.jevProvider;
319
+ if (!jevProvider) {
320
+ throw new ValidationError('[Evaluator] jev criteria requires a jevProvider (on the Evaluator or per-call opts.jevProvider)');
321
+ }
322
+ const toVerdict = typeof jevCriteria?.toVerdict === 'function' ? jevCriteria.toVerdict : null;
323
+ if (!toVerdict) {
324
+ throw new ValidationError('[Evaluator] jev criteria requires a toVerdict(answer) => status function');
325
+ }
326
+ const question = jevCriteria.question;
327
+ const forward = opts.onLlmResult;
328
+
329
+ const out = await jevProvider.classify(stringifyResult(result), { q: question }, {
330
+ // Re-tag jev's own kind:'classify' as kind:'evaluate' at the Evaluator boundary (mirrors rubric/agentic).
331
+ // A HaltError thrown by the consumer's hook propagates clean (JevProvider awaits it, no swallow).
332
+ onLlmResult: forward
333
+ ? async (/** @type {any} */ e) => { await forward({ usage: e.usage, model: e.model, kind: 'evaluate' }); }
334
+ : undefined,
335
+ });
336
+ const answer = out.answers.q;
337
+
338
+ const status = await toVerdict(answer);
339
+ if (status !== 'satisfied' && status !== 'needs_revision' && status !== 'failed') {
340
+ // Name the TYPE only, never the value — mirrors the predicate door's broken-arbiter guard (BA-15/F16).
341
+ const got = status === null ? 'null'
342
+ : status === undefined ? 'undefined'
343
+ : Array.isArray(status) ? 'an array'
344
+ : typeof status === 'object' ? 'an object'
345
+ : `a ${typeof status}`;
346
+ throw new ValidationError(
347
+ `[Evaluator] jev criteria toVerdict must return 'satisfied'|'needs_revision'|'failed', got ${got}.`,
348
+ );
349
+ }
350
+ return {
351
+ status,
352
+ pass: status === 'satisfied',
353
+ score: null,
354
+ critique: status === 'satisfied' ? '' : (typeof contract === 'string' ? contract : ''),
355
+ suggestions: [],
356
+ };
357
+ }
358
+
278
359
  /**
279
360
  * Defensive JSON parse of a grader response into a `Verdict` (mirrors `Planner._parse`).
280
361
  * @param {string} text
@@ -21,7 +21,7 @@ export class JevProvider {
21
21
  * @param {number} [options.deadlineMs] - Total call-duration deadline (ms); 0 disables (default).
22
22
  * @param {{in: number, out: number, cacheReadMult?: number, cacheWriteMult?: number}} [options.rates] - Per-1K-token USD rates for authoritative pricing (Jev: `{ in: 0.042/1000, out: 0 }`).
23
23
  * @param {boolean} [options.exposeErrorBody=false] - Include the raw error body on a ProviderError (default off).
24
- * @param {boolean} [options.harden=true] - Prepend a defensive preamble to each question's instructions, treating `state` as untrusted data and resisting embedded role/label-override attempts. Overridable per-call via `opts.harden`.
24
+ * @param {boolean} [options.harden=true] - Wrap each question's instructions with a defensive preamble (prefixed for a string, prepended as element 0 for an array, or added under a reserved key for an object), treating `state` as untrusted data and resisting embedded role/label-override attempts. Overridable per-call via `opts.harden`.
25
25
  */
26
26
  constructor(options?: {
27
27
  apiKey?: string | undefined;
@@ -54,7 +54,7 @@ export class JevProvider {
54
54
  /**
55
55
  * Classify `state` against one or more typed `questions`. See the class doc for the primitive tags.
56
56
  * @param {string|object|any[]} state - The shared input all questions judge (Jev's `state`).
57
- * @param {Record<string, {type: 'noul'|'choice'|'score', instructions: string, criteria?: any}>} questions - Keyed questions; each judged independently against `state`.
57
+ * @param {Record<string, {type: 'noul'|'choice'|'score', instructions: string|object|any[], criteria?: any}>} questions - Keyed questions; each judged independently against `state`.
58
58
  * @param {object} [opts]
59
59
  * @param {string} [opts.model] - Override the model for this call.
60
60
  * @param {{in: number, out: number, cacheReadMult?: number, cacheWriteMult?: number}} [opts.rates] - Override rates for this call.
@@ -68,7 +68,7 @@ export class JevProvider {
68
68
  */
69
69
  classify(state: string | object | any[], questions: Record<string, {
70
70
  type: "noul" | "choice" | "score";
71
- instructions: string;
71
+ instructions: string | object | any[];
72
72
  criteria?: any;
73
73
  }>, opts?: {
74
74
  model?: string | undefined;
@@ -101,9 +101,13 @@ export class JevProvider {
101
101
  */
102
102
  _validateRequest(state: any, questions: any): void;
103
103
  /**
104
- * Build a hardened COPY of `questions` (new object, new nested question objects) with
105
- * {@link HARDENING_PREAMBLE} prepended to each `instructions` string. Never mutates the
106
- * caller's `questions` argument or its nested objects. @param {Record<string, any>} questions @returns {Record<string, any>}
104
+ * Build a hardened COPY of `questions` (new object, new nested question objects, new
105
+ * nested instructions containers) with {@link HARDENING_PREAMBLE} woven into each
106
+ * `instructions`, shape-aware so a structured form is wrapped rather than stringified:
107
+ * a string gets the preamble prefixed, an array gets it prepended as element 0, and an
108
+ * object gets it added under the reserved {@link HARDENING_KEY}. Never mutates the
109
+ * caller's `questions` argument or its nested objects/arrays.
110
+ * @param {Record<string, any>} questions @returns {Record<string, any>}
107
111
  */
108
112
  _hardenQuestions(questions: Record<string, any>): Record<string, any>;
109
113
  /**
@@ -36,6 +36,9 @@ const CLASSIFY_PATH = '/v1/systemone';
36
36
  const DEFAULT_MODEL = 'jev-latest';
37
37
  const QUESTION_TYPES = new Set(['noul', 'choice', 'score']);
38
38
  const JEV_USAGE_KEYS = ['input_tokens', 'output_tokens'];
39
+ // Reserved key hardening uses to carry the preamble on an object-shaped `instructions` — a
40
+ // caller-supplied key of the same name would silently overwrite the preamble on spread.
41
+ const HARDENING_KEY = '__hardening__';
39
42
 
40
43
  // Injection hardening (default on): prepended to each question's `instructions` before the
41
44
  // request is sent, so an attack embedded in `state` (untrusted) can't hijack the classifier's
@@ -73,7 +76,7 @@ class JevProvider {
73
76
  * @param {number} [options.deadlineMs] - Total call-duration deadline (ms); 0 disables (default).
74
77
  * @param {{in: number, out: number, cacheReadMult?: number, cacheWriteMult?: number}} [options.rates] - Per-1K-token USD rates for authoritative pricing (Jev: `{ in: 0.042/1000, out: 0 }`).
75
78
  * @param {boolean} [options.exposeErrorBody=false] - Include the raw error body on a ProviderError (default off).
76
- * @param {boolean} [options.harden=true] - Prepend a defensive preamble to each question's instructions, treating `state` as untrusted data and resisting embedded role/label-override attempts. Overridable per-call via `opts.harden`.
79
+ * @param {boolean} [options.harden=true] - Wrap each question's instructions with a defensive preamble (prefixed for a string, prepended as element 0 for an array, or added under a reserved key for an object), treating `state` as untrusted data and resisting embedded role/label-override attempts. Overridable per-call via `opts.harden`.
77
80
  */
78
81
  constructor(options = {}) {
79
82
  this.apiKey = options.apiKey;
@@ -89,7 +92,7 @@ class JevProvider {
89
92
  /**
90
93
  * Classify `state` against one or more typed `questions`. See the class doc for the primitive tags.
91
94
  * @param {string|object|any[]} state - The shared input all questions judge (Jev's `state`).
92
- * @param {Record<string, {type: 'noul'|'choice'|'score', instructions: string, criteria?: any}>} questions - Keyed questions; each judged independently against `state`.
95
+ * @param {Record<string, {type: 'noul'|'choice'|'score', instructions: string|object|any[], criteria?: any}>} questions - Keyed questions; each judged independently against `state`.
93
96
  * @param {object} [opts]
94
97
  * @param {string} [opts.model] - Override the model for this call.
95
98
  * @param {{in: number, out: number, cacheReadMult?: number, cacheWriteMult?: number}} [opts.rates] - Override rates for this call.
@@ -139,7 +142,14 @@ class JevProvider {
139
142
  const q = questions[id];
140
143
  if (!isPlainObject(q)) throw invalid(`question "${id}" must be an object`);
141
144
  if (!QUESTION_TYPES.has(q.type)) throw invalid(`question "${id}" has invalid type`, { type: q.type });
142
- if (typeof q.instructions !== 'string' || !q.instructions) throw invalid(`question "${id}" missing instructions`);
145
+ const ins = q.instructions;
146
+ const insValid = (typeof ins === 'string' && ins.length > 0) ||
147
+ (Array.isArray(ins) && ins.length > 0) ||
148
+ (isPlainObject(ins) && Object.keys(ins).length > 0);
149
+ if (!insValid) throw invalid(`question "${id}" missing instructions`);
150
+ if (isPlainObject(q.instructions) && Object.prototype.hasOwnProperty.call(q.instructions, HARDENING_KEY)) {
151
+ throw invalid(`question "${id}" may not use reserved instructions key`, { key: HARDENING_KEY });
152
+ }
143
153
  if (q.type === 'choice') {
144
154
  if (!isPlainObject(q.criteria) || Object.keys(q.criteria).length < 2) {
145
155
  throw invalid(`choice "${id}" needs a criteria object of >=2 {key: description}`);
@@ -156,16 +166,29 @@ class JevProvider {
156
166
  }
157
167
 
158
168
  /**
159
- * Build a hardened COPY of `questions` (new object, new nested question objects) with
160
- * {@link HARDENING_PREAMBLE} prepended to each `instructions` string. Never mutates the
161
- * caller's `questions` argument or its nested objects. @param {Record<string, any>} questions @returns {Record<string, any>}
169
+ * Build a hardened COPY of `questions` (new object, new nested question objects, new
170
+ * nested instructions containers) with {@link HARDENING_PREAMBLE} woven into each
171
+ * `instructions`, shape-aware so a structured form is wrapped rather than stringified:
172
+ * a string gets the preamble prefixed, an array gets it prepended as element 0, and an
173
+ * object gets it added under the reserved {@link HARDENING_KEY}. Never mutates the
174
+ * caller's `questions` argument or its nested objects/arrays.
175
+ * @param {Record<string, any>} questions @returns {Record<string, any>}
162
176
  */
163
177
  _hardenQuestions(questions) {
164
178
  /** @type {Record<string, any>} */
165
179
  const hardened = {};
166
180
  for (const id of Object.keys(questions)) {
167
181
  const q = questions[id];
168
- hardened[id] = { ...q, instructions: HARDENING_PREAMBLE + q.instructions };
182
+ const ins = q.instructions;
183
+ let hardenedIns;
184
+ if (typeof ins === 'string') {
185
+ hardenedIns = HARDENING_PREAMBLE + ins;
186
+ } else if (Array.isArray(ins)) {
187
+ hardenedIns = [HARDENING_PREAMBLE, ...ins];
188
+ } else {
189
+ hardenedIns = { [HARDENING_KEY]: HARDENING_PREAMBLE, ...ins };
190
+ }
191
+ hardened[id] = { ...q, instructions: hardenedIns };
169
192
  }
170
193
  return hardened;
171
194
  }