@sweny-ai/core 0.1.104 → 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 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(() => reject(new Error(`${label} timed out after ${ms / 1000}s`)), ms);
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
- lines.push(`description: ${description}`);
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:");
@@ -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(` ${sanitizeId(id)}([${label}${entryTag}${iterTag}])`);
76
+ lines.push(` ${aliasFor(id)}([${label}${entryTag}${iterTag}])`);
52
77
  }
53
78
  else {
54
- lines.push(` ${sanitizeId(id)}[${label}${iterTag}]`);
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 = sanitizeId(edge.from);
74
- const to = sanitizeId(edge.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(sanitizeId).join(",");
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
- return text.replace(/"/g, """).replace(/\[/g, "[").replace(/\]/g, "]");
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.104",
3
+ "version": "0.1.105",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"