@sweny-ai/core 0.1.128 → 0.1.129
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/executor.js +11 -0
- package/dist/schema.d.ts +18 -1
- package/dist/schema.js +87 -42
- package/package.json +1 -1
package/dist/executor.js
CHANGED
|
@@ -19,6 +19,7 @@ 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 { validateWorkflow } from "./schema.js";
|
|
22
23
|
import { resolveAgentAccess } from "./agent-env.js";
|
|
23
24
|
import { fenceUntrusted } from "./untrusted.js";
|
|
24
25
|
/**
|
|
@@ -1052,6 +1053,16 @@ function validate(workflow, skills) {
|
|
|
1052
1053
|
if (!workflow.nodes[edge.to])
|
|
1053
1054
|
throw new Error(`Edge references unknown node: "${edge.to}"`);
|
|
1054
1055
|
}
|
|
1056
|
+
// Same structural validation the CLI loader runs (reachability, self-loops,
|
|
1057
|
+
// ambiguous edges, unbounded cycles, reserved eval policies, retry and
|
|
1058
|
+
// max_iterations ceilings). Library callers, Studio's simulator and run.ts
|
|
1059
|
+
// reach execute() without the loader, so without this they got none of it
|
|
1060
|
+
// (#326). Runs once per execute() call, before any node runs. Skill
|
|
1061
|
+
// availability stays a warning below, so no knownSkills here.
|
|
1062
|
+
const structural = validateWorkflow(workflow);
|
|
1063
|
+
if (structural.length > 0) {
|
|
1064
|
+
throw new Error(`Invalid workflow "${workflow.id}":\n${structural.map((e) => ` ${e.code}: ${e.message}`).join("\n")}`);
|
|
1065
|
+
}
|
|
1055
1066
|
// Check that each node has at least one available skill (if it lists any)
|
|
1056
1067
|
for (const [nodeId, node] of Object.entries(workflow.nodes)) {
|
|
1057
1068
|
if (node.skills.length === 0)
|
package/dist/schema.d.ts
CHANGED
|
@@ -776,6 +776,22 @@ export declare const nodeToolsZ: z.ZodEffects<z.ZodObject<{
|
|
|
776
776
|
deny?: string[] | undefined;
|
|
777
777
|
allow?: string[] | undefined;
|
|
778
778
|
}>;
|
|
779
|
+
/**
|
|
780
|
+
* Ceiling on `retry.max`. Each retry attempt re-invokes the agent on the
|
|
781
|
+
* node (full model spend), so an unbounded value here is an unbounded spend
|
|
782
|
+
* multiplier per node, independent of the workflow-level `max_steps` budget.
|
|
783
|
+
* 10 comfortably covers every declared workflow today (max observed: 1) while
|
|
784
|
+
* still catching a fat-fingered value (e.g. a missing digit) at author time
|
|
785
|
+
* instead of at runtime. See #325.
|
|
786
|
+
*/
|
|
787
|
+
export declare const NODE_RETRY_MAX_CEILING = 10;
|
|
788
|
+
/**
|
|
789
|
+
* Ceiling on an edge's `max_iterations`. A bounded back-edge is still a spend
|
|
790
|
+
* multiplier (each lap re-runs model nodes), and the default `max_steps` is
|
|
791
|
+
* 200, so a value above this can never be reached in a default run and only
|
|
792
|
+
* hides a typo. See #325.
|
|
793
|
+
*/
|
|
794
|
+
export declare const EDGE_MAX_ITERATIONS_CEILING = 100;
|
|
779
795
|
export declare const nodeRetryZ: z.ZodObject<{
|
|
780
796
|
max: z.ZodNumber;
|
|
781
797
|
instruction: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodObject<{
|
|
@@ -2735,7 +2751,7 @@ export declare function parseWorkflow(raw: unknown): {
|
|
|
2735
2751
|
}> | undefined;
|
|
2736
2752
|
};
|
|
2737
2753
|
export interface WorkflowError {
|
|
2738
|
-
code: "MISSING_ENTRY" | "UNKNOWN_EDGE_SOURCE" | "UNKNOWN_EDGE_TARGET" | "UNREACHABLE_NODE" | "UNKNOWN_SKILL" | "SELF_LOOP" | "UNBOUNDED_CYCLE" | "AMBIGUOUS_EDGES" | "UNSUPPORTED_EVAL_POLICY" | "INVALID_INLINE_SKILL";
|
|
2754
|
+
code: "MISSING_ENTRY" | "UNKNOWN_EDGE_SOURCE" | "UNKNOWN_EDGE_TARGET" | "UNREACHABLE_NODE" | "UNKNOWN_SKILL" | "SELF_LOOP" | "UNBOUNDED_CYCLE" | "AMBIGUOUS_EDGES" | "UNSUPPORTED_EVAL_POLICY" | "INVALID_INLINE_SKILL" | "EDGE_ITERATIONS_EXCEEDED" | "RETRY_MAX_EXCEEDED";
|
|
2739
2755
|
message: string;
|
|
2740
2756
|
nodeId?: string;
|
|
2741
2757
|
}
|
|
@@ -3347,6 +3363,7 @@ export declare const workflowJsonSchema: {
|
|
|
3347
3363
|
readonly max_iterations: {
|
|
3348
3364
|
readonly type: "integer";
|
|
3349
3365
|
readonly minimum: 1;
|
|
3366
|
+
readonly maximum: 100;
|
|
3350
3367
|
readonly description: "Max times this edge can be followed. Enables controlled retry loops.";
|
|
3351
3368
|
};
|
|
3352
3369
|
};
|
package/dist/schema.js
CHANGED
|
@@ -230,7 +230,14 @@ const retryInstructionReflectZ = z.object({ reflect: z.string().min(1) }).strict
|
|
|
230
230
|
* still catching a fat-fingered value (e.g. a missing digit) at author time
|
|
231
231
|
* instead of at runtime. See #325.
|
|
232
232
|
*/
|
|
233
|
-
const NODE_RETRY_MAX_CEILING = 10;
|
|
233
|
+
export const NODE_RETRY_MAX_CEILING = 10;
|
|
234
|
+
/**
|
|
235
|
+
* Ceiling on an edge's `max_iterations`. A bounded back-edge is still a spend
|
|
236
|
+
* multiplier (each lap re-runs model nodes), and the default `max_steps` is
|
|
237
|
+
* 200, so a value above this can never be reached in a default run and only
|
|
238
|
+
* hides a typo. See #325.
|
|
239
|
+
*/
|
|
240
|
+
export const EDGE_MAX_ITERATIONS_CEILING = 100;
|
|
234
241
|
export const nodeRetryZ = z
|
|
235
242
|
.object({
|
|
236
243
|
max: z.number().int().min(1).max(NODE_RETRY_MAX_CEILING),
|
|
@@ -263,7 +270,7 @@ export const edgeZ = z
|
|
|
263
270
|
from: z.string().min(1),
|
|
264
271
|
to: z.string().min(1),
|
|
265
272
|
when: z.string().optional(),
|
|
266
|
-
max_iterations: z.number().int().min(1).optional(),
|
|
273
|
+
max_iterations: z.number().int().min(1).max(EDGE_MAX_ITERATIONS_CEILING).optional(),
|
|
267
274
|
})
|
|
268
275
|
.strict();
|
|
269
276
|
/**
|
|
@@ -352,13 +359,25 @@ export function parseWorkflow(raw) {
|
|
|
352
359
|
export function validateWorkflow(workflow, knownSkills) {
|
|
353
360
|
const errors = [];
|
|
354
361
|
const nodeIds = new Set(Object.keys(workflow.nodes));
|
|
362
|
+
const entryExists = nodeIds.has(workflow.entry);
|
|
355
363
|
// Entry must exist
|
|
356
|
-
if (!
|
|
364
|
+
if (!entryExists) {
|
|
357
365
|
errors.push({
|
|
358
366
|
code: "MISSING_ENTRY",
|
|
359
367
|
message: `Entry node "${workflow.entry}" does not exist`,
|
|
360
368
|
});
|
|
361
369
|
}
|
|
370
|
+
// Adjacency over real nodes, built once (O(nodes + edges)) so reachability
|
|
371
|
+
// and cycle detection stay linear on large graphs.
|
|
372
|
+
const outAll = new Map();
|
|
373
|
+
const outUnbounded = new Map();
|
|
374
|
+
const push = (m, k, v) => {
|
|
375
|
+
const l = m.get(k);
|
|
376
|
+
if (l)
|
|
377
|
+
l.push(v);
|
|
378
|
+
else
|
|
379
|
+
m.set(k, [v]);
|
|
380
|
+
};
|
|
362
381
|
// Edge targets must exist
|
|
363
382
|
for (const edge of workflow.edges) {
|
|
364
383
|
if (!nodeIds.has(edge.from)) {
|
|
@@ -382,6 +401,18 @@ export function validateWorkflow(workflow, knownSkills) {
|
|
|
382
401
|
nodeId: edge.from,
|
|
383
402
|
});
|
|
384
403
|
}
|
|
404
|
+
if (edge.max_iterations != null && edge.max_iterations > EDGE_MAX_ITERATIONS_CEILING) {
|
|
405
|
+
errors.push({
|
|
406
|
+
code: "EDGE_ITERATIONS_EXCEEDED",
|
|
407
|
+
message: `Edge from "${edge.from}" to "${edge.to}" declares max_iterations ${edge.max_iterations}, above the ceiling of ${EDGE_MAX_ITERATIONS_CEILING}`,
|
|
408
|
+
nodeId: edge.from,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
if (nodeIds.has(edge.from) && nodeIds.has(edge.to)) {
|
|
412
|
+
push(outAll, edge.from, edge.to);
|
|
413
|
+
if (!edge.max_iterations && edge.from !== edge.to)
|
|
414
|
+
push(outUnbounded, edge.from, edge.to);
|
|
415
|
+
}
|
|
385
416
|
}
|
|
386
417
|
// Edge determinism: a node with two or more out-edges that have no `when`
|
|
387
418
|
// clause is ambiguous. The executor's resolveNext picks the first such edge
|
|
@@ -396,9 +427,7 @@ export function validateWorkflow(workflow, knownSkills) {
|
|
|
396
427
|
continue;
|
|
397
428
|
if (edge.when)
|
|
398
429
|
continue;
|
|
399
|
-
|
|
400
|
-
list.push(edge.to);
|
|
401
|
-
unconditionalByNode.set(edge.from, list);
|
|
430
|
+
push(unconditionalByNode, edge.from, edge.to);
|
|
402
431
|
}
|
|
403
432
|
for (const [from, targets] of unconditionalByNode) {
|
|
404
433
|
if (targets.length > 1) {
|
|
@@ -409,60 +438,63 @@ export function validateWorkflow(workflow, knownSkills) {
|
|
|
409
438
|
});
|
|
410
439
|
}
|
|
411
440
|
}
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (edge.from === id && !visited.has(edge.to)) {
|
|
425
|
-
queue.push(edge.to);
|
|
441
|
+
// Reachability: BFS from entry. Skipped only when the entry is missing
|
|
442
|
+
// (every node would be reported, which is noise). All other diagnostics
|
|
443
|
+
// accumulate so one pass reports every structural problem.
|
|
444
|
+
if (entryExists) {
|
|
445
|
+
const visited = new Set([workflow.entry]);
|
|
446
|
+
const queue = [workflow.entry];
|
|
447
|
+
for (let i = 0; i < queue.length; i++) {
|
|
448
|
+
for (const to of outAll.get(queue[i]) ?? []) {
|
|
449
|
+
if (!visited.has(to)) {
|
|
450
|
+
visited.add(to);
|
|
451
|
+
queue.push(to);
|
|
452
|
+
}
|
|
426
453
|
}
|
|
427
454
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
455
|
+
for (const nodeId of nodeIds) {
|
|
456
|
+
if (!visited.has(nodeId)) {
|
|
457
|
+
errors.push({
|
|
458
|
+
code: "UNREACHABLE_NODE",
|
|
459
|
+
message: `Node "${nodeId}" is unreachable from entry "${workflow.entry}"`,
|
|
460
|
+
nodeId,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
436
463
|
}
|
|
437
464
|
}
|
|
438
465
|
// Detect unbounded cycles: the graph with max_iterations edges removed must be acyclic.
|
|
439
466
|
// If removing bounded edges still leaves a cycle, it can loop forever.
|
|
440
|
-
|
|
441
|
-
//
|
|
467
|
+
// Iterative DFS with an explicit stack so a deep chain cannot overflow the
|
|
468
|
+
// call stack (#326).
|
|
442
469
|
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
443
470
|
const color = new Map();
|
|
444
471
|
for (const id of nodeIds)
|
|
445
472
|
color.set(id, WHITE);
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
473
|
+
const findCycle = (start) => {
|
|
474
|
+
const stack = [{ node: start, i: 0 }];
|
|
475
|
+
color.set(start, GRAY);
|
|
476
|
+
while (stack.length > 0) {
|
|
477
|
+
const top = stack[stack.length - 1];
|
|
478
|
+
const outs = outUnbounded.get(top.node) ?? [];
|
|
479
|
+
if (top.i >= outs.length) {
|
|
480
|
+
color.set(top.node, BLACK);
|
|
481
|
+
stack.pop();
|
|
450
482
|
continue;
|
|
451
|
-
|
|
483
|
+
}
|
|
484
|
+
const to = outs[top.i++];
|
|
485
|
+
const c = color.get(to);
|
|
452
486
|
if (c === GRAY)
|
|
453
|
-
return
|
|
487
|
+
return to; // back-edge found → cycle
|
|
454
488
|
if (c === WHITE) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
return cycle;
|
|
489
|
+
color.set(to, GRAY);
|
|
490
|
+
stack.push({ node: to, i: 0 });
|
|
458
491
|
}
|
|
459
492
|
}
|
|
460
|
-
color.set(node, BLACK);
|
|
461
493
|
return null;
|
|
462
|
-
}
|
|
494
|
+
};
|
|
463
495
|
for (const id of nodeIds) {
|
|
464
496
|
if (color.get(id) === WHITE) {
|
|
465
|
-
const cycleNode =
|
|
497
|
+
const cycleNode = findCycle(id);
|
|
466
498
|
if (cycleNode) {
|
|
467
499
|
errors.push({
|
|
468
500
|
code: "UNBOUNDED_CYCLE",
|
|
@@ -472,6 +504,18 @@ export function validateWorkflow(workflow, knownSkills) {
|
|
|
472
504
|
}
|
|
473
505
|
}
|
|
474
506
|
}
|
|
507
|
+
// Retry ceiling for workflows that never went through zod (library callers,
|
|
508
|
+
// the e2e batch runner). Mirrors nodeRetryZ so every path enforces it (#325).
|
|
509
|
+
for (const [nodeId, node] of Object.entries(workflow.nodes)) {
|
|
510
|
+
const max = node.retry?.max;
|
|
511
|
+
if (max != null && max > NODE_RETRY_MAX_CEILING) {
|
|
512
|
+
errors.push({
|
|
513
|
+
code: "RETRY_MAX_EXCEEDED",
|
|
514
|
+
message: `Node "${nodeId}" declares retry.max ${max}, above the ceiling of ${NODE_RETRY_MAX_CEILING}`,
|
|
515
|
+
nodeId,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
475
519
|
// Reject eval policies that are reserved in the vocabulary but not yet
|
|
476
520
|
// implemented. The Zod schema accepts the whole EVAL_POLICIES enum (the
|
|
477
521
|
// published JSON schema advertises them as reserved), but only `all_pass`
|
|
@@ -941,6 +985,7 @@ export const workflowJsonSchema = {
|
|
|
941
985
|
max_iterations: {
|
|
942
986
|
type: "integer",
|
|
943
987
|
minimum: 1,
|
|
988
|
+
maximum: EDGE_MAX_ITERATIONS_CEILING,
|
|
944
989
|
description: "Max times this edge can be followed. Enables controlled retry loops.",
|
|
945
990
|
},
|
|
946
991
|
},
|