@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.
- 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
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
173
|
+
// Retry only triggers on eval failure. Bail on tool/API errors.
|
|
165
174
|
if (result.status !== "success")
|
|
166
175
|
break;
|
|
167
|
-
const
|
|
168
|
-
|
|
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
|
|
186
|
+
// Apply eval failure to the result.
|
|
171
187
|
result.status = "failed";
|
|
172
|
-
result.data = { ...result.data, error:
|
|
188
|
+
result.data = { ...result.data, error: outcome.error };
|
|
173
189
|
if (!retry || attempt >= retry.max) {
|
|
174
|
-
logger.warn(`
|
|
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(`
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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 "./
|
|
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
|
-
|
|
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 +
|
|
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
|
|
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
|
|
6
|
+
// falls back to the default static preamble. Reflection failure never
|
|
7
7
|
// escalates to a workflow failure.
|
|
8
|
-
import { isErrorOutput } from "./
|
|
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
|
|
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 +
|
|
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,
|
|
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${
|
|
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,
|
|
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${
|
|
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(
|
|
46
|
+
return defaultPreamble(failureList);
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
-
|
|
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,
|
|
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
|
-
`
|
|
69
|
+
`Evaluation failed:`,
|
|
70
|
+
failureList,
|
|
56
71
|
"",
|
|
57
72
|
`You called these tools during the failed attempt:`,
|
|
58
73
|
summary,
|