@sweny-ai/core 0.1.103 → 0.1.105
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 +30 -6
- 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/dist/skills/custom-loader.js +12 -6
- 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",
|
|
@@ -631,7 +653,7 @@ function resolveConfig(skills, overrides, env = process.env) {
|
|
|
631
653
|
*
|
|
632
654
|
* Returns the next node ID, or null when execution should stop.
|
|
633
655
|
*/
|
|
634
|
-
async function advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace) {
|
|
656
|
+
async function advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, abort) {
|
|
635
657
|
// Dry run hard gate — stop at the first conditional routing decision.
|
|
636
658
|
// Unconditional edges are analysis flow (prepare→gather→investigate);
|
|
637
659
|
// conditional edges are action decisions (investigate→create_issue/skip).
|
|
@@ -645,7 +667,7 @@ async function advanceFromNode(workflow, currentId, results, input, claude, obse
|
|
|
645
667
|
}
|
|
646
668
|
}
|
|
647
669
|
const prevId = currentId;
|
|
648
|
-
const nextId = await resolveNext(workflow, currentId, results, input, claude, observer, edgeCounts, logger);
|
|
670
|
+
const nextId = await resolveNext(workflow, currentId, results, input, claude, observer, edgeCounts, logger, abort);
|
|
649
671
|
if (nextId) {
|
|
650
672
|
const reason = workflow.edges.find((e) => e.from === prevId && e.to === nextId)?.when ?? "only path";
|
|
651
673
|
trace.edges.push({ from: prevId, to: nextId, reason });
|
|
@@ -661,7 +683,7 @@ async function advanceFromNode(workflow, currentId, results, input, claude, obse
|
|
|
661
683
|
*
|
|
662
684
|
* Edges with max_iterations are filtered out once exhausted.
|
|
663
685
|
*/
|
|
664
|
-
async function resolveNext(workflow, current, results, input, claude, observer, edgeCounts, logger) {
|
|
686
|
+
async function resolveNext(workflow, current, results, input, claude, observer, edgeCounts, logger, abort) {
|
|
665
687
|
// Filter out edges that have exceeded their max_iterations
|
|
666
688
|
const outEdges = workflow.edges.filter((e) => {
|
|
667
689
|
if (e.from !== current)
|
|
@@ -731,6 +753,8 @@ async function resolveNext(workflow, current, results, input, claude, observer,
|
|
|
731
753
|
question: "Based on the results so far, which condition is true?",
|
|
732
754
|
context,
|
|
733
755
|
choices,
|
|
756
|
+
signal: abort?.signal,
|
|
757
|
+
timeoutMs: abort?.timeoutMs,
|
|
734
758
|
});
|
|
735
759
|
// Validate that Claude returned a valid target.
|
|
736
760
|
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) {
|
|
@@ -249,14 +249,20 @@ export function configuredSkillsWithDiagnostics(env = process.env, cwd) {
|
|
|
249
249
|
// Custom skills can override built-in IDs. When overriding, merge:
|
|
250
250
|
// keep the built-in's tools and config, add the custom instruction/mcp.
|
|
251
251
|
const configuredMap = new Map(configured.map((s) => [s.id, s]));
|
|
252
|
+
// Resolve override targets against the FULL built-in set, not the
|
|
253
|
+
// env-filtered `configured` list. Otherwise a custom override of a built-in
|
|
254
|
+
// whose required env var happens to be unset falls through to the
|
|
255
|
+
// "net-new custom skill" branch and silently drops the built-in's tools.
|
|
256
|
+
// The tool surface of a built-in must not hinge on an unrelated env check.
|
|
257
|
+
const builtinById = new Map(builtinSkills.map((s) => [s.id, s]));
|
|
252
258
|
for (const skill of custom) {
|
|
253
|
-
const
|
|
254
|
-
if (
|
|
259
|
+
const base = configuredMap.get(skill.id) ?? builtinById.get(skill.id);
|
|
260
|
+
if (base) {
|
|
255
261
|
configuredMap.set(skill.id, {
|
|
256
|
-
...
|
|
257
|
-
config: { ...
|
|
258
|
-
instruction: skill.instruction ??
|
|
259
|
-
mcp: skill.mcp ??
|
|
262
|
+
...base,
|
|
263
|
+
config: { ...base.config, ...skill.config },
|
|
264
|
+
instruction: skill.instruction ?? base.instruction,
|
|
265
|
+
mcp: skill.mcp ?? base.mcp,
|
|
260
266
|
});
|
|
261
267
|
}
|
|
262
268
|
else {
|