@sweny-ai/core 0.1.104 → 0.1.106
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/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/cli/e2e.js +10 -3
- package/dist/cli/skill.js +6 -1
- package/dist/executor.d.ts +14 -0
- package/dist/executor.js +67 -11
- package/dist/mermaid.js +35 -6
- package/dist/retry.d.ts +4 -0
- package/dist/retry.js +2 -2
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +24 -0
- package/package.json +1 -1
package/dist/browser.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
export type { Skill, SkillCategory, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, McpTransport, WorkflowType, SkillHarnessKey, } from "./types.js";
|
|
17
17
|
export { consoleLogger } from "./types.js";
|
|
18
18
|
export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
|
|
19
|
-
export { github, linear, slack, sentry, datadog, notification, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
|
|
19
|
+
export { github, linear, slack, sentry, datadog, betterstack, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
|
|
20
20
|
export type { SkillValidationResult } from "./skills/index.js";
|
|
21
21
|
export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema, skillJsonSchema, } from "./schema.js";
|
|
22
22
|
export type { WorkflowError } from "./schema.js";
|
package/dist/browser.js
CHANGED
|
@@ -20,7 +20,7 @@ export { consoleLogger } from "./types.js";
|
|
|
20
20
|
// skill id, needs these without hardcoding the members.
|
|
21
21
|
export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
|
|
22
22
|
// Skills (browser-safe — no filesystem access, no `process.env` reads)
|
|
23
|
-
export { github, linear, slack, sentry, datadog, notification, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
|
|
23
|
+
export { github, linear, slack, sentry, datadog, betterstack, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
|
|
24
24
|
// Schema & validation
|
|
25
25
|
export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema, skillJsonSchema, } from "./schema.js";
|
|
26
26
|
// Studio adapter
|
package/dist/cli/e2e.js
CHANGED
|
@@ -760,9 +760,15 @@ export async function runE2eInit(options = {}) {
|
|
|
760
760
|
}
|
|
761
761
|
}
|
|
762
762
|
// ── Timeout helper ─────────────────────────────────────────────────────
|
|
763
|
-
function withTimeout(promise, ms, label) {
|
|
763
|
+
function withTimeout(promise, ms, label, onTimeout) {
|
|
764
764
|
return new Promise((resolve, reject) => {
|
|
765
|
-
const timer = setTimeout(() =>
|
|
765
|
+
const timer = setTimeout(() => {
|
|
766
|
+
// Abort the underlying work first so it stops advancing and the in-flight
|
|
767
|
+
// model query is interrupted, then reject the wrapper. Without the abort
|
|
768
|
+
// the execute() promise is abandoned but its model query keeps running.
|
|
769
|
+
onTimeout?.();
|
|
770
|
+
reject(new Error(`${label} timed out after ${ms / 1000}s`));
|
|
771
|
+
}, ms);
|
|
766
772
|
promise
|
|
767
773
|
.then(resolve)
|
|
768
774
|
.catch(reject)
|
|
@@ -905,8 +911,9 @@ export async function runE2eRun(options) {
|
|
|
905
911
|
}
|
|
906
912
|
}
|
|
907
913
|
};
|
|
914
|
+
const controller = new AbortController();
|
|
908
915
|
try {
|
|
909
|
-
const { results } = await withTimeout(execute(workflow, { run_id: vars.run_id, base_url: vars.base_url }, { skills, claude, observer, logger: consoleLogger }), timeoutMs, `Workflow ${workflow.name}
|
|
916
|
+
const { results } = await withTimeout(execute(workflow, { run_id: vars.run_id, base_url: vars.base_url }, { skills, claude, observer, logger: consoleLogger, signal: controller.signal }), timeoutMs, `Workflow ${workflow.name}`, () => controller.abort());
|
|
910
917
|
// Extract report
|
|
911
918
|
const report = results.get("report");
|
|
912
919
|
if (report?.data) {
|
package/dist/cli/skill.js
CHANGED
|
@@ -29,7 +29,12 @@ export function renderSkillTemplate(opts) {
|
|
|
29
29
|
const lines = [];
|
|
30
30
|
lines.push("---");
|
|
31
31
|
lines.push(`name: ${id}`);
|
|
32
|
-
|
|
32
|
+
// JSON-quote the user-supplied description so YAML-special chars (`:`, a
|
|
33
|
+
// leading `[`/`{`/`@`/`>`/`|`, `#`, embedded quotes) round-trip cleanly
|
|
34
|
+
// through the loader's frontmatter parser. A JSON double-quoted string is a
|
|
35
|
+
// valid YAML double-quoted scalar. `id`/`category` are constrained elsewhere
|
|
36
|
+
// (isValidSkillId / SKILL_CATEGORIES) so they stay bare.
|
|
37
|
+
lines.push(`description: ${JSON.stringify(description)}`);
|
|
33
38
|
lines.push(`category: ${category}`);
|
|
34
39
|
lines.push("# config:");
|
|
35
40
|
lines.push("# EXAMPLE_ENV_VAR:");
|
package/dist/executor.d.ts
CHANGED
|
@@ -41,6 +41,20 @@ export interface ExecuteOptions {
|
|
|
41
41
|
* Per-edge `max_iterations` behavior is unchanged.
|
|
42
42
|
*/
|
|
43
43
|
max_steps?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Caller-supplied abort signal. When it aborts, `execute()` rejects promptly
|
|
46
|
+
* and issues no further node / eval / route model calls. The signal is also
|
|
47
|
+
* threaded into every `claude.run` / `claude.evaluate` / `claude.ask` call so
|
|
48
|
+
* the underlying query is interrupted rather than left running detached.
|
|
49
|
+
* Default: no signal (back-compat).
|
|
50
|
+
*/
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
/**
|
|
53
|
+
* Per-model-call wall-clock budget in ms, forwarded to every `claude.run` /
|
|
54
|
+
* `claude.evaluate` / `claude.ask` call. This is a per-call timeout, not a
|
|
55
|
+
* whole-run budget. Default: no timeout (back-compat).
|
|
56
|
+
*/
|
|
57
|
+
timeoutMs?: number;
|
|
44
58
|
}
|
|
45
59
|
/**
|
|
46
60
|
* Default workflow-level step budget. Counts total node executions across the
|
package/dist/executor.js
CHANGED
|
@@ -26,6 +26,16 @@ import { buildToolAliases } from "./skills/index.js";
|
|
|
26
26
|
* before it burns meaningful model spend.
|
|
27
27
|
*/
|
|
28
28
|
export const DEFAULT_MAX_STEPS = 200;
|
|
29
|
+
/**
|
|
30
|
+
* Throw a clear "workflow aborted" error if the caller's signal has aborted.
|
|
31
|
+
* Used at the top of the run loop and before issuing any further model call so
|
|
32
|
+
* an aborted run stops promptly instead of advancing to the next node.
|
|
33
|
+
*/
|
|
34
|
+
function throwIfAborted(signal) {
|
|
35
|
+
if (signal?.aborted) {
|
|
36
|
+
throw new Error("workflow aborted");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
29
39
|
/**
|
|
30
40
|
* Execute a workflow from entry to completion.
|
|
31
41
|
*
|
|
@@ -34,7 +44,9 @@ export const DEFAULT_MAX_STEPS = 200;
|
|
|
34
44
|
* - `trace`: full ordered execution trace including loops and routing decisions
|
|
35
45
|
*/
|
|
36
46
|
export async function execute(workflow, input, options) {
|
|
37
|
-
const { claude, observer } = options;
|
|
47
|
+
const { claude, observer, signal, timeoutMs } = options;
|
|
48
|
+
// If the caller already aborted before we started, fail fast.
|
|
49
|
+
throwIfAborted(signal);
|
|
38
50
|
// Merge inline workflow skills into the skill map so they resolve at runtime.
|
|
39
51
|
// Inline skills (instruction/mcp only) become Skill objects with empty tools/config.
|
|
40
52
|
// The caller's skill map takes precedence — inline skills only fill gaps.
|
|
@@ -93,6 +105,9 @@ export async function execute(workflow, input, options) {
|
|
|
93
105
|
safeObserve(observer, { type: "sources:resolved", sources: resolvedSources }, logger);
|
|
94
106
|
let currentId = workflow.entry;
|
|
95
107
|
while (currentId) {
|
|
108
|
+
// Abort before issuing the next node/eval/route model call. Aborting the
|
|
109
|
+
// signal mid-run makes execute() reject promptly rather than advancing.
|
|
110
|
+
throwIfAborted(signal);
|
|
96
111
|
const node = workflow.nodes[currentId];
|
|
97
112
|
if (!node)
|
|
98
113
|
throw new Error(`Unknown node: "${currentId}"`);
|
|
@@ -156,7 +171,7 @@ export async function execute(workflow, input, options) {
|
|
|
156
171
|
logger.warn(` requires ${onFail === "skip" ? "skipped" : "failed"}: ${requiresError}`, { node: currentId });
|
|
157
172
|
// Apply normal routing rules (dry run gate + resolveNext).
|
|
158
173
|
// TODO: dedupe with requires path — see advanceFromNode helper below
|
|
159
|
-
const next = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace);
|
|
174
|
+
const next = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, { signal, timeoutMs });
|
|
160
175
|
currentId = next;
|
|
161
176
|
continue;
|
|
162
177
|
}
|
|
@@ -192,6 +207,8 @@ export async function execute(workflow, input, options) {
|
|
|
192
207
|
maxTurns: node.max_turns,
|
|
193
208
|
disallowedTools: node.disallowed_tools,
|
|
194
209
|
model: nodeModel,
|
|
210
|
+
signal,
|
|
211
|
+
timeoutMs,
|
|
195
212
|
onProgress: (message) => {
|
|
196
213
|
safeObserve(observer, { type: "node:progress", node: currentId, message }, logger);
|
|
197
214
|
},
|
|
@@ -261,6 +278,8 @@ export async function execute(workflow, input, options) {
|
|
|
261
278
|
logger,
|
|
262
279
|
context,
|
|
263
280
|
model: nodeModel,
|
|
281
|
+
signal,
|
|
282
|
+
timeoutMs,
|
|
264
283
|
});
|
|
265
284
|
currentInstruction = `${preamble}\n\n---\n\n${instruction}`;
|
|
266
285
|
safeObserve(observer, { type: "node:retry", node: currentId, attempt: attempt + 1, reason: outcome.error ?? "eval failed", preamble }, logger);
|
|
@@ -273,7 +292,10 @@ export async function execute(workflow, input, options) {
|
|
|
273
292
|
safeObserve(observer, { type: "node:exit", node: currentId, result }, logger);
|
|
274
293
|
logger.info(` ✓ ${result.status}`, { node: currentId, toolCalls: result.toolCalls.length });
|
|
275
294
|
// Dry run gate + routing — shared with requires path via advanceFromNode helper.
|
|
276
|
-
currentId = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace
|
|
295
|
+
currentId = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, {
|
|
296
|
+
signal,
|
|
297
|
+
timeoutMs,
|
|
298
|
+
});
|
|
277
299
|
}
|
|
278
300
|
safeObserve(observer, {
|
|
279
301
|
type: "workflow:end",
|
|
@@ -603,18 +625,50 @@ function resolveSkillInstructions(skillIds, skills) {
|
|
|
603
625
|
* `options.env ?? process.env` so an explicit env map is honored consistently
|
|
604
626
|
* with Source resolution (which reads the same map). Defaults to
|
|
605
627
|
* `process.env` when omitted.
|
|
628
|
+
*
|
|
629
|
+
* Presence vs. truthiness: an explicitly-provided empty string is treated as
|
|
630
|
+
* present, not absent. Resolution rules:
|
|
631
|
+
* - An override that is present (any string, including "") wins; the env
|
|
632
|
+
* var is not consulted. An explicit "" override does NOT silently fall
|
|
633
|
+
* through to env.
|
|
634
|
+
* - Otherwise the env var (when declared) is used.
|
|
635
|
+
* - A value that is `undefined` (no override, no env var) is absent: a
|
|
636
|
+
* required field reports "not provided".
|
|
637
|
+
* - A value that is "" is present-but-empty: a required field reports a
|
|
638
|
+
* distinct "set to empty" message (you set it, but to an unusable value),
|
|
639
|
+
* while an optional field passes the empty string through.
|
|
606
640
|
*/
|
|
607
641
|
function resolveConfig(skills, overrides, env = process.env) {
|
|
608
642
|
const config = {};
|
|
609
643
|
const missing = [];
|
|
610
644
|
for (const skill of skills.values()) {
|
|
611
645
|
for (const [key, field] of Object.entries(skill.config)) {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
646
|
+
// Explicit override wins, including an empty string — `in` distinguishes
|
|
647
|
+
// a present "" from an absent key, so an override never falls through to
|
|
648
|
+
// env in a surprising way.
|
|
649
|
+
const hasOverride = overrides != null && key in overrides;
|
|
650
|
+
const value = hasOverride ? overrides[key] : field.env ? env[field.env] : undefined;
|
|
651
|
+
const source = field.env ? ` (env: ${field.env})` : "";
|
|
652
|
+
const label = `${skill.id}.${key}${source || " (env: none)"}`;
|
|
653
|
+
if (value === undefined) {
|
|
654
|
+
// Truly absent: no override and no env value.
|
|
655
|
+
if (field.required) {
|
|
656
|
+
missing.push(`${label}: not provided`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
else if (value === "") {
|
|
660
|
+
// Present but empty. An empty credential is unusable for a required
|
|
661
|
+
// field; surface a distinct message so the user can tell "set to empty"
|
|
662
|
+
// from "forgot it". Optional empty values pass through unchanged.
|
|
663
|
+
if (field.required) {
|
|
664
|
+
missing.push(`${label}: set to empty`);
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
config[key] = value;
|
|
668
|
+
}
|
|
615
669
|
}
|
|
616
|
-
else
|
|
617
|
-
|
|
670
|
+
else {
|
|
671
|
+
config[key] = value;
|
|
618
672
|
}
|
|
619
673
|
}
|
|
620
674
|
}
|
|
@@ -631,7 +685,7 @@ function resolveConfig(skills, overrides, env = process.env) {
|
|
|
631
685
|
*
|
|
632
686
|
* Returns the next node ID, or null when execution should stop.
|
|
633
687
|
*/
|
|
634
|
-
async function advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace) {
|
|
688
|
+
async function advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, abort) {
|
|
635
689
|
// Dry run hard gate — stop at the first conditional routing decision.
|
|
636
690
|
// Unconditional edges are analysis flow (prepare→gather→investigate);
|
|
637
691
|
// conditional edges are action decisions (investigate→create_issue/skip).
|
|
@@ -645,7 +699,7 @@ async function advanceFromNode(workflow, currentId, results, input, claude, obse
|
|
|
645
699
|
}
|
|
646
700
|
}
|
|
647
701
|
const prevId = currentId;
|
|
648
|
-
const nextId = await resolveNext(workflow, currentId, results, input, claude, observer, edgeCounts, logger);
|
|
702
|
+
const nextId = await resolveNext(workflow, currentId, results, input, claude, observer, edgeCounts, logger, abort);
|
|
649
703
|
if (nextId) {
|
|
650
704
|
const reason = workflow.edges.find((e) => e.from === prevId && e.to === nextId)?.when ?? "only path";
|
|
651
705
|
trace.edges.push({ from: prevId, to: nextId, reason });
|
|
@@ -661,7 +715,7 @@ async function advanceFromNode(workflow, currentId, results, input, claude, obse
|
|
|
661
715
|
*
|
|
662
716
|
* Edges with max_iterations are filtered out once exhausted.
|
|
663
717
|
*/
|
|
664
|
-
async function resolveNext(workflow, current, results, input, claude, observer, edgeCounts, logger) {
|
|
718
|
+
async function resolveNext(workflow, current, results, input, claude, observer, edgeCounts, logger, abort) {
|
|
665
719
|
// Filter out edges that have exceeded their max_iterations
|
|
666
720
|
const outEdges = workflow.edges.filter((e) => {
|
|
667
721
|
if (e.from !== current)
|
|
@@ -731,6 +785,8 @@ async function resolveNext(workflow, current, results, input, claude, observer,
|
|
|
731
785
|
question: "Based on the results so far, which condition is true?",
|
|
732
786
|
context,
|
|
733
787
|
choices,
|
|
788
|
+
signal: abort?.signal,
|
|
789
|
+
timeoutMs: abort?.timeoutMs,
|
|
734
790
|
});
|
|
735
791
|
// Validate that Claude returned a valid target.
|
|
736
792
|
const validTargets = new Set(outEdges.map((e) => e.to));
|
package/dist/mermaid.js
CHANGED
|
@@ -32,6 +32,31 @@ export function toMermaid(workflow, options = {}) {
|
|
|
32
32
|
lines.push("---");
|
|
33
33
|
}
|
|
34
34
|
lines.push(`graph ${direction}`);
|
|
35
|
+
// Build an injective real-ID → Mermaid-alias map. `sanitizeId` is lossy
|
|
36
|
+
// (every non-alphanumeric char collapses to `_`), so distinct node IDs like
|
|
37
|
+
// `a.b` and `a_b` would otherwise sanitize to the same alias and render as a
|
|
38
|
+
// single merged node, silently corrupting the diagram. Resolve every alias
|
|
39
|
+
// through this map so node declarations, edge endpoints, and class styling
|
|
40
|
+
// all stay consistent and collision-free.
|
|
41
|
+
const idAlias = new Map();
|
|
42
|
+
const usedAliases = new Set();
|
|
43
|
+
const aliasFor = (id) => {
|
|
44
|
+
const cached = idAlias.get(id);
|
|
45
|
+
if (cached)
|
|
46
|
+
return cached;
|
|
47
|
+
const base = sanitizeId(id);
|
|
48
|
+
let alias = base;
|
|
49
|
+
let n = 2;
|
|
50
|
+
while (usedAliases.has(alias))
|
|
51
|
+
alias = `${base}_${n++}`;
|
|
52
|
+
usedAliases.add(alias);
|
|
53
|
+
idAlias.set(id, alias);
|
|
54
|
+
return alias;
|
|
55
|
+
};
|
|
56
|
+
// Seed aliases in node-declaration order so already-alphanumeric IDs keep
|
|
57
|
+
// their byte-identical alias (and stable suffixing for any collisions).
|
|
58
|
+
for (const id of Object.keys(workflow.nodes))
|
|
59
|
+
aliasFor(id);
|
|
35
60
|
// Build node iteration counts from trace
|
|
36
61
|
const nodeIterations = new Map();
|
|
37
62
|
if (trace) {
|
|
@@ -48,10 +73,10 @@ export function toMermaid(workflow, options = {}) {
|
|
|
48
73
|
const iterations = nodeIterations.get(id);
|
|
49
74
|
const iterTag = iterations && iterations > 1 ? ` ×${iterations}` : "";
|
|
50
75
|
if (isEntry) {
|
|
51
|
-
lines.push(` ${
|
|
76
|
+
lines.push(` ${aliasFor(id)}([${label}${entryTag}${iterTag}])`);
|
|
52
77
|
}
|
|
53
78
|
else {
|
|
54
|
-
lines.push(` ${
|
|
79
|
+
lines.push(` ${aliasFor(id)}[${label}${iterTag}]`);
|
|
55
80
|
}
|
|
56
81
|
}
|
|
57
82
|
lines.push("");
|
|
@@ -70,8 +95,8 @@ export function toMermaid(workflow, options = {}) {
|
|
|
70
95
|
const notTakenIndices = [];
|
|
71
96
|
let edgeIndex = 0;
|
|
72
97
|
for (const edge of workflow.edges) {
|
|
73
|
-
const from =
|
|
74
|
-
const to =
|
|
98
|
+
const from = aliasFor(edge.from);
|
|
99
|
+
const to = aliasFor(edge.to);
|
|
75
100
|
const key = `${edge.from}→${edge.to}`;
|
|
76
101
|
const taken = takenEdges.has(key);
|
|
77
102
|
const takenCount = edgeTakenCounts.get(key) ?? 0;
|
|
@@ -111,7 +136,7 @@ export function toMermaid(workflow, options = {}) {
|
|
|
111
136
|
lines.push(" classDef failed fill:#ef4444,stroke:#dc2626,color:#fff,stroke-width:2px");
|
|
112
137
|
lines.push(" classDef skipped fill:#6b7280,stroke:#4b5563,color:#fff,stroke-dasharray:5 5");
|
|
113
138
|
for (const [status, ids] of statusNodes) {
|
|
114
|
-
const sanitized = ids.map(
|
|
139
|
+
const sanitized = ids.map(aliasFor).join(",");
|
|
115
140
|
lines.push(` class ${sanitized} ${status}`);
|
|
116
141
|
}
|
|
117
142
|
}
|
|
@@ -141,7 +166,11 @@ function sanitizeId(id) {
|
|
|
141
166
|
}
|
|
142
167
|
/** Escape characters that break Mermaid labels */
|
|
143
168
|
function escapeLabel(text) {
|
|
144
|
-
|
|
169
|
+
// `|` must be escaped too: edge labels are emitted as `-->|"label"|`, so a
|
|
170
|
+
// literal `|` inside the label (e.g. an edge `when` of "high | critical")
|
|
171
|
+
// terminates Mermaid's edge-label token and corrupts the edge. `|`
|
|
172
|
+
// renders as a literal `|`, consistent with the existing `[`/`]`.
|
|
173
|
+
return text.replace(/"/g, """).replace(/\[/g, "[").replace(/\]/g, "]").replace(/\|/g, "|");
|
|
145
174
|
}
|
|
146
175
|
/** Group node IDs by their status */
|
|
147
176
|
function groupByStatus(state) {
|
package/dist/retry.d.ts
CHANGED
|
@@ -17,6 +17,10 @@ export interface BuildRetryPreambleOptions {
|
|
|
17
17
|
* runs on the node's model. When undefined, the client default applies.
|
|
18
18
|
*/
|
|
19
19
|
model?: string;
|
|
20
|
+
/** Caller-supplied abort signal, forwarded to the reflection `claude.ask`. */
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
/** Per-call wall-clock budget in ms, forwarded to the reflection `claude.ask`. */
|
|
23
|
+
timeoutMs?: number;
|
|
20
24
|
}
|
|
21
25
|
/**
|
|
22
26
|
* Build the retry preamble for a single retry attempt.
|
package/dist/retry.js
CHANGED
|
@@ -21,7 +21,7 @@ const DEFAULT_PREAMBLE_HEADER = "## Previous attempt failed evaluation";
|
|
|
21
21
|
* default static preamble and logs a warning.
|
|
22
22
|
*/
|
|
23
23
|
export async function buildRetryPreamble(opts) {
|
|
24
|
-
const { retry, evalFailures, toolCalls, nodeInstruction, claude, logger, context, model } = opts;
|
|
24
|
+
const { retry, evalFailures, toolCalls, nodeInstruction, claude, logger, context, model, signal, timeoutMs } = opts;
|
|
25
25
|
const failureList = formatEvalFailures(evalFailures);
|
|
26
26
|
const inst = retry.instruction;
|
|
27
27
|
if (typeof inst === "string") {
|
|
@@ -31,7 +31,7 @@ export async function buildRetryPreamble(opts) {
|
|
|
31
31
|
const reflectPrompt = "reflect" in inst && typeof inst.reflect === "string" ? inst.reflect : DEFAULT_REFLECTION_PROMPT;
|
|
32
32
|
const askInstruction = buildReflectionPrompt(reflectPrompt, nodeInstruction, failureList, toolCalls);
|
|
33
33
|
try {
|
|
34
|
-
const diagnosis = await claude.ask({ instruction: askInstruction, context, model });
|
|
34
|
+
const diagnosis = await claude.ask({ instruction: askInstruction, context, model, signal, timeoutMs });
|
|
35
35
|
const trimmed = diagnosis.trim();
|
|
36
36
|
if (trimmed.length > 0) {
|
|
37
37
|
return `## Reflection on previous attempt\n\n${trimmed}\n\n${DEFAULT_PREAMBLE_HEADER}\n\n${failureList}`;
|
package/dist/schema.d.ts
CHANGED
|
@@ -2623,7 +2623,7 @@ export declare function parseWorkflow(raw: unknown): {
|
|
|
2623
2623
|
}> | undefined;
|
|
2624
2624
|
};
|
|
2625
2625
|
export interface WorkflowError {
|
|
2626
|
-
code: "MISSING_ENTRY" | "UNKNOWN_EDGE_SOURCE" | "UNKNOWN_EDGE_TARGET" | "UNREACHABLE_NODE" | "UNKNOWN_SKILL" | "SELF_LOOP" | "UNBOUNDED_CYCLE" | "INVALID_INLINE_SKILL";
|
|
2626
|
+
code: "MISSING_ENTRY" | "UNKNOWN_EDGE_SOURCE" | "UNKNOWN_EDGE_TARGET" | "UNREACHABLE_NODE" | "UNKNOWN_SKILL" | "SELF_LOOP" | "UNBOUNDED_CYCLE" | "UNSUPPORTED_EVAL_POLICY" | "INVALID_INLINE_SKILL";
|
|
2627
2627
|
message: string;
|
|
2628
2628
|
nodeId?: string;
|
|
2629
2629
|
}
|
package/dist/schema.js
CHANGED
|
@@ -102,6 +102,13 @@ export const outputMatchZ = z
|
|
|
102
102
|
}, { message: "output_matches entry must declare exactly one of: equals, in, matches" });
|
|
103
103
|
export const evaluatorKindZ = z.enum(EVALUATOR_KINDS);
|
|
104
104
|
export const evalPolicyZ = z.enum(EVAL_POLICIES);
|
|
105
|
+
/**
|
|
106
|
+
* Eval policies actually implemented at runtime. The Zod schema and published
|
|
107
|
+
* JSON schema accept the full {@link EVAL_POLICIES} vocabulary (the others are
|
|
108
|
+
* reserved), but `validateWorkflow` rejects any policy not in this set so a
|
|
109
|
+
* workflow fails at load time rather than throwing mid-run from `aggregateEval`.
|
|
110
|
+
*/
|
|
111
|
+
const SUPPORTED_EVAL_POLICIES = new Set(["all_pass"]);
|
|
105
112
|
export const evaluatorRuleZ = z
|
|
106
113
|
.object({
|
|
107
114
|
any_tool_called: z.array(z.string().min(1)).min(1).optional(),
|
|
@@ -400,6 +407,23 @@ export function validateWorkflow(workflow, knownSkills) {
|
|
|
400
407
|
}
|
|
401
408
|
}
|
|
402
409
|
}
|
|
410
|
+
// Reject eval policies that are reserved in the vocabulary but not yet
|
|
411
|
+
// implemented. The Zod schema accepts the whole EVAL_POLICIES enum (the
|
|
412
|
+
// published JSON schema advertises them as reserved), but only `all_pass`
|
|
413
|
+
// is implemented at runtime. Without this check a workflow declaring
|
|
414
|
+
// `eval_policy: "any_pass"` or `"weighted"` parses and validates cleanly,
|
|
415
|
+
// then `aggregateEval` throws uncaught mid-run — a validate-then-crash
|
|
416
|
+
// failure mode. Reject it at load time instead. The runtime throw in
|
|
417
|
+
// aggregateEval stays as a defense-in-depth backstop.
|
|
418
|
+
for (const [nodeId, node] of Object.entries(workflow.nodes)) {
|
|
419
|
+
if (node.eval_policy != null && !SUPPORTED_EVAL_POLICIES.has(node.eval_policy)) {
|
|
420
|
+
errors.push({
|
|
421
|
+
code: "UNSUPPORTED_EVAL_POLICY",
|
|
422
|
+
message: `Node "${nodeId}" declares eval_policy "${node.eval_policy}", which is reserved but not yet implemented; use "all_pass" (the only supported policy in v1.0)`,
|
|
423
|
+
nodeId,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
403
427
|
// Inline skill definitions must have instruction or mcp
|
|
404
428
|
for (const [skillId, def] of Object.entries(workflow.skills ?? {})) {
|
|
405
429
|
if (!def.instruction && !def.mcp) {
|