@sweny-ai/core 0.1.123 → 0.1.124
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/agent-env.d.ts +171 -0
- package/dist/agent-env.js +393 -0
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +1 -1
- package/dist/claude.d.ts +61 -3
- package/dist/claude.js +187 -27
- package/dist/cli/check.d.ts +42 -1
- package/dist/cli/check.js +130 -8
- package/dist/cli/cloud-lifecycle.d.ts +24 -29
- package/dist/cli/cloud-lifecycle.js +187 -12
- package/dist/cli/cloud-report.js +45 -2
- package/dist/cli/config-file.d.ts +8 -1
- package/dist/cli/config-file.js +42 -1
- package/dist/cli/config.js +11 -14
- package/dist/cli/e2e.d.ts +22 -0
- package/dist/cli/e2e.js +31 -4
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +138 -46
- package/dist/cli/marketplace.d.ts +3 -1
- package/dist/cli/marketplace.js +2 -2
- package/dist/cli/new.d.ts +15 -0
- package/dist/cli/new.js +139 -65
- package/dist/cli/publish.js +9 -0
- package/dist/cli/run-output.d.ts +59 -0
- package/dist/cli/run-output.js +189 -0
- package/dist/cli/skill-env.d.ts +34 -0
- package/dist/cli/skill-env.js +64 -0
- package/dist/cli/templates.js +587 -0
- package/dist/cli/workflow-input.d.ts +41 -0
- package/dist/cli/workflow-input.js +56 -0
- package/dist/executor.d.ts +17 -1
- package/dist/executor.js +119 -9
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/mermaid.d.ts +6 -0
- package/dist/mermaid.js +10 -5
- package/dist/schema.d.ts +218 -188
- package/dist/schema.js +24 -3
- package/dist/skills/betterstack.js +4 -0
- package/dist/skills/datadog.js +3 -0
- package/dist/skills/github.js +8 -0
- package/dist/skills/linear.js +8 -0
- package/dist/skills/notification.js +3 -0
- package/dist/skills/sentry.js +4 -0
- package/dist/skills/slack.js +2 -0
- package/dist/skills/supabase.js +9 -0
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +9 -3
- package/dist/types.d.ts +99 -2
- package/dist/types.js +17 -0
- package/dist/untrusted.d.ts +32 -0
- package/dist/untrusted.js +46 -0
- package/dist/workflows/browser.js +2 -2
- package/dist/workflows/implement.yml +2 -0
- package/dist/workflows/triage.yml +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small, side-effect-free helpers for building the `workflowInput` bag that
|
|
3
|
+
* `sweny workflow run` hands to `execute()`.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately NOT in main.ts: main.ts ends with a top-level
|
|
6
|
+
* `await program.parseAsync()` (see bottom of file), so importing it at all
|
|
7
|
+
* triggers full CLI argv parsing as a side effect. That makes main.ts unsafe
|
|
8
|
+
* to `import` from a test. Anything worth unit-testing in isolation lives
|
|
9
|
+
* here instead (same pattern as config.ts / credentials.ts / e2e.ts).
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Merge the CLI-level `--dry-run` flag into a `--input`-validated workflow
|
|
13
|
+
* input bag.
|
|
14
|
+
*
|
|
15
|
+
* #324: `sweny workflow run <file> --dry-run --input '{...}'` (what the
|
|
16
|
+
* GitHub Action and MCP server both pass) silently dropped `dryRun` before
|
|
17
|
+
* this fix: the `--input` branch built `workflowInput` from the caller's
|
|
18
|
+
* JSON alone, and only the config-derived (no-`--input`) branch merged in
|
|
19
|
+
* `config.dryRun`. The executor's fail-closed dry-run gate (see
|
|
20
|
+
* `advanceFromNode` in executor.ts) reads `input.dryRun` directly, so a
|
|
21
|
+
* dropped flag meant the full workflow, side effects included, ran anyway.
|
|
22
|
+
*
|
|
23
|
+
* Only forces `dryRun` *on*: when `--dry-run` is set it always wins, but a
|
|
24
|
+
* caller-supplied `dryRun: true` already present in the `--input` JSON must
|
|
25
|
+
* never be downgraded just because `--dry-run` was omitted.
|
|
26
|
+
*/
|
|
27
|
+
export function mergeDryRunIntoInput(validatedInput, cliDryRun) {
|
|
28
|
+
if (!cliDryRun)
|
|
29
|
+
return validatedInput;
|
|
30
|
+
return { ...validatedInput, dryRun: true };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parse `--timeout` / `--max-steps` for `sweny workflow run`. Throws a clear
|
|
34
|
+
* error on non-numeric or out-of-range values instead of silently falling
|
|
35
|
+
* back to a default (which hid typos like `--timeout 15m`).
|
|
36
|
+
*
|
|
37
|
+
* - `--timeout <ms>`: whole-run wall-clock budget in ms. `0` means no
|
|
38
|
+
* wall-clock budget (returned as `0`). Absent returns `defaultTimeoutMs`.
|
|
39
|
+
* - `--max-steps <n>`: positive integer. Absent returns `undefined` (the
|
|
40
|
+
* executor's own default applies).
|
|
41
|
+
*/
|
|
42
|
+
export function parseRunBudgetFlags(timeout, maxSteps, defaultTimeoutMs) {
|
|
43
|
+
const parse = (raw, flag, min) => {
|
|
44
|
+
const t = raw.trim();
|
|
45
|
+
const n = t === "" ? Number.NaN : Number(t);
|
|
46
|
+
if (!Number.isInteger(n) || n < min) {
|
|
47
|
+
throw new Error(`${flag} must be ${min === 0 ? "a non-negative" : "a positive"} integer, got "${raw}"` +
|
|
48
|
+
(flag === "--timeout" ? " (milliseconds; 0 disables the wall-clock budget)" : ""));
|
|
49
|
+
}
|
|
50
|
+
return n;
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
timeoutMs: timeout === undefined ? defaultTimeoutMs : parse(timeout, "--timeout", 0),
|
|
54
|
+
maxSteps: maxSteps === undefined ? undefined : parse(maxSteps, "--max-steps", 1),
|
|
55
|
+
};
|
|
56
|
+
}
|
package/dist/executor.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* This replaces ~8k lines of engine + recipe step code.
|
|
14
14
|
*/
|
|
15
|
-
import type { Workflow, Skill, Claude, Observer, Logger, ExecutionResult } from "./types.js";
|
|
15
|
+
import type { Workflow, Skill, Tool, Claude, Observer, Logger, ExecutionResult } from "./types.js";
|
|
16
16
|
export interface ExecuteOptions {
|
|
17
17
|
/** Registered skills (id → Skill) */
|
|
18
18
|
skills: Map<string, Skill>;
|
|
@@ -80,3 +80,19 @@ export declare const DEFAULT_MAX_STEPS = 200;
|
|
|
80
80
|
* - `trace`: full ordered execution trace including loops and routing decisions
|
|
81
81
|
*/
|
|
82
82
|
export declare function execute(workflow: Workflow, input: unknown, options: ExecuteOptions): Promise<ExecutionResult>;
|
|
83
|
+
/**
|
|
84
|
+
* Thrown when a conditional routing decision could not be made and there is
|
|
85
|
+
* no explicit default (unconditional) edge to fall back to. Fails closed: the
|
|
86
|
+
* executor refuses to guess an edge (the historical fail-open bug), so an LLM
|
|
87
|
+
* outage terminates the run loudly instead of silently taking the first
|
|
88
|
+
* conditional edge.
|
|
89
|
+
*/
|
|
90
|
+
export declare class RouteEvaluationError extends Error {
|
|
91
|
+
readonly node: string;
|
|
92
|
+
constructor(node: string, message: string);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Dry-run tool gate (#380). Only an explicit `access: "read"` passes; a tool
|
|
96
|
+
* with no `access` is treated as a write so new tools fail safe.
|
|
97
|
+
*/
|
|
98
|
+
export declare function isReadTool(tool: Pick<Tool, "access">): boolean;
|
package/dist/executor.js
CHANGED
|
@@ -19,6 +19,8 @@ import { evaluateRequires } from "./requires.js";
|
|
|
19
19
|
import { buildRetryPreamble } from "./retry.js";
|
|
20
20
|
import { resolveExecutionModel } from "./model.js";
|
|
21
21
|
import { buildToolAliases } from "./skills/index.js";
|
|
22
|
+
import { resolveAgentAccess } from "./agent-env.js";
|
|
23
|
+
import { fenceUntrusted } from "./untrusted.js";
|
|
22
24
|
/**
|
|
23
25
|
* Default workflow-level step budget. Counts total node executions across the
|
|
24
26
|
* whole run (loop iterations included). Generous enough for legitimate deep
|
|
@@ -47,6 +49,7 @@ export async function execute(workflow, input, options) {
|
|
|
47
49
|
const { claude, observer, signal, timeoutMs } = options;
|
|
48
50
|
// If the caller already aborted before we started, fail fast.
|
|
49
51
|
throwIfAborted(signal);
|
|
52
|
+
const dryRun = isDryRunInput(input);
|
|
50
53
|
// Merge inline workflow skills into the skill map so they resolve at runtime.
|
|
51
54
|
// Inline skills (instruction/mcp only) become Skill objects with empty tools/config.
|
|
52
55
|
// The caller's skill map takes precedence — inline skills only fill gaps.
|
|
@@ -135,7 +138,15 @@ export async function execute(workflow, input, options) {
|
|
|
135
138
|
// skill-tool filter (`tools.allow` / `tools.deny`). Filtered tools are
|
|
136
139
|
// never registered for the run, so the model cannot see or call them.
|
|
137
140
|
const resolvedSkillTools = resolveTools(node.skills, skills);
|
|
138
|
-
const
|
|
141
|
+
const filteredTools = filterNodeTools(resolvedSkillTools, node.tools, currentId, logger);
|
|
142
|
+
// Dry run (#380): only `access: "read"` tools reach the node. Write and
|
|
143
|
+
// unclassified tools are withheld (never registered, so never callable)
|
|
144
|
+
// and recorded on the node result as `skippedWrites`.
|
|
145
|
+
const tools = dryRun ? filteredTools.filter(isReadTool) : filteredTools;
|
|
146
|
+
const skippedWrites = dryRun ? filteredTools.filter((t) => !isReadTool(t)).map((t) => t.name) : [];
|
|
147
|
+
if (skippedWrites.length > 0) {
|
|
148
|
+
logger.info(` dry run: withheld write tools: ${skippedWrites.join(", ")}`, { node: currentId });
|
|
149
|
+
}
|
|
139
150
|
const skillInstructions = resolveSkillInstructions(node.skills, skills);
|
|
140
151
|
// Runtime guard: if this node declares skills but none resolved, the node
|
|
141
152
|
// cannot do its job (e.g. "create a Linear issue" with no linear skill).
|
|
@@ -176,6 +187,13 @@ export async function execute(workflow, input, options) {
|
|
|
176
187
|
trace.steps.push({ node: currentId, status: result.status, iteration });
|
|
177
188
|
safeObserve(observer, { type: "node:exit", node: currentId, result }, logger);
|
|
178
189
|
logger.warn(` requires ${onFail === "skip" ? "skipped" : "failed"}: ${requiresError}`, { node: currentId });
|
|
190
|
+
// Fail closed here too: a requires failure with on_fail: fail yields a
|
|
191
|
+
// failed node, which halts by default (same policy as a failed run).
|
|
192
|
+
if (result.status === "failed" && (node.on_fail ?? "halt") === "halt") {
|
|
193
|
+
logger.warn(` requires failed; halting workflow (on_fail: halt)`, { node: currentId });
|
|
194
|
+
safeObserve(observer, { type: "route", from: currentId, to: "(end)", reason: "node failed (on_fail: halt)" }, logger);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
179
197
|
// Apply normal routing rules (dry run gate + resolveNext).
|
|
180
198
|
// TODO: dedupe with requires path — see advanceFromNode helper below
|
|
181
199
|
const next = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, { signal, timeoutMs });
|
|
@@ -196,7 +214,8 @@ export async function execute(workflow, input, options) {
|
|
|
196
214
|
// Prepend rules and context to instruction per cascade semantics
|
|
197
215
|
const effectiveRules = assembleCascaded("rules", currentId, workflow.nodes[currentId], inputRules.length, (workflow.rules ?? []).length, resolvedSources);
|
|
198
216
|
const effectiveContext = assembleCascaded("context", currentId, workflow.nodes[currentId], inputContext.length, (workflow.context ?? []).length, resolvedSources);
|
|
199
|
-
const
|
|
217
|
+
const baseInstruction = buildNodeInstruction(resolvedInstruction, effectiveRules, effectiveContext, input, skillInstructions);
|
|
218
|
+
const instruction = dryRun ? `${dryRunNotice(skippedWrites)}\n\n---\n\n${baseInstruction}` : baseInstruction;
|
|
200
219
|
// Run Claude on this node, with optional eval-failure retry loop.
|
|
201
220
|
let attempt = 0;
|
|
202
221
|
let result;
|
|
@@ -221,6 +240,8 @@ export async function execute(workflow, input, options) {
|
|
|
221
240
|
model: nodeModel,
|
|
222
241
|
signal,
|
|
223
242
|
timeoutMs,
|
|
243
|
+
agentAccess: resolveAgentAccess(node.skills, skills),
|
|
244
|
+
...(dryRun ? { readOnly: true } : {}),
|
|
224
245
|
onProgress: (message) => {
|
|
225
246
|
safeObserve(observer, { type: "node:progress", node: currentId, message }, logger);
|
|
226
247
|
},
|
|
@@ -283,6 +304,23 @@ export async function execute(workflow, input, options) {
|
|
|
283
304
|
logger.warn(` eval failed (attempt ${attempt + 1}/${retry.max + 1}): ${outcome.error}`, {
|
|
284
305
|
node: currentId,
|
|
285
306
|
});
|
|
307
|
+
// #325: a retry attempt re-invokes the agent on this node (full model
|
|
308
|
+
// spend) exactly like a fresh node visit, so it counts against the same
|
|
309
|
+
// `stepCount`/`maxSteps` budget. Checked BEFORE buildRetryPreamble so no
|
|
310
|
+
// paid reflection call happens once the budget is exhausted. On
|
|
311
|
+
// exhaustion, record a failed result and emit node:exit so observers
|
|
312
|
+
// never see node:enter without a matching node:exit.
|
|
313
|
+
stepCount++;
|
|
314
|
+
if (stepCount > maxSteps) {
|
|
315
|
+
const budgetError = `step budget exceeded: workflow '${workflow.id}' ran ${stepCount} steps (max_steps: ${maxSteps}) ` +
|
|
316
|
+
`while retrying node '${currentId}' (attempt ${attempt + 1}/${retry.max}). Lower 'retry.max' on the ` +
|
|
317
|
+
`offending node or raise 'max_steps' if the workflow legitimately needs more steps.`;
|
|
318
|
+
result = { ...result, status: "failed", data: { ...result.data, error: budgetError } };
|
|
319
|
+
results.set(currentId, result);
|
|
320
|
+
trace.steps.push({ node: currentId, status: "failed", iteration, retryAttempt: attempt });
|
|
321
|
+
safeObserve(observer, { type: "node:exit", node: currentId, result }, logger);
|
|
322
|
+
throw new Error(budgetError);
|
|
323
|
+
}
|
|
286
324
|
const preamble = await buildRetryPreamble({
|
|
287
325
|
retry,
|
|
288
326
|
evalFailures: outcome.failures,
|
|
@@ -316,12 +354,26 @@ export async function execute(workflow, input, options) {
|
|
|
316
354
|
data: { ...(result.data ?? {}), fail_soft: true },
|
|
317
355
|
};
|
|
318
356
|
}
|
|
357
|
+
if (skippedWrites.length > 0)
|
|
358
|
+
result = { ...result, skippedWrites };
|
|
319
359
|
results.set(currentId, result);
|
|
320
360
|
trace.steps.push(attempt > 0
|
|
321
361
|
? { node: currentId, status: result.status, iteration, retryAttempt: attempt }
|
|
322
362
|
: { node: currentId, status: result.status, iteration });
|
|
323
363
|
safeObserve(observer, { type: "node:exit", node: currentId, result }, logger);
|
|
324
364
|
logger.info(` ✓ ${result.status}`, { node: currentId, toolCalls: result.toolCalls.length });
|
|
365
|
+
// Fail closed on node failure. A node that finishes `failed` (agent-level
|
|
366
|
+
// failure, or an eval failure that exhausted retries and was not softened
|
|
367
|
+
// by `fail_soft`) HALTS the workflow by default. This stops a broken node
|
|
368
|
+
// from advancing down a conditional out-edge (e.g. filing an issue/PR) on
|
|
369
|
+
// the back of a failure. The failed result stays in `results`, so the run
|
|
370
|
+
// surfaces as failed to callers (CLI exit code, cloud status). Authors who
|
|
371
|
+
// want the legacy fall-through opt in per-node with `on_fail: "continue"`.
|
|
372
|
+
if (result.status === "failed" && (node.on_fail ?? "halt") === "halt") {
|
|
373
|
+
logger.warn(` node failed; halting workflow (on_fail: halt)`, { node: currentId });
|
|
374
|
+
safeObserve(observer, { type: "route", from: currentId, to: "(end)", reason: "node failed (on_fail: halt)" }, logger);
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
325
377
|
// Dry run gate + routing — shared with requires path via advanceFromNode helper.
|
|
326
378
|
currentId = await advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, {
|
|
327
379
|
signal,
|
|
@@ -335,6 +387,21 @@ export async function execute(workflow, input, options) {
|
|
|
335
387
|
return { results, trace };
|
|
336
388
|
}
|
|
337
389
|
// ─── Internals ───────────────────────────────────────────────────
|
|
390
|
+
/**
|
|
391
|
+
* Thrown when a conditional routing decision could not be made and there is
|
|
392
|
+
* no explicit default (unconditional) edge to fall back to. Fails closed: the
|
|
393
|
+
* executor refuses to guess an edge (the historical fail-open bug), so an LLM
|
|
394
|
+
* outage terminates the run loudly instead of silently taking the first
|
|
395
|
+
* conditional edge.
|
|
396
|
+
*/
|
|
397
|
+
export class RouteEvaluationError extends Error {
|
|
398
|
+
node;
|
|
399
|
+
constructor(node, message) {
|
|
400
|
+
super(message);
|
|
401
|
+
this.node = node;
|
|
402
|
+
this.name = "RouteEvaluationError";
|
|
403
|
+
}
|
|
404
|
+
}
|
|
338
405
|
/**
|
|
339
406
|
* Build the full instruction for a node.
|
|
340
407
|
*
|
|
@@ -353,7 +420,8 @@ function buildNodeInstruction(baseInstruction, effectiveRules, effectiveContext,
|
|
|
353
420
|
sections.push(`## Rules — You MUST Follow These\n\n${effectiveRules}`);
|
|
354
421
|
}
|
|
355
422
|
if (effectiveContext) {
|
|
356
|
-
|
|
423
|
+
// Context Sources can be fetched pages or runtime input: fence as untrusted (#360).
|
|
424
|
+
sections.push(`## Background Context\n\n${fenceUntrusted(effectiveContext, "background-context")}`);
|
|
357
425
|
}
|
|
358
426
|
// Legacy fallback for `input.additionalContext` when no rules/context cascaded
|
|
359
427
|
if (sections.length === 0) {
|
|
@@ -635,6 +703,24 @@ function mergeInlineSkills(skills, inlineSkills) {
|
|
|
635
703
|
}
|
|
636
704
|
return merged;
|
|
637
705
|
}
|
|
706
|
+
/** True when the run input requests a dry run (`dryRun === true`, strictly). */
|
|
707
|
+
function isDryRunInput(input) {
|
|
708
|
+
return input != null && typeof input === "object" && input.dryRun === true;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Dry-run tool gate (#380). Only an explicit `access: "read"` passes; a tool
|
|
712
|
+
* with no `access` is treated as a write so new tools fail safe.
|
|
713
|
+
*/
|
|
714
|
+
export function isReadTool(tool) {
|
|
715
|
+
return tool.access === "read";
|
|
716
|
+
}
|
|
717
|
+
/** Instruction section prepended to every node under dry-run. */
|
|
718
|
+
function dryRunNotice(skippedWrites) {
|
|
719
|
+
const withheld = skippedWrites.length > 0 ? `These write tools were withheld from this step: ${skippedWrites.join(", ")}. ` : "";
|
|
720
|
+
return (`## Dry run\n\nThis is a dry run. Only read-only tools are available. ${withheld}` +
|
|
721
|
+
`Do not create, modify, post, or send anything. Do the analysis, and where this step would ` +
|
|
722
|
+
`normally write, describe exactly what it would have written instead.`);
|
|
723
|
+
}
|
|
638
724
|
function resolveTools(skillIds, skills) {
|
|
639
725
|
return skillIds
|
|
640
726
|
.map((id) => skills.get(id))
|
|
@@ -745,12 +831,13 @@ function resolveConfig(skills, overrides, env = process.env) {
|
|
|
745
831
|
* Returns the next node ID, or null when execution should stop.
|
|
746
832
|
*/
|
|
747
833
|
async function advanceFromNode(workflow, currentId, results, input, claude, observer, edgeCounts, logger, trace, abort) {
|
|
748
|
-
// Dry run
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
//
|
|
752
|
-
|
|
753
|
-
|
|
834
|
+
// Dry run path gate: stop at the first conditional routing decision.
|
|
835
|
+
// Safety does not depend on this (#380): under dry-run every node already
|
|
836
|
+
// runs read-only (see execute()). The stop keeps the dry-run path a pure
|
|
837
|
+
// function of the graph (no LLM route evaluation, so the same workflow
|
|
838
|
+
// always visits the same nodes) and skips action branches whose output
|
|
839
|
+
// would only describe writes that cannot happen.
|
|
840
|
+
if (isDryRunInput(input)) {
|
|
754
841
|
const outEdges = workflow.edges.filter((e) => e.from === currentId);
|
|
755
842
|
if (outEdges.some((e) => e.when)) {
|
|
756
843
|
safeObserve(observer, { type: "route", from: currentId, to: "(end)", reason: "dry run" }, logger);
|
|
@@ -871,6 +958,29 @@ async function resolveNext(workflow, current, results, input, claude, observer,
|
|
|
871
958
|
signal: abort?.signal,
|
|
872
959
|
timeoutMs: abort?.timeoutMs,
|
|
873
960
|
});
|
|
961
|
+
// Fail closed. `evaluate` returns null when the routing decision could not
|
|
962
|
+
// be made (SDK error, timeout, non-success subtype, or an unparseable
|
|
963
|
+
// answer). We must NOT fall through to a conditional edge: on a node with a
|
|
964
|
+
// single conditional out-edge, "the first choice" IS that edge, so an outage
|
|
965
|
+
// would always take it (the fail-open bug that silently filed real
|
|
966
|
+
// issues/PRs instead of routing to `skip`). Take the author's explicit
|
|
967
|
+
// default/else edge if one exists; otherwise terminate the run loudly.
|
|
968
|
+
if (chosen === null) {
|
|
969
|
+
if (defaultEdge) {
|
|
970
|
+
logger?.warn(` route eval: evaluation failed for node '${current}'; taking default (unconditional) edge '${defaultEdge.to}'.`, { node: current });
|
|
971
|
+
safeObserve(observer, { type: "route", from: current, to: defaultEdge.to, reason: "route evaluation failed; default edge" }, logger);
|
|
972
|
+
if (edgeCounts) {
|
|
973
|
+
const key = `${current}→${defaultEdge.to}`;
|
|
974
|
+
edgeCounts.set(key, (edgeCounts.get(key) ?? 0) + 1);
|
|
975
|
+
}
|
|
976
|
+
return defaultEdge.to;
|
|
977
|
+
}
|
|
978
|
+
logger?.error(` route eval: evaluation failed for node '${current}' and there is no default (unconditional) edge; ` +
|
|
979
|
+
`refusing to fail open. Terminating the run. Add a default edge or fix the model backend.`, { node: current });
|
|
980
|
+
safeObserve(observer, { type: "route", from: current, to: "(end)", reason: "route evaluation failed; no default edge" }, logger);
|
|
981
|
+
throw new RouteEvaluationError(current, `route evaluation failed for node '${current}' and no default (unconditional) edge exists; ` +
|
|
982
|
+
`refusing to fail open`);
|
|
983
|
+
}
|
|
874
984
|
// Validate that Claude returned a valid target.
|
|
875
985
|
const validTargets = new Set(outEdges.map((e) => e.to));
|
|
876
986
|
if (!validTargets.has(chosen)) {
|
package/dist/index.d.ts
CHANGED
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
* })
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
|
-
export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, McpTransport, WorkflowType, SkillHarnessKey, } from "./types.js";
|
|
24
|
+
export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, } from "./types.js";
|
|
25
25
|
export { WORKFLOW_INPUT_TYPES } from "./types.js";
|
|
26
|
-
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";
|
|
26
|
+
export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
|
|
27
27
|
export { consoleLogger } from "./types.js";
|
|
28
|
-
export { execute } from "./executor.js";
|
|
28
|
+
export { execute, RouteEvaluationError } from "./executor.js";
|
|
29
29
|
export type { ExecuteOptions } from "./executor.js";
|
|
30
30
|
export { ClaudeClient, resolveAuthEnv } from "./claude.js";
|
|
31
31
|
export type { ClaudeClientOptions, SwenyAuthMode, ResolveAuthEnvOpts } from "./claude.js";
|
package/dist/index.js
CHANGED
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
*/
|
|
24
24
|
export { WORKFLOW_INPUT_TYPES } from "./types.js";
|
|
25
25
|
// Runtime enum constants + skill-id helpers
|
|
26
|
-
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";
|
|
26
|
+
export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
|
|
27
27
|
export { consoleLogger } from "./types.js";
|
|
28
28
|
// Executor
|
|
29
|
-
export { execute } from "./executor.js";
|
|
29
|
+
export { execute, RouteEvaluationError } from "./executor.js";
|
|
30
30
|
// Claude client
|
|
31
31
|
export { ClaudeClient, resolveAuthEnv } from "./claude.js";
|
|
32
32
|
// Execution model resolution
|
package/dist/mermaid.d.ts
CHANGED
|
@@ -20,6 +20,12 @@ export interface MermaidOptions {
|
|
|
20
20
|
direction?: "TB" | "LR";
|
|
21
21
|
/** Title rendered above the diagram */
|
|
22
22
|
title?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Override the `classDef` style body (the part after the class name) for
|
|
25
|
+
* specific statuses, e.g. `{ success: "fill:#2563eb,stroke:#1d4ed8,color:#fff" }`.
|
|
26
|
+
* Unset statuses keep the defaults.
|
|
27
|
+
*/
|
|
28
|
+
classDefs?: Partial<Record<NodeStatus, string>>;
|
|
23
29
|
}
|
|
24
30
|
/**
|
|
25
31
|
* Convert a Workflow to a Mermaid flowchart diagram string.
|
package/dist/mermaid.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* ```
|
|
25
25
|
*/
|
|
26
26
|
export function toMermaid(workflow, options = {}) {
|
|
27
|
-
const { state = {}, trace, direction = "TB", title } = options;
|
|
27
|
+
const { state = {}, trace, direction = "TB", title, classDefs = {} } = options;
|
|
28
28
|
const lines = [];
|
|
29
29
|
if (title) {
|
|
30
30
|
lines.push("---");
|
|
@@ -131,10 +131,15 @@ export function toMermaid(workflow, options = {}) {
|
|
|
131
131
|
const statusNodes = groupByStatus(state);
|
|
132
132
|
if (statusNodes.size > 0) {
|
|
133
133
|
lines.push("");
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
const defaults = {
|
|
135
|
+
current: "fill:#3b82f6,stroke:#2563eb,color:#fff,stroke-width:2px",
|
|
136
|
+
success: "fill:#22c55e,stroke:#16a34a,color:#fff,stroke-width:2px",
|
|
137
|
+
failed: "fill:#ef4444,stroke:#dc2626,color:#fff,stroke-width:2px",
|
|
138
|
+
skipped: "fill:#6b7280,stroke:#4b5563,color:#fff,stroke-dasharray:5 5",
|
|
139
|
+
};
|
|
140
|
+
for (const status of ["current", "success", "failed", "skipped"]) {
|
|
141
|
+
lines.push(` classDef ${status} ${classDefs[status] ?? defaults[status]}`);
|
|
142
|
+
}
|
|
138
143
|
for (const [status, ids] of statusNodes) {
|
|
139
144
|
const sanitized = ids.map(aliasFor).join(",");
|
|
140
145
|
lines.push(` class ${sanitized} ${status}`);
|