@sweny-ai/core 0.1.84 → 0.1.85
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/claude.d.ts +1 -0
- package/dist/claude.js +5 -4
- package/dist/eval/function.d.ts +25 -0
- package/dist/eval/function.js +106 -0
- package/dist/eval/index.d.ts +41 -0
- package/dist/eval/index.js +91 -0
- package/dist/eval/judge.d.ts +43 -0
- package/dist/eval/judge.js +142 -0
- package/dist/eval/path.d.ts +9 -0
- package/dist/eval/path.js +72 -0
- package/dist/eval/value.d.ts +15 -0
- package/dist/eval/value.js +205 -0
- package/dist/executor.js +85 -18
- package/dist/requires.js +2 -2
- package/dist/retry.d.ts +17 -3
- package/dist/retry.js +29 -14
- package/dist/schema.d.ts +778 -316
- package/dist/schema.js +200 -60
- package/dist/skills/index.js +1 -1
- package/dist/types.d.ts +96 -33
- package/dist/workflows/browser.js +1 -1
- package/dist/workflows/triage.yml +68 -28
- package/package.json +1 -1
- package/dist/verify.d.ts +0 -25
- package/dist/verify.js +0 -357
package/dist/claude.d.ts
CHANGED
package/dist/claude.js
CHANGED
|
@@ -158,8 +158,8 @@ export class ClaudeClient {
|
|
|
158
158
|
const parsed = parseToolResultContent(block.content);
|
|
159
159
|
call.output = isError ? { error: parsed } : parsed;
|
|
160
160
|
// Surface tool errors in the CI log stream at warn level so
|
|
161
|
-
// postmortems don't have to reconstruct them from
|
|
162
|
-
// tool-call summaries. Without this, "
|
|
161
|
+
// postmortems don't have to reconstruct them from eval-time
|
|
162
|
+
// tool-call summaries. Without this, "eval failed: tool X
|
|
163
163
|
// did not succeed" never answers the WHY question, because
|
|
164
164
|
// the error body is buried in the per-call output captured
|
|
165
165
|
// only for in-memory verify evaluation.
|
|
@@ -263,7 +263,7 @@ export class ClaudeClient {
|
|
|
263
263
|
return validIds[0];
|
|
264
264
|
}
|
|
265
265
|
async ask(opts) {
|
|
266
|
-
const { instruction, context } = opts;
|
|
266
|
+
const { instruction, context, model } = opts;
|
|
267
267
|
const prompt = [
|
|
268
268
|
instruction,
|
|
269
269
|
Object.keys(context).length > 0 ? `\nContext:\n\`\`\`json\n${JSON.stringify(context, null, 2)}\n\`\`\`` : "",
|
|
@@ -272,6 +272,7 @@ export class ClaudeClient {
|
|
|
272
272
|
.join("\n");
|
|
273
273
|
const env = this.buildEnv();
|
|
274
274
|
let response = "";
|
|
275
|
+
const effectiveModel = model ?? this.model;
|
|
275
276
|
try {
|
|
276
277
|
const stream = query({
|
|
277
278
|
prompt,
|
|
@@ -282,7 +283,7 @@ export class ClaudeClient {
|
|
|
282
283
|
permissionMode: "bypassPermissions",
|
|
283
284
|
allowDangerouslySkipPermissions: true,
|
|
284
285
|
stderr: (data) => this.logger.debug(`[claude-code] ${data}`),
|
|
285
|
-
...(
|
|
286
|
+
...(effectiveModel ? { model: effectiveModel } : {}),
|
|
286
287
|
},
|
|
287
288
|
});
|
|
288
289
|
for await (const message of stream) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EvaluatorRule, ToolCall } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Legacy output-shape heuristic: a tool call "succeeded" when its output
|
|
4
|
+
* does not carry a non-null `error` field. Kept as a fallback for ToolCall
|
|
5
|
+
* records that predate the explicit `status` field (hand-constructed in
|
|
6
|
+
* tests, older runtimes). The Claude runtime now sets `status` directly.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isErrorOutput(output: unknown): boolean;
|
|
9
|
+
export type ToolAliases = ReadonlyMap<string, ReadonlySet<string>>;
|
|
10
|
+
export declare function checkAnyToolCalled(required: string[], toolCalls: ToolCall[], aliases?: ToolAliases): string | null;
|
|
11
|
+
export declare function checkAllToolsCalled(required: string[], toolCalls: ToolCall[], aliases?: ToolAliases): string | null;
|
|
12
|
+
export declare function checkNoToolCalled(forbidden: string[], toolCalls: ToolCall[], aliases?: ToolAliases): string | null;
|
|
13
|
+
/**
|
|
14
|
+
* Evaluate a `function` rule against the node's tool-call trace.
|
|
15
|
+
*
|
|
16
|
+
* Returns `{ pass: true }` when every declared check passes, otherwise
|
|
17
|
+
* `{ pass: false, reasoning: <semicolon-joined failure list> }`.
|
|
18
|
+
*
|
|
19
|
+
* Value-rule fields (`output_required` / `output_matches`) are ignored
|
|
20
|
+
* even if present.
|
|
21
|
+
*/
|
|
22
|
+
export declare function evaluateFunctionRule(rule: EvaluatorRule, toolCalls: ToolCall[], aliases?: ToolAliases): {
|
|
23
|
+
pass: boolean;
|
|
24
|
+
reasoning?: string;
|
|
25
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// ─── Eval: function (trace-shape) evaluator ──────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Operates on the node's tool-call trace. Accepts an alias map so a workflow
|
|
4
|
+
// rule can reference a canonical tool name and match against an equivalent
|
|
5
|
+
// MCP tool name. Each skill owns its own alias map; core stays vendor-neutral.
|
|
6
|
+
// ─── Tool-call success heuristic ─────────────────────────────────────
|
|
7
|
+
/**
|
|
8
|
+
* Legacy output-shape heuristic: a tool call "succeeded" when its output
|
|
9
|
+
* does not carry a non-null `error` field. Kept as a fallback for ToolCall
|
|
10
|
+
* records that predate the explicit `status` field (hand-constructed in
|
|
11
|
+
* tests, older runtimes). The Claude runtime now sets `status` directly.
|
|
12
|
+
*/
|
|
13
|
+
export function isErrorOutput(output) {
|
|
14
|
+
if (!output || typeof output !== "object")
|
|
15
|
+
return false;
|
|
16
|
+
const err = output.error;
|
|
17
|
+
return err !== undefined && err !== null && err !== false;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A tool call is considered successful when:
|
|
21
|
+
* - explicit `status === "success"`, OR
|
|
22
|
+
* - no explicit status AND output does not look like an error.
|
|
23
|
+
*
|
|
24
|
+
* `status` is authoritative. It's the only signal we have for external MCP
|
|
25
|
+
* tools whose structured output isn't exposed to the workflow runtime.
|
|
26
|
+
*/
|
|
27
|
+
function didSucceed(c) {
|
|
28
|
+
if (c.status === "error")
|
|
29
|
+
return false;
|
|
30
|
+
if (c.status === "success")
|
|
31
|
+
return true;
|
|
32
|
+
return !isErrorOutput(c.output);
|
|
33
|
+
}
|
|
34
|
+
function succeededTools(toolCalls) {
|
|
35
|
+
const names = new Set();
|
|
36
|
+
for (const c of toolCalls) {
|
|
37
|
+
if (didSucceed(c))
|
|
38
|
+
names.add(c.tool);
|
|
39
|
+
}
|
|
40
|
+
return names;
|
|
41
|
+
}
|
|
42
|
+
function calledList(toolCalls) {
|
|
43
|
+
return toolCalls.map((c) => c.tool).join(", ") || "none";
|
|
44
|
+
}
|
|
45
|
+
function expandAliases(name, aliases) {
|
|
46
|
+
return aliases?.get(name) ?? new Set([name]);
|
|
47
|
+
}
|
|
48
|
+
function anyAliasIn(name, set, aliases) {
|
|
49
|
+
for (const alias of expandAliases(name, aliases)) {
|
|
50
|
+
if (set.has(alias))
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
// ─── Public check helpers ────────────────────────────────────────────
|
|
56
|
+
export function checkAnyToolCalled(required, toolCalls, aliases) {
|
|
57
|
+
const succeeded = succeededTools(toolCalls);
|
|
58
|
+
if (required.some((t) => anyAliasIn(t, succeeded, aliases)))
|
|
59
|
+
return null;
|
|
60
|
+
return `any_tool_called: required one of [${required.join(", ")}] to succeed, called: [${calledList(toolCalls)}]`;
|
|
61
|
+
}
|
|
62
|
+
export function checkAllToolsCalled(required, toolCalls, aliases) {
|
|
63
|
+
const succeeded = succeededTools(toolCalls);
|
|
64
|
+
const missing = required.filter((t) => !anyAliasIn(t, succeeded, aliases));
|
|
65
|
+
if (missing.length === 0)
|
|
66
|
+
return null;
|
|
67
|
+
return `all_tools_called: missing successful calls to [${missing.join(", ")}], called: [${calledList(toolCalls)}]`;
|
|
68
|
+
}
|
|
69
|
+
export function checkNoToolCalled(forbidden, toolCalls, aliases) {
|
|
70
|
+
const calledNames = new Set(toolCalls.map((c) => c.tool));
|
|
71
|
+
const violated = forbidden.filter((t) => anyAliasIn(t, calledNames, aliases));
|
|
72
|
+
if (violated.length === 0)
|
|
73
|
+
return null;
|
|
74
|
+
return `no_tool_called: forbidden tools were invoked: [${violated.join(", ")}], called: [${calledList(toolCalls)}]`;
|
|
75
|
+
}
|
|
76
|
+
// ─── Evaluator entry point ───────────────────────────────────────────
|
|
77
|
+
/**
|
|
78
|
+
* Evaluate a `function` rule against the node's tool-call trace.
|
|
79
|
+
*
|
|
80
|
+
* Returns `{ pass: true }` when every declared check passes, otherwise
|
|
81
|
+
* `{ pass: false, reasoning: <semicolon-joined failure list> }`.
|
|
82
|
+
*
|
|
83
|
+
* Value-rule fields (`output_required` / `output_matches`) are ignored
|
|
84
|
+
* even if present.
|
|
85
|
+
*/
|
|
86
|
+
export function evaluateFunctionRule(rule, toolCalls, aliases) {
|
|
87
|
+
const failures = [];
|
|
88
|
+
if (rule.any_tool_called && rule.any_tool_called.length > 0) {
|
|
89
|
+
const e = checkAnyToolCalled(rule.any_tool_called, toolCalls, aliases);
|
|
90
|
+
if (e)
|
|
91
|
+
failures.push(e);
|
|
92
|
+
}
|
|
93
|
+
if (rule.all_tools_called && rule.all_tools_called.length > 0) {
|
|
94
|
+
const e = checkAllToolsCalled(rule.all_tools_called, toolCalls, aliases);
|
|
95
|
+
if (e)
|
|
96
|
+
failures.push(e);
|
|
97
|
+
}
|
|
98
|
+
if (rule.no_tool_called && rule.no_tool_called.length > 0) {
|
|
99
|
+
const e = checkNoToolCalled(rule.no_tool_called, toolCalls, aliases);
|
|
100
|
+
if (e)
|
|
101
|
+
failures.push(e);
|
|
102
|
+
}
|
|
103
|
+
if (failures.length === 0)
|
|
104
|
+
return { pass: true };
|
|
105
|
+
return { pass: false, reasoning: failures.join("; ") };
|
|
106
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Claude, EvalPolicy, EvalResult, Evaluator, NodeResult, Workflow, Node } from "../types.js";
|
|
2
|
+
import { type ToolAliases } from "./function.js";
|
|
3
|
+
export type { ToolAliases } from "./function.js";
|
|
4
|
+
export interface EvaluateAllOptions {
|
|
5
|
+
/** Tool-name aliases from the loaded skills. Used by `function` evaluators only. */
|
|
6
|
+
aliases?: ToolAliases;
|
|
7
|
+
/** Optional Claude client. Required when any evaluator has kind: "judge". */
|
|
8
|
+
claude?: Claude;
|
|
9
|
+
/** The node being evaluated. Used to resolve per-node `judge_model` overrides. */
|
|
10
|
+
node?: Node;
|
|
11
|
+
/** The workflow being executed. Used to resolve workflow-level `judge_model` overrides. */
|
|
12
|
+
workflow?: Workflow;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Run every declared evaluator against the node's result and return one
|
|
16
|
+
* {@link EvalResult} per evaluator, in declaration order.
|
|
17
|
+
*
|
|
18
|
+
* The function never throws on a failing rule (failures become
|
|
19
|
+
* `{ pass: false, reasoning }` results). It DOES throw if the underlying
|
|
20
|
+
* judge implementation throws (e.g. v1.0 judge stub) so the executor can
|
|
21
|
+
* surface the error clearly at the call site.
|
|
22
|
+
*/
|
|
23
|
+
export declare function evaluateAll(evaluators: Evaluator[] | undefined, result: NodeResult, opts?: EvaluateAllOptions): Promise<EvalResult[]>;
|
|
24
|
+
export interface AggregateOutcome {
|
|
25
|
+
/** Whether the node passed under the configured policy. */
|
|
26
|
+
pass: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Structured failure message for the executor when `pass` is false.
|
|
29
|
+
* Format: header line + one bulleted line per failing evaluator.
|
|
30
|
+
* Suitable as input to {@link buildRetryPreamble}.
|
|
31
|
+
*/
|
|
32
|
+
error?: string;
|
|
33
|
+
/** Direct access to the failed evaluator results, in declaration order. */
|
|
34
|
+
failures: EvalResult[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Collapse a list of {@link EvalResult}s into a single pass/fail under the
|
|
38
|
+
* node's `eval_policy`. v1 implements `all_pass`. Other policies are
|
|
39
|
+
* accepted at parse time but rejected here so users get a clear error.
|
|
40
|
+
*/
|
|
41
|
+
export declare function aggregateEval(results: EvalResult[], policy?: EvalPolicy): AggregateOutcome;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// ─── Eval: dispatcher + aggregation ──────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// `evaluateAll(...)` runs every declared evaluator and returns a structured
|
|
4
|
+
// list of EvalResults. `aggregateEval(...)` collapses that list into a
|
|
5
|
+
// single pass/fail under the node's eval_policy. v1 implements `all_pass`.
|
|
6
|
+
import { evaluateValueRule } from "./value.js";
|
|
7
|
+
import { evaluateFunctionRule } from "./function.js";
|
|
8
|
+
import { evaluateJudge } from "./judge.js";
|
|
9
|
+
/** Reasoning cap, matching the spec's ~500-character ceiling. */
|
|
10
|
+
const MAX_REASONING_LENGTH = 500;
|
|
11
|
+
/** Default judge model when neither evaluator nor node nor workflow specify one. */
|
|
12
|
+
const DEFAULT_JUDGE_MODEL = "claude-haiku-4-5";
|
|
13
|
+
function capReasoning(reasoning) {
|
|
14
|
+
if (!reasoning)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (reasoning.length <= MAX_REASONING_LENGTH)
|
|
17
|
+
return reasoning;
|
|
18
|
+
return reasoning.slice(0, MAX_REASONING_LENGTH - 1) + "…";
|
|
19
|
+
}
|
|
20
|
+
function resolveJudgeModel(evaluator, node, workflow) {
|
|
21
|
+
return evaluator.model ?? node?.judge_model ?? workflow?.judge_model ?? DEFAULT_JUDGE_MODEL;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Run every declared evaluator against the node's result and return one
|
|
25
|
+
* {@link EvalResult} per evaluator, in declaration order.
|
|
26
|
+
*
|
|
27
|
+
* The function never throws on a failing rule (failures become
|
|
28
|
+
* `{ pass: false, reasoning }` results). It DOES throw if the underlying
|
|
29
|
+
* judge implementation throws (e.g. v1.0 judge stub) so the executor can
|
|
30
|
+
* surface the error clearly at the call site.
|
|
31
|
+
*/
|
|
32
|
+
export async function evaluateAll(evaluators, result, opts = {}) {
|
|
33
|
+
if (!evaluators || evaluators.length === 0)
|
|
34
|
+
return [];
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const e of evaluators) {
|
|
37
|
+
if (e.kind === "value") {
|
|
38
|
+
const verdict = evaluateValueRule(e.rule ?? {}, result.data);
|
|
39
|
+
out.push({
|
|
40
|
+
name: e.name,
|
|
41
|
+
kind: "value",
|
|
42
|
+
pass: verdict.pass,
|
|
43
|
+
reasoning: capReasoning(verdict.reasoning),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else if (e.kind === "function") {
|
|
47
|
+
const verdict = evaluateFunctionRule(e.rule ?? {}, result.toolCalls, opts.aliases);
|
|
48
|
+
out.push({
|
|
49
|
+
name: e.name,
|
|
50
|
+
kind: "function",
|
|
51
|
+
pass: verdict.pass,
|
|
52
|
+
reasoning: capReasoning(verdict.reasoning),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else if (e.kind === "judge") {
|
|
56
|
+
if (!opts.claude) {
|
|
57
|
+
throw new Error(`evaluator '${e.name}' is kind: judge but no Claude client was provided to evaluateAll`);
|
|
58
|
+
}
|
|
59
|
+
const model = resolveJudgeModel(e, opts.node, opts.workflow);
|
|
60
|
+
const verdict = await evaluateJudge(e, result, opts.claude, { model });
|
|
61
|
+
out.push({
|
|
62
|
+
name: e.name,
|
|
63
|
+
kind: "judge",
|
|
64
|
+
pass: verdict.pass,
|
|
65
|
+
reasoning: capReasoning(verdict.reasoning),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Collapse a list of {@link EvalResult}s into a single pass/fail under the
|
|
73
|
+
* node's `eval_policy`. v1 implements `all_pass`. Other policies are
|
|
74
|
+
* accepted at parse time but rejected here so users get a clear error.
|
|
75
|
+
*/
|
|
76
|
+
export function aggregateEval(results, policy = "all_pass") {
|
|
77
|
+
if (results.length === 0)
|
|
78
|
+
return { pass: true, failures: [] };
|
|
79
|
+
if (policy === "all_pass") {
|
|
80
|
+
const failures = results.filter((r) => !r.pass);
|
|
81
|
+
if (failures.length === 0)
|
|
82
|
+
return { pass: true, failures: [] };
|
|
83
|
+
const lines = failures.map((f) => ` - ${f.name} (${f.kind}): ${f.reasoning ?? "no reasoning"}`);
|
|
84
|
+
return {
|
|
85
|
+
pass: false,
|
|
86
|
+
error: `eval failed (policy: all_pass):\n${lines.join("\n")}`,
|
|
87
|
+
failures,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`eval_policy '${policy}' is reserved in v1.0 but not yet implemented; use 'all_pass' (default) for v1.0 workflows`);
|
|
91
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Claude, Evaluator, NodeResult } from "../types.js";
|
|
2
|
+
export interface JudgeOptions {
|
|
3
|
+
/** Resolved judge model (evaluator > node > workflow > package default). */
|
|
4
|
+
model: string;
|
|
5
|
+
}
|
|
6
|
+
interface ParsedResponse {
|
|
7
|
+
verdict: string | null;
|
|
8
|
+
reasoning: string | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Build the structured judge prompt.
|
|
12
|
+
*
|
|
13
|
+
* The prompt format is fixed so the parser can rely on it. Model authors
|
|
14
|
+
* MUST NOT change the section markers without updating {@link parseJudgeResponse}
|
|
15
|
+
* in lockstep.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildJudgePrompt(evaluator: Evaluator, result: NodeResult): string;
|
|
18
|
+
/**
|
|
19
|
+
* Parse a judge response into `{ verdict, reasoning }`.
|
|
20
|
+
*
|
|
21
|
+
* Format expected:
|
|
22
|
+
* VERDICT: <token>
|
|
23
|
+
* REASONING: <text...>
|
|
24
|
+
*
|
|
25
|
+
* Lines may have any leading whitespace. Reasoning may span multiple lines
|
|
26
|
+
* (everything after the `REASONING:` marker is consumed). Returns null
|
|
27
|
+
* fields when the markers are missing.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseJudgeResponse(text: string): ParsedResponse;
|
|
30
|
+
/**
|
|
31
|
+
* Run a single judge evaluator against a node result.
|
|
32
|
+
*
|
|
33
|
+
* Resolves a verdict by calling the judge model, parsing VERDICT/REASONING,
|
|
34
|
+
* and comparing the verdict to `pass_when` (default `yes`). On parse
|
|
35
|
+
* failure, retries the call once. If the retry also fails to parse,
|
|
36
|
+
* returns `pass: false` with a "judge parse failure" reasoning instead of
|
|
37
|
+
* throwing, so a single flaky model response doesn't halt the workflow.
|
|
38
|
+
*/
|
|
39
|
+
export declare function evaluateJudge(evaluator: Evaluator, result: NodeResult, claude: Claude, opts: JudgeOptions): Promise<{
|
|
40
|
+
pass: boolean;
|
|
41
|
+
reasoning?: string;
|
|
42
|
+
}>;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// ─── Eval: judge (LLM-as-judge) evaluator ────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Calls a small Claude model with the node's data, the node's tool-call
|
|
4
|
+
// trace, and the author's rubric. Parses a verdict token and a short
|
|
5
|
+
// reasoning paragraph from a structured response.
|
|
6
|
+
//
|
|
7
|
+
// Cost: one model call per judge evaluator per node attempt. Judges are
|
|
8
|
+
// expected to use a small fast model (default `claude-haiku-4-5`); the
|
|
9
|
+
// reasoning is capped at ~500 chars before it lands on EvalResult so a
|
|
10
|
+
// chatty judge doesn't bloat the trace.
|
|
11
|
+
//
|
|
12
|
+
// Latency: a judge adds one round-trip per node beyond the main LLM call.
|
|
13
|
+
// For a typical 4-node workflow with 2 judges per node, that's 8 extra
|
|
14
|
+
// calls per run. The workflow-level `judge_budget` (default 50) gives
|
|
15
|
+
// authors a load-time warning when the total stretches past expected.
|
|
16
|
+
/**
|
|
17
|
+
* Build the structured judge prompt.
|
|
18
|
+
*
|
|
19
|
+
* The prompt format is fixed so the parser can rely on it. Model authors
|
|
20
|
+
* MUST NOT change the section markers without updating {@link parseJudgeResponse}
|
|
21
|
+
* in lockstep.
|
|
22
|
+
*/
|
|
23
|
+
export function buildJudgePrompt(evaluator, result) {
|
|
24
|
+
const passWhen = evaluator.pass_when ?? "yes";
|
|
25
|
+
const failWhen = passWhen === "yes" ? "no" : `not_${passWhen}`;
|
|
26
|
+
return [
|
|
27
|
+
`You are a judge evaluator. Read the rubric, the node's result data, and`,
|
|
28
|
+
`the tool-call trace, then decide whether the result satisfies the rubric.`,
|
|
29
|
+
``,
|
|
30
|
+
`## Rubric`,
|
|
31
|
+
``,
|
|
32
|
+
evaluator.rubric ?? "(no rubric provided)",
|
|
33
|
+
``,
|
|
34
|
+
`## Node result data`,
|
|
35
|
+
``,
|
|
36
|
+
"```json",
|
|
37
|
+
JSON.stringify(result.data ?? {}, null, 2),
|
|
38
|
+
"```",
|
|
39
|
+
``,
|
|
40
|
+
`## Tool calls`,
|
|
41
|
+
``,
|
|
42
|
+
summarizeToolCalls(result.toolCalls ?? []),
|
|
43
|
+
``,
|
|
44
|
+
`## Output format`,
|
|
45
|
+
``,
|
|
46
|
+
`Respond with EXACTLY two lines, in this order, and nothing else:`,
|
|
47
|
+
``,
|
|
48
|
+
`VERDICT: <${passWhen}|${failWhen}>`,
|
|
49
|
+
`REASONING: <one paragraph, max 3 sentences, explaining the verdict>`,
|
|
50
|
+
``,
|
|
51
|
+
`If the rubric passes vacuously (e.g. a conditional clause does not`,
|
|
52
|
+
`apply to this result), respond with VERDICT: ${passWhen} and a brief`,
|
|
53
|
+
`note in REASONING.`,
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
|
56
|
+
function summarizeToolCalls(calls) {
|
|
57
|
+
if (calls.length === 0)
|
|
58
|
+
return "(no tools were called)";
|
|
59
|
+
return calls
|
|
60
|
+
.map((c) => {
|
|
61
|
+
const status = c.status ?? (looksLikeError(c.output) ? "error" : "success");
|
|
62
|
+
const inputPreview = preview(c.input);
|
|
63
|
+
const outputPreview = preview(c.output);
|
|
64
|
+
return `- ${c.tool} [${status}] input=${inputPreview} output=${outputPreview}`;
|
|
65
|
+
})
|
|
66
|
+
.join("\n");
|
|
67
|
+
}
|
|
68
|
+
function looksLikeError(output) {
|
|
69
|
+
if (!output || typeof output !== "object")
|
|
70
|
+
return false;
|
|
71
|
+
const err = output.error;
|
|
72
|
+
return err !== undefined && err !== null && err !== false;
|
|
73
|
+
}
|
|
74
|
+
/** Compact JSON preview. Caps long values to keep the prompt short. */
|
|
75
|
+
function preview(v) {
|
|
76
|
+
let s;
|
|
77
|
+
try {
|
|
78
|
+
s = JSON.stringify(v);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
s = String(v);
|
|
82
|
+
}
|
|
83
|
+
if (!s)
|
|
84
|
+
return "<empty>";
|
|
85
|
+
return s.length > 200 ? s.slice(0, 200) + "…" : s;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Parse a judge response into `{ verdict, reasoning }`.
|
|
89
|
+
*
|
|
90
|
+
* Format expected:
|
|
91
|
+
* VERDICT: <token>
|
|
92
|
+
* REASONING: <text...>
|
|
93
|
+
*
|
|
94
|
+
* Lines may have any leading whitespace. Reasoning may span multiple lines
|
|
95
|
+
* (everything after the `REASONING:` marker is consumed). Returns null
|
|
96
|
+
* fields when the markers are missing.
|
|
97
|
+
*/
|
|
98
|
+
export function parseJudgeResponse(text) {
|
|
99
|
+
const verdictMatch = /VERDICT\s*:\s*(\S+)/i.exec(text);
|
|
100
|
+
const reasoningMatch = /REASONING\s*:\s*([\s\S]*)$/i.exec(text);
|
|
101
|
+
const verdict = verdictMatch ? verdictMatch[1].trim().toLowerCase() : null;
|
|
102
|
+
const reasoning = reasoningMatch ? reasoningMatch[1].trim() : null;
|
|
103
|
+
return { verdict, reasoning };
|
|
104
|
+
}
|
|
105
|
+
const PARSE_FAILURE_REASONING = "judge parse failure: model response did not contain a parseable VERDICT line";
|
|
106
|
+
/**
|
|
107
|
+
* Run a single judge evaluator against a node result.
|
|
108
|
+
*
|
|
109
|
+
* Resolves a verdict by calling the judge model, parsing VERDICT/REASONING,
|
|
110
|
+
* and comparing the verdict to `pass_when` (default `yes`). On parse
|
|
111
|
+
* failure, retries the call once. If the retry also fails to parse,
|
|
112
|
+
* returns `pass: false` with a "judge parse failure" reasoning instead of
|
|
113
|
+
* throwing, so a single flaky model response doesn't halt the workflow.
|
|
114
|
+
*/
|
|
115
|
+
export async function evaluateJudge(evaluator, result, claude, opts) {
|
|
116
|
+
const passWhen = (evaluator.pass_when ?? "yes").toLowerCase();
|
|
117
|
+
const prompt = buildJudgePrompt(evaluator, result);
|
|
118
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
119
|
+
let response;
|
|
120
|
+
try {
|
|
121
|
+
response = await claude.ask({
|
|
122
|
+
instruction: prompt,
|
|
123
|
+
context: {},
|
|
124
|
+
model: opts.model,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Treat throws as parse failure for retry purposes; the second attempt
|
|
129
|
+
// either succeeds or surfaces the same path through the same channel.
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const parsed = parseJudgeResponse(response);
|
|
133
|
+
if (parsed.verdict === null)
|
|
134
|
+
continue;
|
|
135
|
+
const pass = parsed.verdict === passWhen;
|
|
136
|
+
return {
|
|
137
|
+
pass,
|
|
138
|
+
reasoning: parsed.reasoning ?? undefined,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return { pass: false, reasoning: PARSE_FAILURE_REASONING };
|
|
142
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// ─── Eval: path parser + resolver ────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Shared by `value` evaluators (operate on `result.data`) and the `requires`
|
|
4
|
+
// pre-condition gate (operates on the cross-node context map). The grammar
|
|
5
|
+
// is documented at https://spec.sweny.ai/nodes/#path-grammar.
|
|
6
|
+
const SEGMENT_RE = /^([a-zA-Z_][a-zA-Z0-9_]*)(\[\*\])?$/;
|
|
7
|
+
const PREFIX_RE = /^([a-z]+):/;
|
|
8
|
+
function parsePath(path) {
|
|
9
|
+
if (path.length === 0)
|
|
10
|
+
return "empty path";
|
|
11
|
+
let mode = "all";
|
|
12
|
+
let body = path;
|
|
13
|
+
const prefixMatch = PREFIX_RE.exec(path);
|
|
14
|
+
if (prefixMatch) {
|
|
15
|
+
const prefix = prefixMatch[1];
|
|
16
|
+
if (prefix !== "all" && prefix !== "any") {
|
|
17
|
+
return `unknown prefix '${prefix}' (expected 'all' or 'any')`;
|
|
18
|
+
}
|
|
19
|
+
mode = prefix;
|
|
20
|
+
body = path.slice(prefixMatch[0].length);
|
|
21
|
+
if (body.length === 0)
|
|
22
|
+
return "empty path after prefix";
|
|
23
|
+
}
|
|
24
|
+
const parts = body.split(".");
|
|
25
|
+
const segments = [];
|
|
26
|
+
for (const part of parts) {
|
|
27
|
+
const m = SEGMENT_RE.exec(part);
|
|
28
|
+
if (!m)
|
|
29
|
+
return `malformed segment '${part}'`;
|
|
30
|
+
segments.push({ name: m[1], wildcard: m[2] === "[*]" });
|
|
31
|
+
}
|
|
32
|
+
return { mode, segments };
|
|
33
|
+
}
|
|
34
|
+
export function resolvePath(data, path) {
|
|
35
|
+
const parsed = parsePath(path);
|
|
36
|
+
if (typeof parsed === "string")
|
|
37
|
+
return { ok: false, reason: parsed };
|
|
38
|
+
let current = [data];
|
|
39
|
+
for (const seg of parsed.segments) {
|
|
40
|
+
const next = [];
|
|
41
|
+
for (const v of current) {
|
|
42
|
+
if (v === null)
|
|
43
|
+
return { ok: false, reason: `null encountered before segment '${seg.name}'` };
|
|
44
|
+
if (typeof v !== "object") {
|
|
45
|
+
return { ok: false, reason: `expected object before segment '${seg.name}', got ${typeof v}` };
|
|
46
|
+
}
|
|
47
|
+
const obj = v;
|
|
48
|
+
if (!(seg.name in obj)) {
|
|
49
|
+
return { ok: false, reason: `missing segment '${seg.name}'` };
|
|
50
|
+
}
|
|
51
|
+
const child = obj[seg.name];
|
|
52
|
+
if (seg.wildcard) {
|
|
53
|
+
if (!Array.isArray(child)) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
reason: `expected array at '${seg.name}', got ${child === null ? "null" : typeof child}`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
for (const el of child)
|
|
60
|
+
next.push(el);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
next.push(child);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
current = next;
|
|
67
|
+
if (current.length === 0) {
|
|
68
|
+
return { ok: true, mode: parsed.mode, values: [] };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { ok: true, mode: parsed.mode, values: current };
|
|
72
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EvaluatorRule, OutputMatch } from "../types.js";
|
|
2
|
+
export declare function checkOutputRequired(paths: string[], data: unknown): string | null;
|
|
3
|
+
export declare function checkOutputMatches(matches: OutputMatch[], data: unknown): string | null;
|
|
4
|
+
/**
|
|
5
|
+
* Evaluate a `value` rule against the node's `result.data`.
|
|
6
|
+
*
|
|
7
|
+
* Returns `{ pass: true }` when every declared check passes, otherwise
|
|
8
|
+
* `{ pass: false, reasoning: <semicolon-joined failure list> }`.
|
|
9
|
+
*
|
|
10
|
+
* Function-rule fields (`*_tool_called`) are ignored even if present.
|
|
11
|
+
*/
|
|
12
|
+
export declare function evaluateValueRule(rule: EvaluatorRule, data: unknown): {
|
|
13
|
+
pass: boolean;
|
|
14
|
+
reasoning?: string;
|
|
15
|
+
};
|