@sweny-ai/core 0.1.84 → 0.1.86

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.
@@ -0,0 +1,205 @@
1
+ // ─── Eval: value (data-shape) evaluator ──────────────────────────────
2
+ //
3
+ // Operates on `result.data`. Combines `output_required` and `output_matches`
4
+ // checks. Pure, deterministic, fast.
5
+ import { resolvePath } from "./path.js";
6
+ // ─── Internals ───────────────────────────────────────────────────────
7
+ function deepEqual(a, b) {
8
+ if (Object.is(a, b))
9
+ return true;
10
+ if (typeof a !== typeof b)
11
+ return false;
12
+ if (a === null || b === null)
13
+ return false;
14
+ if (typeof a !== "object")
15
+ return false;
16
+ if (Array.isArray(a)) {
17
+ if (!Array.isArray(b) || a.length !== b.length)
18
+ return false;
19
+ for (let i = 0; i < a.length; i++) {
20
+ if (!deepEqual(a[i], b[i]))
21
+ return false;
22
+ }
23
+ return true;
24
+ }
25
+ if (Array.isArray(b))
26
+ return false;
27
+ const ao = a;
28
+ const bo = b;
29
+ const aKeys = Object.keys(ao);
30
+ const bKeys = Object.keys(bo);
31
+ if (aKeys.length !== bKeys.length)
32
+ return false;
33
+ for (const k of aKeys) {
34
+ if (!Object.prototype.hasOwnProperty.call(bo, k))
35
+ return false;
36
+ if (!deepEqual(ao[k], bo[k]))
37
+ return false;
38
+ }
39
+ return true;
40
+ }
41
+ function formatValue(v) {
42
+ if (v === null)
43
+ return "null";
44
+ if (v === undefined)
45
+ return "undefined";
46
+ return JSON.stringify(v);
47
+ }
48
+ /**
49
+ * Cap on `output_matches[].matches` source length.
50
+ *
51
+ * `matches` is user-supplied through workflow YAML, including marketplace
52
+ * workflows. A pathological pattern like `(a+)+$` causes exponential
53
+ * backtracking in V8's regex engine. Capping the source length is a cheap,
54
+ * useful defense; well-formed regexes are tens of characters at most.
55
+ *
56
+ * Caveat: length alone does NOT prevent all ReDoS. A 12-char pattern like
57
+ * `(a+)+$` is still catastrophic against the right input. Future work:
58
+ * gate compilation through `safe-regex` or use the `re2` engine for
59
+ * marketplace inputs. The cap closes the obvious abuse surface without
60
+ * adding a runtime dep right now.
61
+ */
62
+ const MAX_REGEX_SOURCE_LENGTH = 1000;
63
+ function compileMatch(m) {
64
+ if (m.matches !== undefined) {
65
+ if (m.matches.length > MAX_REGEX_SOURCE_LENGTH) {
66
+ throw new Error(`regex source exceeds ${MAX_REGEX_SOURCE_LENGTH} characters (got ${m.matches.length}); ` +
67
+ `tighten the pattern to avoid catastrophic-backtracking risk`);
68
+ }
69
+ // Throws synchronously if the regex source is invalid; surfaced as a
70
+ // value-evaluator failure rather than a crash because the executor catches.
71
+ return { match: m, regex: new RegExp(m.matches) };
72
+ }
73
+ return { match: m };
74
+ }
75
+ function applyOperator(c, value) {
76
+ const m = c.match;
77
+ if (m.equals !== undefined) {
78
+ if (deepEqual(value, m.equals))
79
+ return { ok: true };
80
+ return { ok: false, reason: `equals ${formatValue(m.equals)}, got ${formatValue(value)}` };
81
+ }
82
+ if (m.in !== undefined) {
83
+ if (m.in.some((x) => deepEqual(value, x)))
84
+ return { ok: true };
85
+ return {
86
+ ok: false,
87
+ reason: `in [${m.in.map(formatValue).join(", ")}], got ${formatValue(value)}`,
88
+ };
89
+ }
90
+ if (c.regex) {
91
+ const s = typeof value === "string" ? value : JSON.stringify(value);
92
+ if (c.regex.test(s))
93
+ return { ok: true };
94
+ return { ok: false, reason: `matches /${m.matches}/, got ${formatValue(value)}` };
95
+ }
96
+ return { ok: false, reason: "no operator declared (zod should have caught this)" };
97
+ }
98
+ function describeOperator(c) {
99
+ const m = c.match;
100
+ if (m.equals !== undefined)
101
+ return `equals ${formatValue(m.equals)}`;
102
+ if (m.in !== undefined)
103
+ return `in [${m.in.map(formatValue).join(", ")}]`;
104
+ if (m.matches !== undefined)
105
+ return `matches /${m.matches}/`;
106
+ return "no operator";
107
+ }
108
+ // ─── Public check helpers ────────────────────────────────────────────
109
+ export function checkOutputRequired(paths, data) {
110
+ const failures = [];
111
+ for (const path of paths) {
112
+ const r = resolvePath(data, path);
113
+ if (!r.ok) {
114
+ failures.push(`'${path}' ${r.reason}`);
115
+ continue;
116
+ }
117
+ if (r.mode === "all") {
118
+ if (r.values.length > 0 && !r.values.every((v) => v !== null && v !== undefined)) {
119
+ failures.push(`'${path}' resolved to null/undefined value(s)`);
120
+ }
121
+ }
122
+ else {
123
+ if (r.values.length === 0) {
124
+ failures.push(`'${path}' (any:) resolved to no elements`);
125
+ }
126
+ else if (!r.values.some((v) => v !== null && v !== undefined)) {
127
+ failures.push(`'${path}' (any:) all resolved values are null/undefined`);
128
+ }
129
+ }
130
+ }
131
+ if (failures.length === 0)
132
+ return null;
133
+ return `output_required: ${failures.join("; ")}`;
134
+ }
135
+ export function checkOutputMatches(matches, data) {
136
+ const failures = [];
137
+ for (const m of matches) {
138
+ let compiled;
139
+ try {
140
+ compiled = compileMatch(m);
141
+ }
142
+ catch (err) {
143
+ failures.push(`'${m.path}' invalid regex /${m.matches}/: ${err.message}`);
144
+ continue;
145
+ }
146
+ const r = resolvePath(data, m.path);
147
+ if (!r.ok) {
148
+ failures.push(`'${m.path}' ${r.reason}`);
149
+ continue;
150
+ }
151
+ if (r.mode === "all") {
152
+ if (r.values.length === 0)
153
+ continue;
154
+ const offenders = [];
155
+ for (const v of r.values) {
156
+ const res = applyOperator(compiled, v);
157
+ if (!res.ok)
158
+ offenders.push(formatValue(v));
159
+ }
160
+ if (offenders.length > 0) {
161
+ const opDescription = describeOperator(compiled);
162
+ failures.push(`'${m.path}' ${opDescription}, got [${offenders.join(", ")}]`);
163
+ }
164
+ }
165
+ else {
166
+ if (r.values.length === 0) {
167
+ failures.push(`'${m.path}' (any:) resolved to no elements`);
168
+ continue;
169
+ }
170
+ const anyOk = r.values.some((v) => applyOperator(compiled, v).ok);
171
+ if (!anyOk) {
172
+ const opDescription = describeOperator(compiled);
173
+ failures.push(`'${m.path}' (any:) no element satisfies ${opDescription}`);
174
+ }
175
+ }
176
+ }
177
+ if (failures.length === 0)
178
+ return null;
179
+ return `output_matches: ${failures.join("; ")}`;
180
+ }
181
+ // ─── Evaluator entry point ───────────────────────────────────────────
182
+ /**
183
+ * Evaluate a `value` rule against the node's `result.data`.
184
+ *
185
+ * Returns `{ pass: true }` when every declared check passes, otherwise
186
+ * `{ pass: false, reasoning: <semicolon-joined failure list> }`.
187
+ *
188
+ * Function-rule fields (`*_tool_called`) are ignored even if present.
189
+ */
190
+ export function evaluateValueRule(rule, data) {
191
+ const failures = [];
192
+ if (rule.output_required && rule.output_required.length > 0) {
193
+ const e = checkOutputRequired(rule.output_required, data);
194
+ if (e)
195
+ failures.push(e);
196
+ }
197
+ if (rule.output_matches && rule.output_matches.length > 0) {
198
+ const e = checkOutputMatches(rule.output_matches, data);
199
+ if (e)
200
+ failures.push(e);
201
+ }
202
+ if (failures.length === 0)
203
+ return { pass: true };
204
+ return { pass: false, reasoning: failures.join("; ") };
205
+ }
package/dist/executor.js CHANGED
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { consoleLogger } from "./types.js";
16
16
  import { resolveSources } from "./source-resolver.js";
