@sweny-ai/core 0.1.27 → 0.1.28

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.
@@ -66,7 +66,7 @@ describe("executor", () => {
66
66
  },
67
67
  });
68
68
  const events = [];
69
- const results = await execute(linearWorkflow, { alert: "test" }, {
69
+ const { results } = await execute(linearWorkflow, { alert: "test" }, {
70
70
  skills: createSkillMap([fileSkill]),
71
71
  claude,
72
72
  observer: (e) => events.push(e),
@@ -92,7 +92,7 @@ describe("executor", () => {
92
92
  },
93
93
  routes: { check: "high" },
94
94
  });
95
- const results = await execute(branchingWorkflow, { alert: "cpu" }, {
95
+ const { results } = await execute(branchingWorkflow, { alert: "cpu" }, {
96
96
  skills: createSkillMap([]),
97
97
  claude,
98
98
  config: {},
@@ -111,7 +111,7 @@ describe("executor", () => {
111
111
  },
112
112
  routes: { check: "low" },
113
113
  });
114
- const results = await execute(branchingWorkflow, { alert: "minor" }, {
114
+ const { results } = await execute(branchingWorkflow, { alert: "minor" }, {
115
115
  skills: createSkillMap([]),
116
116
  claude,
117
117
  config: {},
@@ -154,7 +154,7 @@ describe("executor", () => {
154
154
  c: { data: {} },
155
155
  },
156
156
  });
157
- const results = await execute(linearWorkflow, {}, {
157
+ const { results } = await execute(linearWorkflow, {}, {
158
158
  skills: createSkillMap([]),
159
159
  claude,
160
160
  observer: () => {
@@ -193,7 +193,7 @@ describe("executor", () => {
193
193
  const claude = new MockClaude({
194
194
  responses: { only: { data: { result: "done" } } },
195
195
  });
196
- const results = await execute(singleNode, {}, {
196
+ const { results } = await execute(singleNode, {}, {
197
197
  skills: createSkillMap([]),
198
198
  claude,
199
199
  config: {},
@@ -252,7 +252,7 @@ describe("executor", () => {
252
252
  claude,
253
253
  })).rejects.toThrow("Missing required config");
254
254
  // Should succeed with override
255
- const results = await execute(linearWorkflow, {}, {
255
+ const { results } = await execute(linearWorkflow, {}, {
256
256
  skills: createSkillMap([skillWithConfig]),
257
257
  claude,
258
258
  config: { MY_KEY: "provided" },
@@ -267,7 +267,7 @@ describe("executor", () => {
267
267
  c: { data: {} },
268
268
  },
269
269
  });
270
- const results = await execute(linearWorkflow, {}, {
270
+ const { results } = await execute(linearWorkflow, {}, {
271
271
  skills: createSkillMap([]),
272
272
  claude,
273
273
  config: {},
@@ -316,13 +316,20 @@ describe("executor", () => {
316
316
  return opts.choices[0].id;
317
317
  },
318
318
  };
319
- const results = await execute(retryWorkflow, {}, { skills: createSkillMap([]), claude: mockClaude, config: {} });
319
+ const { results, trace } = await execute(retryWorkflow, {}, { skills: createSkillMap([]), claude: mockClaude, config: {} });
320
320
  // extract ran twice (initial + 1 retry), judge ran twice, done ran once
321
321
  expect(extractCount).toBe(2);
322
322
  expect(judgeCount).toBe(2);
323
323
  expect(results.has("done")).toBe(true);
324
324
  // The second extract result overwrites the first
325
325
  expect(results.get("extract")?.data.attempt).toBe(2);
326
+ // Trace captures the full execution path including loops
327
+ expect(trace.steps).toHaveLength(5); // extract, judge, extract(retry), judge, done
328
+ expect(trace.steps[0]).toEqual({ node: "extract", status: "success", iteration: 1 });
329
+ expect(trace.steps[2]).toEqual({ node: "extract", status: "success", iteration: 2 });
330
+ // Trace edges include the retry loop
331
+ const retryEdge = trace.edges.find((e) => e.from === "judge" && e.to === "extract");
332
+ expect(retryEdge).toBeDefined();
326
333
  });
327
334
  it("respects max_iterations=1 as single retry", async () => {
328
335
  const singleRetry = {
@@ -356,7 +363,7 @@ describe("executor", () => {
356
363
  return opts.choices[0].id;
357
364
  },
358
365
  };
359
- const results = await execute(singleRetry, {}, { skills: createSkillMap([]), claude: mockClaude, config: {} });
366
+ const { results } = await execute(singleRetry, {}, { skills: createSkillMap([]), claude: mockClaude, config: {} });
360
367
  // A runs twice (once initially, once via retry), then edge is exhausted → falls to "end"
361
368
  expect(aCount).toBe(2);
362
369
  expect(results.has("end")).toBe(true);
@@ -371,7 +378,7 @@ describe("executor", () => {
371
378
  return "nonexistent_node"; // invalid target
372
379
  },
373
380
  };
374
- const results = await execute(branchingWorkflow, {}, {
381
+ const { results } = await execute(branchingWorkflow, {}, {
375
382
  skills: createSkillMap([]),
376
383
  claude: badRouteClaude,
377
384
  config: {},
@@ -47,7 +47,7 @@ describe.runIf(available.claude)("e2e workflow with real Claude", () => {
47
47
  maxTurns: 3,
48
48
  });
49
49
  const events = [];
50
- const results = await execute(analysisWorkflow, {
50
+ const { results } = await execute(analysisWorkflow, {
51
51
  alert: "CPU usage at 98% for 15 minutes on production server web-03",
52
52
  service: "api-gateway",
53
53
  timestamp: "2025-03-25T08:00:00Z",
package/dist/cli/main.js CHANGED
@@ -358,7 +358,7 @@ triageCmd.action(async (options) => {
358
358
  contextUrls,
359
359
  };
360
360
  try {
361
- const results = await execute(triageWorkflow, workflowInput, {
361
+ const { results } = await execute(triageWorkflow, workflowInput, {
362
362
  skills,
363
363
  claude,
364
364
  observer,
@@ -474,7 +474,7 @@ implementCmd.action(async (issueId, options) => {
474
474
  contextUrls,
475
475
  };
476
476
  try {
477
- const results = await execute(implementWorkflow, workflowInput, {
477
+ const { results } = await execute(implementWorkflow, workflowInput, {
478
478
  skills,
479
479
  claude,
480
480
  observer,
@@ -624,7 +624,7 @@ export async function workflowRunAction(file, options) {
624
624
  };
625
625
  }
626
626
  try {
627
- const results = await execute(workflow, workflowInput, {
627
+ const { results, trace } = await execute(workflow, workflowInput, {
628
628
  skills,
629
629
  claude,
630
630
  observer,
@@ -642,7 +642,7 @@ export async function workflowRunAction(file, options) {
642
642
  for (const [nodeId, result] of results) {
643
643
  state[nodeId] = result.status === "success" ? "success" : result.status === "failed" ? "failed" : "skipped";
644
644
  }
645
- process.stdout.write(toMermaidBlock(workflow, { state, title: workflow.name }) + "\n");
645
+ process.stdout.write(toMermaidBlock(workflow, { state, trace, title: workflow.name }) + "\n");
646
646
  }
647
647
  const hasFailed = [...results.values()].some((r) => r.status === "failed");
648
648
  if (hasFailed) {
@@ -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, NodeResult, Logger } from "./types.js";
15
+ import type { Workflow, Skill, Claude, Observer, Logger, ExecutionResult } from "./types.js";
16
16
  export interface ExecuteOptions {
17
17
  /** Registered skills (id → Skill) */
18
18
  skills: Map<string, Skill>;
@@ -28,6 +28,8 @@ export interface ExecuteOptions {
28
28
  /**
29
29
  * Execute a workflow from entry to completion.
30
30
  *
31
- * Returns a map of node ID → result for every node that ran.
31
+ * Returns an ExecutionResult with:
32
+ * - `results`: map of node ID → final result (last execution if retried)
33
+ * - `trace`: full ordered execution trace including loops and routing decisions
32
34
  */
33
- export declare function execute(workflow: Workflow, input: unknown, options: ExecuteOptions): Promise<Map<string, NodeResult>>;
35
+ export declare function execute(workflow: Workflow, input: unknown, options: ExecuteOptions): Promise<ExecutionResult>;
package/dist/executor.js CHANGED
@@ -16,7 +16,9 @@ import { consoleLogger } from "./types.js";
16
16
  /**
17
17
  * Execute a workflow from entry to completion.
18
18
  *
19
- * Returns a map of node ID → result for every node that ran.
19
+ * Returns an ExecutionResult with:
20
+ * - `results`: map of node ID → final result (last execution if retried)
21
+ * - `trace`: full ordered execution trace including loops and routing decisions
20
22
  */
21
23
  export async function execute(workflow, input, options) {
22
24
  const { skills, claude, observer } = options;
@@ -24,6 +26,8 @@ export async function execute(workflow, input, options) {
24
26
  const logger = options.logger ?? consoleLogger;
25
27
  const results = new Map();
26
28
  const edgeCounts = new Map(); // "from→to" → times followed
29
+ const nodeRunCounts = new Map(); // node → times executed
30
+ const trace = { steps: [], edges: [] };
27
31
  validate(workflow, skills);
28
32
  safeObserve(observer, { type: "workflow:start", workflow: workflow.id }, logger);
29
33
  let currentId = workflow.entry;
@@ -31,6 +35,8 @@ export async function execute(workflow, input, options) {
31
35
  const node = workflow.nodes[currentId];
32
36
  if (!node)
33
37
  throw new Error(`Unknown node: "${currentId}"`);
38
+ const iteration = (nodeRunCounts.get(currentId) ?? 0) + 1;
39
+ nodeRunCounts.set(currentId, iteration);
34
40
  safeObserve(observer, { type: "node:enter", node: currentId, instruction: node.instruction }, logger);
35
41
  logger.info(`→ ${node.name}`, { node: currentId });
36
42
  // Gather tools from the node's skills
@@ -64,6 +70,7 @@ export async function execute(workflow, input, options) {
64
70
  },
65
71
  });
66
72
  results.set(currentId, result);
73
+ trace.steps.push({ node: currentId, status: result.status, iteration });
67
74
  safeObserve(observer, { type: "node:exit", node: currentId, result }, logger);
68
75
  logger.info(` ✓ ${result.status}`, { node: currentId, toolCalls: result.toolCalls.length });
69
76
  // Dry run hard gate — stop at the first conditional routing decision.
@@ -80,13 +87,20 @@ export async function execute(workflow, input, options) {
80
87
  }
81
88
  }
82
89
  // Resolve next node via edge conditions
90
+ const prevId = currentId;
83
91
  currentId = await resolveNext(workflow, currentId, results, input, claude, observer, edgeCounts, logger);
92
+ // Record the routing decision in the trace
93
+ if (currentId) {
94
+ const edgeKey = `${prevId}→${currentId}`;
95
+ const reason = workflow.edges.find((e) => e.from === prevId && e.to === currentId)?.when ?? "only path";
96
+ trace.edges.push({ from: prevId, to: currentId, reason });
97
+ }
84
98
  }
85
99
  safeObserve(observer, {
86
100
  type: "workflow:end",
87
101
  results: Object.fromEntries(results),
88
102
  }, logger);
89
- return results;
103
+ return { results, trace };
90
104
  }
91
105
  // ─── Internals ───────────────────────────────────────────────────
92
106
  /**
@@ -119,7 +133,12 @@ function buildNodeInstruction(baseInstruction, input) {
119
133
  return baseInstruction;
120
134
  return `${sections.join("\n\n")}\n\n---\n\n${baseInstruction}`;
121
135
  }
122
- /** Call observer without letting exceptions crash the workflow */
136
+ /**
137
+ * Call observer without letting exceptions crash the workflow.
138
+ *
139
+ * Accepts a typed ExecutionEvent so TypeScript catches mistakes in
140
+ * event construction at compile time rather than silently at runtime.
141
+ */
123
142
  function safeObserve(observer, event, logger) {
124
143
  if (!observer)
125
144
  return;
@@ -167,7 +167,7 @@ describe("executor", () => {
167
167
  },
168
168
  });
169
169
  const events = [];
170
- const results = await execute(simpleWorkflow, { alert: "test" }, {
170
+ const { results } = await execute(simpleWorkflow, { alert: "test" }, {
171
171
  skills,
172
172
  claude,
173
173
  observer: (e) => events.push(e),
@@ -202,7 +202,7 @@ describe("executor", () => {
202
202
  check: "handle_high", // Route to high severity handler
203
203
  },
204
204
  });
205
- const results = await execute(branchingWorkflow, { alert: "cpu spike" }, {
205
+ const { results } = await execute(branchingWorkflow, { alert: "cpu spike" }, {
206
206
  skills,
207
207
  claude,
208
208
  config: {},
@@ -224,7 +224,7 @@ describe("executor", () => {
224
224
  check: "handle_low",
225
225
  },
226
226
  });
227
- const results = await execute(branchingWorkflow, { alert: "minor" }, {
227
+ const { results } = await execute(branchingWorkflow, { alert: "minor" }, {
228
228
  skills,
229
229
  claude,
230
230
  config: {},
package/dist/index.d.ts CHANGED
@@ -21,7 +21,7 @@
21
21
  * })
22
22
  * ```
23
23
  */
24
- export type { Skill, SkillCategory, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, } from "./types.js";
24
+ export type { Skill, SkillCategory, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, } from "./types.js";
25
25
  export { consoleLogger } from "./types.js";
26
26
  export { execute } from "./executor.js";
27
27
  export type { ExecuteOptions } from "./executor.js";
package/dist/mermaid.d.ts CHANGED
@@ -6,13 +6,16 @@
6
6
  * VS Code preview, documentation sites, and anywhere Mermaid is supported.
7
7
  *
8
8
  * Accepts optional execution state to color nodes by status
9
- * (current/success/failed/skipped).
9
+ * (current/success/failed/skipped) and optional execution trace
10
+ * to highlight which edges were taken and show loop iterations.
10
11
  */
11
- import type { Workflow } from "./types.js";
12
+ import type { Workflow, ExecutionTrace } from "./types.js";
12
13
  export type NodeStatus = "current" | "success" | "failed" | "skipped";
13
14
  export interface MermaidOptions {
14
15
  /** Execution state — nodes not in this map render as default (pending) */
15
16
  state?: Record<string, NodeStatus>;
17
+ /** Execution trace — highlights taken/not-taken edges and loop counts */
18
+ trace?: ExecutionTrace;
16
19
  /** Graph direction: TB (top-bottom) or LR (left-right). Default: TB */
17
20
  direction?: "TB" | "LR";
18
21
  /** Title rendered above the diagram */
@@ -27,6 +30,7 @@ export interface MermaidOptions {
27
30
  *
28
31
  * const md = toMermaid(workflow, {
29
32
  * state: { triage: 'success', investigate: 'current' },
33
+ * trace: executionResult.trace,
30
34
  * });
31
35
  * // Wrap in ```mermaid fence for GitHub rendering
32
36
  * ```
package/dist/mermaid.js CHANGED
@@ -6,7 +6,8 @@
6
6
  * VS Code preview, documentation sites, and anywhere Mermaid is supported.
7
7
  *
8
8
  * Accepts optional execution state to color nodes by status
9
- * (current/success/failed/skipped).
9
+ * (current/success/failed/skipped) and optional execution trace
10
+ * to highlight which edges were taken and show loop iterations.
10
11
  */
11
12
  /**
12
13
  * Convert a Workflow to a Mermaid flowchart diagram string.
@@ -17,12 +18,13 @@
17
18
  *
18
19
  * const md = toMermaid(workflow, {
19
20
  * state: { triage: 'success', investigate: 'current' },
21
+ * trace: executionResult.trace,
20
22
  * });
21
23
  * // Wrap in ```mermaid fence for GitHub rendering
22
24
  * ```
23
25
  */
24
26
  export function toMermaid(workflow, options = {}) {
25
- const { state = {}, direction = "TB", title } = options;
27
+ const { state = {}, trace, direction = "TB", title } = options;
26
28
  const lines = [];
27
29
  if (title) {
28
30
  lines.push("---");
@@ -30,45 +32,80 @@ export function toMermaid(workflow, options = {}) {
30
32
  lines.push("---");
31
33
  }
32
34
  lines.push(`graph ${direction}`);
35
+ // Build node iteration counts from trace
36
+ const nodeIterations = new Map();
37
+ if (trace) {
38
+ for (const step of trace.steps) {
39
+ nodeIterations.set(step.node, Math.max(nodeIterations.get(step.node) ?? 0, step.iteration));
40
+ }
41
+ }
33
42
  // Nodes
34
43
  for (const [id, node] of Object.entries(workflow.nodes)) {
35
- const label = escapeLabel(node.name);
44
+ let label = escapeLabel(node.name);
36
45
  const isEntry = id === workflow.entry;
37
46
  const entryTag = isEntry ? " \\u25B6" : "";
38
- // Use stadium shape ([...]) for entry, rounded ([(...)]) for others
47
+ // Show iteration count for nodes that ran multiple times
48
+ const iterations = nodeIterations.get(id);
49
+ const iterTag = iterations && iterations > 1 ? ` ×${iterations}` : "";
39
50
  if (isEntry) {
40
- lines.push(` ${sanitizeId(id)}([${label}${entryTag}])`);
51
+ lines.push(` ${sanitizeId(id)}([${label}${entryTag}${iterTag}])`);
41
52
  }
42
53
  else {
43
- lines.push(` ${sanitizeId(id)}[${label}]`);
54
+ lines.push(` ${sanitizeId(id)}[${label}${iterTag}]`);
44
55
  }
45
56
  }
46
57
  lines.push("");
47
- // Edges
58
+ // Build set of taken edges from trace for lookup
59
+ const takenEdges = new Set();
60
+ const edgeTakenCounts = new Map();
61
+ if (trace) {
62
+ for (const te of trace.edges) {
63
+ const key = `${te.from}→${te.to}`;
64
+ takenEdges.add(key);
65
+ edgeTakenCounts.set(key, (edgeTakenCounts.get(key) ?? 0) + 1);
66
+ }
67
+ }
68
+ // Edges — track index for linkStyle directives
69
+ const takenIndices = [];
70
+ const notTakenIndices = [];
71
+ let edgeIndex = 0;
48
72
  for (const edge of workflow.edges) {
49
73
  const from = sanitizeId(edge.from);
50
74
  const to = sanitizeId(edge.to);
51
- if (edge.when) {
52
- const label = escapeLabel(edge.when);
53
- if (edge.max_iterations) {
54
- lines.push(` ${from} -->|"${label} (max ${edge.max_iterations}x)"| ${to}`);
55
- }
56
- else {
57
- lines.push(` ${from} -->|"${label}"| ${to}`);
58
- }
59
- }
60
- else if (edge.max_iterations) {
61
- lines.push(` ${from} -->|"max ${edge.max_iterations}x"| ${to}`);
75
+ const key = `${edge.from}→${edge.to}`;
76
+ const taken = takenEdges.has(key);
77
+ const takenCount = edgeTakenCounts.get(key) ?? 0;
78
+ // Build label parts
79
+ const labelParts = [];
80
+ if (edge.when)
81
+ labelParts.push(edge.when);
82
+ if (edge.max_iterations)
83
+ labelParts.push(`max ${edge.max_iterations}x`);
84
+ // Show how many times this edge was actually followed
85
+ if (takenCount > 1)
86
+ labelParts.push(`taken ${takenCount}x`);
87
+ if (labelParts.length > 0) {
88
+ const label = escapeLabel(labelParts.join(" · "));
89
+ lines.push(` ${from} -->|"${label}"| ${to}`);
62
90
  }
63
91
  else {
64
92
  lines.push(` ${from} --> ${to}`);
65
93
  }
94
+ // Track edge indices for styling
95
+ if (trace) {
96
+ if (taken) {
97
+ takenIndices.push(edgeIndex);
98
+ }
99
+ else {
100
+ notTakenIndices.push(edgeIndex);
101
+ }
102
+ }
103
+ edgeIndex++;
66
104
  }
67
- // Status styling
105
+ // Node status styling
68
106
  const statusNodes = groupByStatus(state);
69
107
  if (statusNodes.size > 0) {
70
108
  lines.push("");
71
- // Define style classes
72
109
  lines.push(" classDef current fill:#3b82f6,stroke:#2563eb,color:#fff,stroke-width:2px");
73
110
  lines.push(" classDef success fill:#22c55e,stroke:#16a34a,color:#fff,stroke-width:2px");
74
111
  lines.push(" classDef failed fill:#ef4444,stroke:#dc2626,color:#fff,stroke-width:2px");
@@ -78,6 +115,16 @@ export function toMermaid(workflow, options = {}) {
78
115
  lines.push(` class ${sanitized} ${status}`);
79
116
  }
80
117
  }
118
+ // Edge styling — taken edges bold green, not-taken edges dashed gray
119
+ if (trace) {
120
+ lines.push("");
121
+ if (takenIndices.length > 0) {
122
+ lines.push(` linkStyle ${takenIndices.join(",")} stroke:#22c55e,stroke-width:3px`);
123
+ }
124
+ if (notTakenIndices.length > 0) {
125
+ lines.push(` linkStyle ${notTakenIndices.join(",")} stroke:#6b7280,stroke-width:1px,stroke-dasharray:5 5`);
126
+ }
127
+ }
81
128
  return lines.join("\n");
82
129
  }
83
130
  /**
@@ -72,7 +72,7 @@ describe("toMermaid", () => {
72
72
  });
73
73
  it("renders max_iterations on edges", () => {
74
74
  const result = toMermaid(withLoop);
75
- expect(result).toContain('a -->|"needs retry (max 3x)"| a');
75
+ expect(result).toContain('a -->|"needs retry · max 3x"| a');
76
76
  expect(result).toContain("a --> b");
77
77
  });
78
78
  it("renders max_iterations without condition", () => {
@@ -168,6 +168,43 @@ describe("toMermaid", () => {
168
168
  expect(result).toContain("class a,b success");
169
169
  expect(result).toContain("class c current");
170
170
  });
171
+ it("highlights taken edges and dims not-taken edges with trace", () => {
172
+ const trace = {
173
+ steps: [
174
+ { node: "triage", status: "success", iteration: 1 },
175
+ { node: "investigate", status: "success", iteration: 1 },
176
+ { node: "close", status: "success", iteration: 1 },
177
+ ],
178
+ edges: [
179
+ { from: "triage", to: "investigate", reason: "severity is high or critical" },
180
+ { from: "investigate", to: "close", reason: "only path" },
181
+ ],
182
+ };
183
+ const result = toMermaid(branching, { trace });
184
+ // Taken edges get green bold styling (edges 0 and 2 in the workflow)
185
+ expect(result).toContain("linkStyle");
186
+ expect(result).toContain("stroke:#22c55e,stroke-width:3px");
187
+ // Not-taken edges get dashed gray
188
+ expect(result).toContain("stroke:#6b7280,stroke-width:1px,stroke-dasharray:5 5");
189
+ });
190
+ it("shows iteration count on nodes and edges from trace", () => {
191
+ const trace = {
192
+ steps: [
193
+ { node: "a", status: "success", iteration: 1 },
194
+ { node: "a", status: "success", iteration: 2 },
195
+ { node: "b", status: "success", iteration: 1 },
196
+ ],
197
+ edges: [
198
+ { from: "a", to: "a", reason: "needs retry" },
199
+ { from: "a", to: "b", reason: "only path" },
200
+ ],
201
+ };
202
+ const result = toMermaid(withLoop, { trace });
203
+ // Node a ran twice — should show ×2
204
+ expect(result).toContain("×2");
205
+ // Node b ran once — no iteration tag
206
+ expect(result).not.toContain("Done ×");
207
+ });
171
208
  });
172
209
  describe("builtin workflows", () => {
173
210
  it("renders triage workflow", () => {
package/dist/schema.d.ts CHANGED
@@ -238,7 +238,11 @@ export interface WorkflowError {
238
238
  * - All edge sources and targets reference existing nodes
239
239
  * - No self-loops
240
240
  * - All nodes are reachable from entry
241
+ * - Unbounded cycles (cycles with no max_iterations guard)
241
242
  * - (optional) All referenced skills exist in the provided skill set
243
+ *
244
+ * Note: nodes with no outgoing edges are valid terminal nodes — the executor
245
+ * returns null when it reaches one, ending the workflow cleanly.
242
246
  */
243
247
  export declare function validateWorkflow(workflow: z.infer<typeof workflowZ>, knownSkills?: Set<string>): WorkflowError[];
244
248
  /**
package/dist/schema.js CHANGED
@@ -65,7 +65,11 @@ export function parseWorkflow(raw) {
65
65
  * - All edge sources and targets reference existing nodes
66
66
  * - No self-loops
67
67
  * - All nodes are reachable from entry
68
+ * - Unbounded cycles (cycles with no max_iterations guard)
68
69
  * - (optional) All referenced skills exist in the provided skill set
70
+ *
71
+ * Note: nodes with no outgoing edges are valid terminal nodes — the executor
72
+ * returns null when it reaches one, ending the workflow cleanly.
69
73
  */
70
74
  export function validateWorkflow(workflow, knownSkills) {
71
75
  const errors = [];
package/dist/types.d.ts CHANGED
@@ -107,6 +107,36 @@ export type ExecutionEvent = {
107
107
  results: Record<string, NodeResult>;
108
108
  };
109
109
  export type Observer = (event: ExecutionEvent) => void;
110
+ /** A single node execution within the trace */
111
+ export interface TraceStep {
112
+ /** Node ID */
113
+ node: string;
114
+ /** Outcome of this execution */
115
+ status: "success" | "failed" | "skipped";
116
+ /** 1-based iteration count (2 = second time this node ran) */
117
+ iteration: number;
118
+ }
119
+ /** A routing decision between nodes */
120
+ export interface TraceEdge {
121
+ from: string;
122
+ to: string;
123
+ /** Why this edge was chosen (condition text or "only path") */
124
+ reason: string;
125
+ }
126
+ /** Full execution trace — ordered steps + edges taken */
127
+ export interface ExecutionTrace {
128
+ /** Ordered list of node executions (includes repeats from retry loops) */
129
+ steps: TraceStep[];
130
+ /** Ordered list of routing decisions */
131
+ edges: TraceEdge[];
132
+ }
133
+ /** Result of execute() — final node results + full execution trace */
134
+ export interface ExecutionResult {
135
+ /** Final result per node (last execution if retried) */
136
+ results: Map<string, NodeResult>;
137
+ /** Full execution trace including loops and routing decisions */
138
+ trace: ExecutionTrace;
139
+ }
110
140
  export interface Claude {
111
141
  /** Run a node: give Claude an instruction, context, and tools */
112
142
  run(opts: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"