@sweny-ai/core 0.1.58 → 0.1.60

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.
@@ -139,6 +139,157 @@ describe("workflowToFlow", () => {
139
139
  });
140
140
  });
141
141
  });
142
+ describe("edge IDs and edgeIndex", () => {
143
+ it("generates unique edge IDs using array index", () => {
144
+ const { edges } = workflowToFlow(testWorkflow);
145
+ const ids = edges.map((e) => e.id);
146
+ expect(ids[0]).toBe("edge-0");
147
+ expect(ids[1]).toBe("edge-1");
148
+ // All IDs are unique
149
+ expect(new Set(ids).size).toBe(ids.length);
150
+ });
151
+ it("assigns correct edgeIndex matching array position", () => {
152
+ const { edges } = workflowToFlow(testWorkflow);
153
+ edges.forEach((edge, i) => {
154
+ expect(edge.edgeIndex).toBe(i);
155
+ });
156
+ });
157
+ it("produces unique IDs for multiple edges between the same node pair", () => {
158
+ const wf = {
159
+ id: "multi-edge",
160
+ name: "Multi-Edge",
161
+ description: "",
162
+ entry: "a",
163
+ nodes: {
164
+ a: { name: "A", instruction: "A", skills: [] },
165
+ b: { name: "B", instruction: "B", skills: [] },
166
+ },
167
+ edges: [
168
+ { from: "a", to: "b" },
169
+ { from: "a", to: "b", when: "error detected" },
170
+ { from: "a", to: "b", when: "retry needed" },
171
+ ],
172
+ };
173
+ const { edges } = workflowToFlow(wf);
174
+ expect(edges).toHaveLength(3);
175
+ const ids = edges.map((e) => e.id);
176
+ expect(new Set(ids).size).toBe(3);
177
+ expect(ids).toEqual(["edge-0", "edge-1", "edge-2"]);
178
+ });
179
+ it("produces stable IDs across multiple conversions", () => {
180
+ const first = workflowToFlow(testWorkflow).edges.map((e) => e.id);
181
+ const second = workflowToFlow(testWorkflow).edges.map((e) => e.id);
182
+ expect(first).toEqual(second);
183
+ });
184
+ });
185
+ describe("max_iterations in conversion", () => {
186
+ const wfWithMaxIter = {
187
+ id: "retry-loop",
188
+ name: "Retry Loop",
189
+ description: "",
190
+ entry: "check",
191
+ nodes: {
192
+ check: { name: "Check", instruction: "Check it", skills: [] },
193
+ fix: { name: "Fix", instruction: "Fix it", skills: [] },
194
+ },
195
+ edges: [
196
+ { from: "check", to: "fix", when: "issue found" },
197
+ { from: "fix", to: "check", when: "needs re-check", max_iterations: 3 },
198
+ ],
199
+ };
200
+ it("includes max_iterations in flow edge data", () => {
201
+ const { edges } = workflowToFlow(wfWithMaxIter);
202
+ const retryEdge = edges.find((e) => e.source === "fix" && e.target === "check");
203
+ expect(retryEdge?.data.max_iterations).toBe(3);
204
+ });
205
+ it("omits max_iterations when not set", () => {
206
+ const { edges } = workflowToFlow(wfWithMaxIter);
207
+ const firstEdge = edges.find((e) => e.source === "check" && e.target === "fix");
208
+ expect(firstEdge?.data.max_iterations).toBeUndefined();
209
+ });
210
+ it("round-trips max_iterations through flowToWorkflow", () => {
211
+ const { nodes, edges } = workflowToFlow(wfWithMaxIter);
212
+ const rebuilt = flowToWorkflow({
213
+ id: wfWithMaxIter.id,
214
+ name: wfWithMaxIter.name,
215
+ description: wfWithMaxIter.description,
216
+ entry: wfWithMaxIter.entry,
217
+ }, nodes, edges);
218
+ const retryEdge = rebuilt.edges.find((e) => e.from === "fix" && e.to === "check");
219
+ expect(retryEdge?.max_iterations).toBe(3);
220
+ // Non-retry edge should not have max_iterations
221
+ const normalEdge = rebuilt.edges.find((e) => e.from === "check" && e.to === "fix");
222
+ expect(normalEdge).not.toHaveProperty("max_iterations");
223
+ });
224
+ });
225
+ describe("Source type preservation", () => {
226
+ it("preserves inline string instructions through round-trip", () => {
227
+ const { nodes, edges } = workflowToFlow(testWorkflow);
228
+ const rebuilt = flowToWorkflow({ id: "x", name: "X", description: "", entry: "a" }, nodes, edges);
229
+ expect(rebuilt.nodes.a.instruction).toBe("Do A");
230
+ });
231
+ it("preserves {file:} Source through round-trip", () => {
232
+ const wf = {
233
+ id: "file-src",
234
+ name: "File Source",
235
+ description: "",
236
+ entry: "a",
237
+ nodes: {
238
+ a: { name: "A", instruction: { file: "./instructions/step-a.md" }, skills: [] },
239
+ },
240
+ edges: [],
241
+ };
242
+ const { nodes, edges } = workflowToFlow(wf);
243
+ const rebuilt = flowToWorkflow({ id: wf.id, name: wf.name, description: "", entry: "a" }, nodes, edges);
244
+ expect(rebuilt.nodes.a.instruction).toEqual({ file: "./instructions/step-a.md" });
245
+ });
246
+ it("preserves {url:} Source through round-trip", () => {
247
+ const wf = {
248
+ id: "url-src",
249
+ name: "URL Source",
250
+ description: "",
251
+ entry: "a",
252
+ nodes: {
253
+ a: { name: "A", instruction: { url: "https://example.com/instruction.md" }, skills: [] },
254
+ },
255
+ edges: [],
256
+ };
257
+ const { nodes, edges } = workflowToFlow(wf);
258
+ const rebuilt = flowToWorkflow({ id: wf.id, name: wf.name, description: "", entry: "a" }, nodes, edges);
259
+ expect(rebuilt.nodes.a.instruction).toEqual({ url: "https://example.com/instruction.md" });
260
+ });
261
+ it("preserves {inline:} Source through round-trip", () => {
262
+ const wf = {
263
+ id: "inline-src",
264
+ name: "Inline Source",
265
+ description: "",
266
+ entry: "a",
267
+ nodes: {
268
+ a: { name: "A", instruction: { inline: "Explicit inline text" }, skills: [] },
269
+ },
270
+ edges: [],
271
+ };
272
+ const { nodes, edges } = workflowToFlow(wf);
273
+ const rebuilt = flowToWorkflow({ id: wf.id, name: wf.name, description: "", entry: "a" }, nodes, edges);
274
+ expect(rebuilt.nodes.a.instruction).toEqual({ inline: "Explicit inline text" });
275
+ });
276
+ it("preserves {url:, type:} Source through round-trip", () => {
277
+ const wf = {
278
+ id: "url-type-src",
279
+ name: "URL Type Source",
280
+ description: "",
281
+ entry: "a",
282
+ nodes: {
283
+ a: { name: "A", instruction: { url: "https://example.com/data", type: "fetch" }, skills: [] },
284
+ },
285
+ edges: [],
286
+ };
287
+ const { nodes, edges } = workflowToFlow(wf);
288
+ const rebuilt = flowToWorkflow({ id: wf.id, name: wf.name, description: "", entry: "a" }, nodes, edges);
289
+ const src = rebuilt.nodes.a.instruction;
290
+ expect(src).toEqual({ url: "https://example.com/data", type: "fetch" });
291
+ });
292
+ });
142
293
  describe("flowToWorkflow", () => {
143
294
  it("round-trips correctly", () => {
144
295
  const { nodes, edges } = workflowToFlow(testWorkflow);
@@ -160,6 +311,13 @@ describe("flowToWorkflow", () => {
160
311
  const unconditional = rebuilt.edges.find((e) => e.from === "a" && e.to === "b");
161
312
  expect(unconditional).not.toHaveProperty("when");
162
313
  });
314
+ it("omits max_iterations when not set", () => {
315
+ const { nodes, edges } = workflowToFlow(testWorkflow);
316
+ const rebuilt = flowToWorkflow({ id: "x", name: "X", description: "", entry: "a" }, nodes, edges);
317
+ for (const edge of rebuilt.edges) {
318
+ expect(edge).not.toHaveProperty("max_iterations");
319
+ }
320
+ });
163
321
  });
164
322
  describe("applyExecutionEvent", () => {
165
323
  function makeNodeDataMap() {
package/dist/schema.d.ts CHANGED
@@ -697,9 +697,9 @@ export interface WorkflowError {
697
697
  export declare function validateWorkflow(workflow: Workflow | z.infer<typeof workflowZ>, knownSkills?: Set<string>): WorkflowError[];
698
698
  export declare const workflowJsonSchema: {
699
699
  readonly $schema: "https://json-schema.org/draft/2020-12/schema";
700
- readonly $id: "https://sweny.ai/schemas/workflow.json";
700
+ readonly $id: "https://spec.sweny.ai/schemas/workflow.json";
701
701
  readonly title: "SWEny Workflow";
702
- readonly description: "A workflow definition for skill-based orchestration (supports controlled cycles via max_iterations)";
702
+ readonly description: "A declarative YAML format for AI agent orchestration as a directed graph with natural language routing.";
703
703
  readonly type: "object";
704
704
  readonly required: readonly ["id", "name", "nodes", "edges", "entry"];
705
705
  readonly additionalProperties: false;
@@ -774,7 +774,7 @@ export declare const workflowJsonSchema: {
774
774
  };
775
775
  readonly instruction: {
776
776
  readonly $ref: "#/$defs/Source";
777
- readonly description: "What Claude should do at this node";
777
+ readonly description: "Natural language instruction for the AI model.";
778
778
  };
779
779
  readonly skills: {
780
780
  readonly type: "array";
package/dist/schema.js CHANGED
@@ -262,9 +262,9 @@ const sourceJsonSchema = {
262
262
  };
263
263
  export const workflowJsonSchema = {
264
264
  $schema: "https://json-schema.org/draft/2020-12/schema",
265
- $id: "https://sweny.ai/schemas/workflow.json",
265
+ $id: "https://spec.sweny.ai/schemas/workflow.json",
266
266
  title: "SWEny Workflow",
267
- description: "A workflow definition for skill-based orchestration (supports controlled cycles via max_iterations)",
267
+ description: "A declarative YAML format for AI agent orchestration as a directed graph with natural language routing.",
268
268
  type: "object",
269
269
  required: ["id", "name", "nodes", "edges", "entry"],
270
270
  additionalProperties: false,
@@ -282,7 +282,7 @@ export const workflowJsonSchema = {
282
282
  additionalProperties: false,
283
283
  properties: {
284
284
  name: { type: "string", minLength: 1 },
285
- instruction: { $ref: "#/$defs/Source", description: "What Claude should do at this node" },
285
+ instruction: { $ref: "#/$defs/Source", description: "Natural language instruction for the AI model." },
286
286
  skills: {
287
287
  type: "array",
288
288
  items: { type: "string" },
package/dist/studio.d.ts CHANGED
@@ -71,8 +71,11 @@ export interface FlowEdge {
71
71
  source: string;
72
72
  target: string;
73
73
  type: "conditionEdge";
74
+ /** Array index of this edge in workflow.edges — used for stable lookups. */
75
+ edgeIndex: number;
74
76
  data: {
75
77
  when?: string;
78
+ max_iterations?: number;
76
79
  isConditional: boolean;
77
80
  };
78
81
  }
package/dist/studio.js CHANGED
@@ -77,13 +77,15 @@ export function workflowToFlow(workflow, skillCatalog = builtinSkills) {
77
77
  exec: { status: "pending" },
78
78
  },
79
79
  }));
80
- const edges = workflowEdges.map((edge) => ({
81
- id: `${edge.from}--${edge.to}`,
80
+ const edges = workflowEdges.map((edge, i) => ({
81
+ id: `edge-${i}`,
82
82
  source: edge.from,
83
83
  target: edge.to,
84
84
  type: "conditionEdge",
85
+ edgeIndex: i,
85
86
  data: {
86
87
  when: edge.when,
88
+ max_iterations: edge.max_iterations,
87
89
  isConditional: !!edge.when,
88
90
  },
89
91
  }));
@@ -146,6 +148,7 @@ export function flowToWorkflow(meta, nodes, edges) {
146
148
  from: e.source,
147
149
  to: e.target,
148
150
  ...(e.data.when ? { when: e.data.when } : {}),
151
+ ...(e.data.max_iterations ? { max_iterations: e.data.max_iterations } : {}),
149
152
  })),
150
153
  };
151
154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"