17
- import { evaluateVerify } from "./verify.js";
17
+ import { evaluateAll, aggregateEval } from "./eval/index.js";
18
18
  import { evaluateRequires } from "./requires.js";
19
19
  import { buildRetryPreamble } from "./retry.js";
20
20
  import { buildToolAliases } from "./skills/index.js";
@@ -38,10 +38,15 @@ export async function execute(workflow, input, options) {
38
38
  const nodeRunCounts = new Map(); // node → times executed
39
39
  const trace = { steps: [], edges: [], sources: {} };
40
40
  validate(workflow, skills);
41
- // Build a verify-time alias table from the loaded skills. Each skill owns
41
+ // Build an eval-time alias table from the loaded skills. Each skill owns
42
42
  // its own mapping between skill-tool names and equivalent MCP names. Core
43
- // stays vendor-neutral — this call just unions what the skills declare.
43
+ // stays vendor-neutral; this call just unions what the skills declare.
44
44
  const toolAliases = buildToolAliases(skills.values(), logger);
45
+ // Soft-cap warning: count declared judge evaluators across all nodes and
46
+ // warn if the total exceeds workflow.judge_budget (default 50). Spec says
47
+ // this is informational, not a hard runtime cap; users hit the warning
48
+ // when they unintentionally fan out judges (e.g. 10 judges × 50 nodes).
49
+ warnOnJudgeBudget(workflow, logger);
45
50
  // ── Source resolution phase ─────────────────────────────────────
46
51
  // Resolve all Source values (node instructions + rules/context at every
47
52
  // level) into plain text before the main execution loop. This eagerly
@@ -98,10 +103,14 @@ export async function execute(workflow, input, options) {
98
103
  throw new Error(`Node "${currentId}" requires skills [${node.skills.join(", ")}] but none are configured. ` +
99
104
  `Set the required environment variables and try again.`);
100
105
  }
101
- // Build context: input + all prior node results
106
+ // Build context: input + all prior node results.
107
+ // Each prior node entry is the node's `data` augmented with `evals`
108
+ // (a Record<name, EvalResult> for downstream lookup like
109
+ // `priorNode.evals.tests_run_clean.pass`). When `data` already has an
110
+ // `evals` key, the data field wins (back-compat with existing workflows).
102
111
  const context = {
103
112
  input,
104
- ...Object.fromEntries([...results.entries()].map(([k, v]) => [k, v.data])),
113
+ ...Object.fromEntries([...results.entries()].map(([k, v]) => [k, buildPriorNodeContext(v)])),
105
114
  };
106
115
  // Pre-condition gate: evaluate `requires` against the cross-node context
107
116
  // BEFORE invoking the LLM. Failure either marks the node failed (on_fail
@@ -145,7 +154,7 @@ export async function execute(workflow, input, options) {
145
154
  const effectiveRules = assembleCascaded("rules", currentId, workflow.nodes[currentId], inputRules.length, (workflow.rules ?? []).length, resolvedSources);
146
155
  const effectiveContext = assembleCascaded("context", currentId, workflow.nodes[currentId], inputContext.length, (workflow.context ?? []).length, resolvedSources);
147
156
  const instruction = buildNodeInstruction(resolvedInstruction, effectiveRules, effectiveContext, input, skillInstructions);
148
- // Run Claude on this node, with optional verify-failure retry loop.
157
+ // Run Claude on this node, with optional eval-failure retry loop.
149
158
  let attempt = 0;
150
159
  let result;
151
160
  let currentInstruction = instruction;
@@ -161,25 +170,34 @@ export async function execute(workflow, input, options) {
161
170
  safeObserve(observer, { type: "node:progress", node: currentId, message }, logger);
162
171
  },
163
172
  });
