@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.
- package/dist/__tests__/schema.test.js +1 -1
- package/dist/__tests__/spec-conformance.test.d.ts +18 -0
- package/dist/__tests__/spec-conformance.test.js +1074 -0
- package/dist/__tests__/studio.test.js +158 -0
- package/dist/schema.d.ts +3 -3
- package/dist/schema.js +3 -3
- package/dist/studio.d.ts +3 -0
- package/dist/studio.js +5 -2
- package/package.json +1 -1
|
@@ -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
|
|
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: "
|
|
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
|
|
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: "
|
|
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:
|
|
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
|
}
|