164
- // Retry only triggers on verify failure — bail on tool/API errors.
173
+ // Retry only triggers on eval failure. Bail on tool/API errors.
165
174
  if (result.status !== "success")
166
175
  break;
167
- const verifyError = evaluateVerify(node.verify, result, toolAliases);
168
- if (!verifyError)
176
+ const evalResults = await evaluateAll(node.eval, result, {
177
+ aliases: toolAliases,
178
+ claude,
179
+ node,
180
+ workflow,
181
+ });
182
+ result.evals = evalResults;
183
+ const outcome = aggregateEval(evalResults, node.eval_policy ?? "all_pass");
184
+ if (outcome.pass)
169
185
  break;
170
- // Apply verify failure to the result.
186
+ // Apply eval failure to the result.
171
187
  result.status = "failed";
172
- result.data = { ...result.data, error: verifyError };
188
+ result.data = { ...result.data, error: outcome.error };
173
189
  if (!retry || attempt >= retry.max) {
174
- logger.warn(` verify failed: ${verifyError}`, { node: currentId });
190
+ logger.warn(` eval failed: ${outcome.error}`, { node: currentId });
175
191
  break;
176
192
  }
177
193
  // Build retry preamble + record attempt in trace.
178
194
  trace.steps.push({ node: currentId, status: "failed", iteration, retryAttempt: attempt });
179
- logger.warn(` verify failed (attempt ${attempt + 1}/${retry.max + 1}): ${verifyError}`, { node: currentId });
195
+ logger.warn(` eval failed (attempt ${attempt + 1}/${retry.max + 1}): ${outcome.error}`, {
196
+ node: currentId,
197
+ });
180
198
  const preamble = await buildRetryPreamble({
181
199
  retry,
182
- verifyError,
200
+ evalFailures: outcome.failures,
183
201
  toolCalls: result.toolCalls,
184
202
  nodeInstruction: resolvedInstruction,
185
203
  claude,
@@ -187,7 +205,7 @@ export async function execute(workflow, input, options) {
187
205
  context,
188
206
  });
189
207
  currentInstruction = `${preamble}\n\n---\n\n${instruction}`;
190
- safeObserve(observer, { type: "node:retry", node: currentId, attempt: attempt + 1, reason: verifyError, preamble }, logger);
208
+ safeObserve(observer, { type: "node:retry", node: currentId, attempt: attempt + 1, reason: outcome.error ?? "eval failed", preamble }, logger);
191
209
  attempt++;
192
210
  }
193
211
  results.set(currentId, result);
@@ -249,6 +267,54 @@ function buildNodeInstruction(baseInstruction, effectiveRules, effectiveContext,
249
267
  * - Object form → use its `only` and `sources`
250
268
  * - undefined → empty additive
251
269
  */
270
+ const DEFAULT_JUDGE_BUDGET = 50;
271
+ /**
272
+ * Soft cap on judge calls per workflow run. Logs a warn when the count of
273
+ * declared `kind: judge` evaluators exceeds the configured budget. This is
274
+ * a load-time signal only; the executor does not refuse to run.
275
+ *
276
+ * Each judge counts once per node visit, but the executor counts judges
277
+ * once at declaration time (a fan-out of repeated visits via cycles is the
278
+ * author's responsibility, not core's).
279
+ */
280
+ function warnOnJudgeBudget(workflow, logger) {
281
+ const budget = workflow.judge_budget ?? DEFAULT_JUDGE_BUDGET;
282
+ let total = 0;
283
+ for (const node of Object.values(workflow.nodes)) {
284
+ for (const evaluator of node.eval ?? []) {
285
+ if (evaluator.kind === "judge")
286
+ total++;
287
+ }
288
+ }
289
+ if (total > budget) {
290
+ logger.warn(`Workflow declares ${total} judge evaluators across all nodes (budget: ${budget}). ` +
291
+ `Consider reducing the count or raising 'judge_budget' on the workflow. ` +
292
+ `Each judge adds one model call per node attempt.`, { judgeCount: total, judgeBudget: budget });
293
+ }
294
+ }
295
+ /**
296
+ * Build the context-map entry for a single prior node.
297
+ *
298
+ * Spec contract (https://spec.sweny.ai/nodes/#evalresult-type): downstream
299
+ * nodes can read `priorNode.evals.<name>.pass` via the natural lookup path.
300
+ * To honor this without breaking back-compat with workflows that read
301
+ * `priorNode.<dataField>` directly, we spread `data` first and then attach
302
+ * an `evals` namespace keyed by evaluator name. If the agent's structured
303
+ * output happens to include a literal `evals` field, that field wins (and
304
+ * the lookup degrades to "evals not available" for downstream readers).
305
+ */
306
+ function buildPriorNodeContext(result) {
307
+ const data = (result.data ?? {});
308
+ const evals = result.evals ?? [];
309
+ if (evals.length === 0)
310
+ return data;
311
+ const evalsByName = {};
312
+ for (const e of evals) {
313
+ evalsByName[e.name] = e;
314
+ }
315
+ // Spread evals first so a data-side `evals` key shadows it (back-compat).
316
+ return { evals: evalsByName, ...data };
317
+ }
252
318
  function nodeSourcesToArray(ns) {
253
319
  if (!ns)
254
320
  return { sources: [], only: false };
@@ -443,11 +509,12 @@ async function resolveNext(workflow, current, results, input, claude, observer,
443
509
  // Check for a default (unconditional) edge among conditionals
444
510
  const defaultEdge = outEdges.find((e) => !e.when);
445
511
  const conditionalEdges = outEdges.filter((e) => e.when);
446
- // Claude evaluates which condition matches — include input so conditions
447
- // can reference workflow-level flags like dryRun
512
+ // Claude evaluates which condition matches. Include input so conditions
513
+ // can reference workflow-level flags like dryRun, and expose evals so
514
+ // routing edges can read `priorNode.evals.X.pass`.
448
515
  const context = {
449
516
  input,
450
- ...Object.fromEntries([...results.entries()].map(([k, v]) => [k, v.data])),
517
+ ...Object.fromEntries([...results.entries()].map(([k, v]) => [k, buildPriorNodeContext(v)])),
451
518
  };
452
519
  const choices = conditionalEdges.map((e) => ({
453
520
  id: e.to,
package/dist/requires.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // ─── Requires: pre-condition gate ───────────────────────────────────
2
2
  //
3
3
  // Evaluates `node.requires` BEFORE the LLM runs. Same path grammar and
4
- // resolver as verify — the only differences are the data root (cross-node
4
+ // resolver as eval. The only differences are the data root (cross-node
5
5
  // context map instead of result.data) and the error prefix.
6
- import { checkOutputRequired, checkOutputMatches } from "./verify.js";
6
+ import { checkOutputRequired, checkOutputMatches } from "./eval/value.js";
7
7
  /**
8
8
  * Evaluate a node's `requires` block against the cross-node context map.
9
9
  * Returns null when all checks pass (or when `requires` is undefined),
package/dist/retry.d.ts CHANGED
@@ -1,7 +1,12 @@
1
- import type { Claude, NodeRetry, ToolCall, Logger } from "./types.js";
1
+ import type { Claude, EvalResult, NodeRetry, ToolCall, Logger } from "./types.js";
2
2
  export interface BuildRetryPreambleOptions {
3
3
  retry: NodeRetry;
4
- verifyError: string;
4
+ /**
5
+ * Failing evaluator results from the previous attempt. Used both for the
6
+ * structured failure list shown to the agent and as input to autonomous
7
+ * reflection.
8
+ */
9
+ evalFailures: EvalResult[];
5
10
  toolCalls: ToolCall[];
6
11
  nodeInstruction: string;
7
12
  claude: Claude;
@@ -12,7 +17,7 @@ export interface BuildRetryPreambleOptions {
12
17
  * Build the retry preamble for a single retry attempt.
13
18
  *
14
19
  * Resolution order:
15
- * 1. retry.instruction is a string → static preamble + verify error
20
+ * 1. retry.instruction is a string → static preamble + structured failure list
16
21
  * 2. retry.instruction is { auto: true } → claude.ask(default prompt)
17
22
  * 3. retry.instruction is { reflect: "..." } → claude.ask(author prompt)
18
23
  * 4. Otherwise (omitted) → default preamble
@@ -21,3 +26,12 @@ export interface BuildRetryPreambleOptions {
21
26
  * default static preamble and logs a warning.
22
27
  */
23
28
  export declare function buildRetryPreamble(opts: BuildRetryPreambleOptions): Promise<string>;
29
+ /**
30
+ * Render a list of failing evaluator results as a structured bullet list
31
+ * suitable for prepending to a retry instruction.
32
+ *
33
+ * Each entry takes the form `- name (kind): reasoning`. The list is the
34
+ * same shape regardless of whether the originating policy is `all_pass`,
35
+ * `any_pass`, or `weighted`.
36
+ */
37
+ export declare function formatEvalFailures(failures: EvalResult[]): string;
package/dist/retry.js CHANGED
@@ -1,18 +1,18 @@
1
- // ─── Retry: preamble construction for verify-failure self-healing ──
1
+ // ─── Retry: preamble construction for eval-failure self-healing ──────
2
2
  //
3
3
  // Builds the preamble that gets prepended to the node instruction on retry.
4
4
  // In autonomous modes ({ auto: true } or { reflect: ... }), this calls
5
5
  // claude.ask to generate a diagnosis. If reflection fails or returns empty,
6
- // falls back to the default static preamble — reflection failure never
6
+ // falls back to the default static preamble. Reflection failure never
7
7
  // escalates to a workflow failure.
8
- import { isErrorOutput } from "./verify.js";
8
+ import { isErrorOutput } from "./eval/function.js";
9
9
  const DEFAULT_REFLECTION_PROMPT = "Briefly diagnose the failure and state your strategy for the retry. Keep your response to 2-4 sentences.";
10
- const DEFAULT_PREAMBLE_HEADER = "## Previous attempt failed verification";
10
+ const DEFAULT_PREAMBLE_HEADER = "## Previous attempt failed evaluation";
11
11
  /**
12
12
  * Build the retry preamble for a single retry attempt.
13
13
  *
14
14
  * Resolution order:
15
- * 1. retry.instruction is a string → static preamble + verify error
15
+ * 1. retry.instruction is a string → static preamble + structured failure list
16
16
  * 2. retry.instruction is { auto: true } → claude.ask(default prompt)
17
17
  * 3. retry.instruction is { reflect: "..." } → claude.ask(author prompt)
18
18
  * 4. Otherwise (omitted) → default preamble
@@ -21,19 +21,20 @@ const DEFAULT_PREAMBLE_HEADER = "## Previous attempt failed verification";
21
21
  * default static preamble and logs a warning.
22
22
  */
23
23
  export async function buildRetryPreamble(opts) {
24
- const { retry, verifyError, toolCalls, nodeInstruction, claude, logger, context } = opts;
24
+ const { retry, evalFailures, toolCalls, nodeInstruction, claude, logger, context } = opts;
25
+ const failureList = formatEvalFailures(evalFailures);
25
26
  const inst = retry.instruction;
26
27
  if (typeof inst === "string") {
27
- return `## Retry guidance\n\n${inst}\n\n${DEFAULT_PREAMBLE_HEADER}\n\n${verifyError}`;
28
+ return `## Retry guidance\n\n${inst}\n\n${DEFAULT_PREAMBLE_HEADER}\n\n${failureList}`;
28
29
  }
29
30
  if (inst && typeof inst === "object") {
30
31
  const reflectPrompt = "reflect" in inst && typeof inst.reflect === "string" ? inst.reflect : DEFAULT_REFLECTION_PROMPT;
31
- const askInstruction = buildReflectionPrompt(reflectPrompt, nodeInstruction, verifyError, toolCalls);
32
+ const askInstruction = buildReflectionPrompt(reflectPrompt, nodeInstruction, failureList, toolCalls);
32
33
  try {
33
34
  const diagnosis = await claude.ask({ instruction: askInstruction, context });
34
35
  const trimmed = diagnosis.trim();
35
36
  if (trimmed.length > 0) {
36
- return `## Reflection on previous attempt\n\n${trimmed}\n\n${DEFAULT_PREAMBLE_HEADER}\n\n${verifyError}`;
37
+ return `## Reflection on previous attempt\n\n${trimmed}\n\n${DEFAULT_PREAMBLE_HEADER}\n\n${failureList}`;
37
38
  }
38
39
  logger.warn("Retry reflection returned empty response; falling back to default preamble.");
39
40
  }
@@ -42,17 +43,31 @@ export async function buildRetryPreamble(opts) {
42
43
  logger.warn(`Retry reflection threw: ${msg}; falling back to default preamble.`);
43
44
  }
44
45
  }
45
- return defaultPreamble(verifyError);
46
+ return defaultPreamble(failureList);
46
47
  }
47
- function defaultPreamble(verifyError) {
48
- return `${DEFAULT_PREAMBLE_HEADER}\n\n${verifyError}\n\nFix the issue and try again.`;
48
+ /**
49
+ * Render a list of failing evaluator results as a structured bullet list
50
+ * suitable for prepending to a retry instruction.
51
+ *
52
+ * Each entry takes the form `- name (kind): reasoning`. The list is the
53
+ * same shape regardless of whether the originating policy is `all_pass`,
54
+ * `any_pass`, or `weighted`.
55
+ */
56
+ export function formatEvalFailures(failures) {
57
+ if (failures.length === 0)
58
+ return "(no failing evaluators reported)";
59
+ return failures.map((f) => ` - ${f.name} (${f.kind}): ${f.reasoning ?? "no reasoning"}`).join("\n");
60
+ }
61
+ function defaultPreamble(failureList) {
62
+ return `${DEFAULT_PREAMBLE_HEADER}\n\n${failureList}\n\nFix the issue and try again.`;
49
63
  }
50
- function buildReflectionPrompt(reflectPrompt, nodeInstruction, verifyError, toolCalls) {
64
+ function buildReflectionPrompt(reflectPrompt, nodeInstruction, failureList, toolCalls) {
51
65
  const summary = summarizeToolCalls(toolCalls);
52
66
  return [
53
67
  `You attempted to: ${nodeInstruction}`,
54
68
  "",
55
- `Verification failed with: ${verifyError}`,
69
+ `Evaluation failed:`,
70
+ failureList,
56
71
  "",
57
72
  `You called these tools during the failed attempt:`,
58
73
  summary,