@smithers-orchestrator/graph 0.20.4 → 0.22.0
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/package.json +2 -2
- package/src/dom/extract.js +28 -6
- package/src/extract.js +46 -27
- package/src/types.ts +2 -0
- package/src/validateForkSources.js +107 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Framework-neutral Smithers workflow graph model and extraction helpers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"drizzle-orm": "^0.45.2",
|
|
24
24
|
"zod": "^4.3.6",
|
|
25
|
-
"@smithers-orchestrator/errors": "0.
|
|
25
|
+
"@smithers-orchestrator/errors": "0.22.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/bun": "latest",
|
package/src/dom/extract.js
CHANGED
|
@@ -398,7 +398,8 @@ export function extractFromHost(root, opts) {
|
|
|
398
398
|
: undefined;
|
|
399
399
|
const parallelGroup = nextParallelStack[nextParallelStack.length - 1];
|
|
400
400
|
const topWorktree = nextWorktreeStack[nextWorktreeStack.length - 1];
|
|
401
|
-
const runtime = raw.__smithersSandboxRuntime ?? raw.runtime
|
|
401
|
+
const runtime = raw.__smithersSandboxRuntime ?? raw.runtime;
|
|
402
|
+
const provider = raw.__smithersSandboxProvider ?? raw.provider;
|
|
402
403
|
const workflowDef = raw.__smithersSandboxWorkflow ??
|
|
403
404
|
raw.workflow;
|
|
404
405
|
const descriptor = {
|
|
@@ -431,23 +432,27 @@ export function extractFromHost(root, opts) {
|
|
|
431
432
|
const [
|
|
432
433
|
{ executeSandbox },
|
|
433
434
|
{ executeChildWorkflow },
|
|
435
|
+
{ applyDiffBundle },
|
|
434
436
|
] = await Promise.all([
|
|
435
437
|
loadRuntimeModule("@smithers-orchestrator/sandbox/execute"),
|
|
436
438
|
loadRuntimeModule("@smithers-orchestrator/engine/child-workflow"),
|
|
439
|
+
loadRuntimeModule("@smithers-orchestrator/engine/effect/diff-bundle"),
|
|
437
440
|
]);
|
|
438
441
|
if (!workflowDef) {
|
|
439
442
|
throw new SmithersError("INVALID_INPUT", `Sandbox ${nodeId} is missing workflow definition.`, { nodeId });
|
|
440
443
|
}
|
|
441
444
|
return executeSandbox({
|
|
442
|
-
parentWorkflow:
|
|
443
|
-
? workflowDef
|
|
444
|
-
: undefined,
|
|
445
|
+
parentWorkflow: undefined,
|
|
445
446
|
sandboxId: nodeId,
|
|
446
|
-
|
|
447
|
+
provider,
|
|
448
|
+
runtime: runtime === undefined || runtime === "docker" || runtime === "codeplane" || runtime === "bubblewrap"
|
|
447
449
|
? runtime
|
|
448
|
-
:
|
|
450
|
+
: (() => {
|
|
451
|
+
throw new SmithersError("INVALID_INPUT", `Unsupported sandbox runtime: ${String(runtime)}`, { runtime });
|
|
452
|
+
})(),
|
|
449
453
|
workflow: workflowDef,
|
|
450
454
|
executeChildWorkflow,
|
|
455
|
+
applyDiffBundle,
|
|
451
456
|
input: raw.__smithersSandboxInput ?? raw.input,
|
|
452
457
|
rootDir: topWorktree?.path ?? process.cwd(),
|
|
453
458
|
allowNetwork: Boolean(raw.allowNetwork),
|
|
@@ -455,6 +460,7 @@ export function extractFromHost(root, opts) {
|
|
|
455
460
|
toolTimeoutMs: 60_000,
|
|
456
461
|
reviewDiffs: raw.reviewDiffs,
|
|
457
462
|
autoAcceptDiffs: raw.autoAcceptDiffs,
|
|
463
|
+
allowNested: Boolean(raw.__smithersSandboxAllowNested ?? raw.allowNested),
|
|
458
464
|
config: {
|
|
459
465
|
image: raw.image,
|
|
460
466
|
env: raw.env,
|
|
@@ -472,7 +478,23 @@ export function extractFromHost(root, opts) {
|
|
|
472
478
|
...raw.meta,
|
|
473
479
|
__sandbox: true,
|
|
474
480
|
__sandboxRuntime: runtime,
|
|
481
|
+
__sandboxProvider: provider,
|
|
482
|
+
__sandboxWorkflow: workflowDef,
|
|
475
483
|
__sandboxInput: raw.__smithersSandboxInput ?? raw.input,
|
|
484
|
+
__sandboxAllowNetwork: Boolean(raw.allowNetwork),
|
|
485
|
+
__sandboxReviewDiffs: raw.reviewDiffs,
|
|
486
|
+
__sandboxAutoAcceptDiffs: raw.autoAcceptDiffs,
|
|
487
|
+
__sandboxAllowNested: raw.__smithersSandboxAllowNested ?? raw.allowNested,
|
|
488
|
+
__sandboxConfig: {
|
|
489
|
+
image: raw.image,
|
|
490
|
+
env: raw.env,
|
|
491
|
+
ports: raw.ports,
|
|
492
|
+
volumes: raw.volumes,
|
|
493
|
+
memoryLimit: raw.memoryLimit,
|
|
494
|
+
cpuLimit: raw.cpuLimit,
|
|
495
|
+
command: raw.command,
|
|
496
|
+
workspace: raw.workspace,
|
|
497
|
+
},
|
|
476
498
|
},
|
|
477
499
|
parallelGroupId: parallelGroup?.id,
|
|
478
500
|
parallelMaxConcurrency: parallelGroup?.max,
|
package/src/extract.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
2
|
+
import { getTableName } from "drizzle-orm";
|
|
2
3
|
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
4
|
+
import { validateForkSources } from "./validateForkSources.js";
|
|
3
5
|
/** @typedef {import("./TaskDescriptor.ts").TaskDescriptor} TaskDescriptor */
|
|
4
6
|
/** @typedef {import("./XmlNode.ts").XmlNode} XmlNode */
|
|
5
7
|
/** @typedef {import("./ExtractOptions.ts").ExtractOptions} ExtractOptions */
|
|
@@ -58,23 +60,18 @@ function isZodObject(value) {
|
|
|
58
60
|
}
|
|
59
61
|
/**
|
|
60
62
|
* @param {unknown} value
|
|
61
|
-
* @returns {
|
|
63
|
+
* @returns {boolean}
|
|
62
64
|
*/
|
|
63
|
-
function
|
|
65
|
+
function isDrizzleTable(value) {
|
|
64
66
|
if (!value || typeof value !== "object")
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return symbolValue;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
67
|
+
return false;
|
|
68
|
+
try {
|
|
69
|
+
const name = getTableName(value);
|
|
70
|
+
return typeof name === "string" && name.length > 0;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
75
74
|
}
|
|
76
|
-
const named = value.name;
|
|
77
|
-
return typeof named === "string" && named.length > 0 ? named : undefined;
|
|
78
75
|
}
|
|
79
76
|
/**
|
|
80
77
|
* @param {Record<string, unknown>} raw
|
|
@@ -90,13 +87,17 @@ function resolveOutput(raw) {
|
|
|
90
87
|
outputSchema: undefined,
|
|
91
88
|
};
|
|
92
89
|
}
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
90
|
+
const outputTable = isDrizzleTable(outputRaw) ? outputRaw : null;
|
|
91
|
+
const outputTableName = outputTable
|
|
92
|
+
? getTableName(outputTable)
|
|
93
|
+
: typeof outputRaw === "string"
|
|
94
|
+
? outputRaw
|
|
95
|
+
: "";
|
|
96
|
+
const outputRef = !outputTable && isZodObject(outputRaw) ? outputRaw : undefined;
|
|
96
97
|
const outputSchema = isZodObject(raw.outputSchema) ? raw.outputSchema : outputRef;
|
|
97
98
|
return {
|
|
98
99
|
outputTable,
|
|
99
|
-
outputTableName
|
|
100
|
+
outputTableName,
|
|
100
101
|
outputRef,
|
|
101
102
|
outputSchema,
|
|
102
103
|
};
|
|
@@ -308,13 +309,13 @@ export function extractGraph(root, opts) {
|
|
|
308
309
|
const seenTcf = new Set();
|
|
309
310
|
let ordinal = 0;
|
|
310
311
|
/**
|
|
311
|
-
* @param {Record<string, unknown>} raw
|
|
312
312
|
* @param {string} nodeId
|
|
313
|
+
* @param {string} kind
|
|
313
314
|
* @param {Omit<TaskDescriptor, "ordinal" | "nodeId">} descriptor
|
|
314
315
|
*/
|
|
315
|
-
function addDescriptor(
|
|
316
|
+
function addDescriptor(nodeId, kind, descriptor) {
|
|
316
317
|
if (seen.has(nodeId)) {
|
|
317
|
-
throw new SmithersError("DUPLICATE_ID", `Duplicate ${
|
|
318
|
+
throw new SmithersError("DUPLICATE_ID", `Duplicate ${kind} id detected: ${nodeId}`, { id: nodeId });
|
|
318
319
|
}
|
|
319
320
|
seen.add(nodeId);
|
|
320
321
|
tasks.push({ nodeId, ordinal: ordinal++, ...descriptor });
|
|
@@ -399,7 +400,7 @@ export function extractGraph(root, opts) {
|
|
|
399
400
|
requireOutput(raw, nodeId, "Subflow");
|
|
400
401
|
const { retries, retryPolicy } = resolveRetryConfig(raw);
|
|
401
402
|
const output = resolveOutput(raw);
|
|
402
|
-
addDescriptor(
|
|
403
|
+
addDescriptor(nodeId, "Subflow", {
|
|
403
404
|
...common,
|
|
404
405
|
...output,
|
|
405
406
|
needsApproval: false,
|
|
@@ -431,8 +432,8 @@ export function extractGraph(root, opts) {
|
|
|
431
432
|
requireOutput(raw, nodeId, "Sandbox");
|
|
432
433
|
const { retries, retryPolicy } = resolveRetryConfig(raw);
|
|
433
434
|
const output = resolveOutput(raw);
|
|
434
|
-
const runtime = raw.__smithersSandboxRuntime ?? raw.runtime
|
|
435
|
-
addDescriptor(
|
|
435
|
+
const runtime = raw.__smithersSandboxRuntime ?? raw.runtime;
|
|
436
|
+
addDescriptor(nodeId, "Sandbox", {
|
|
436
437
|
...common,
|
|
437
438
|
...output,
|
|
438
439
|
needsApproval: false,
|
|
@@ -452,7 +453,23 @@ export function extractGraph(root, opts) {
|
|
|
452
453
|
: {}),
|
|
453
454
|
__sandbox: true,
|
|
454
455
|
__sandboxRuntime: runtime,
|
|
456
|
+
__sandboxProvider: raw.__smithersSandboxProvider ?? raw.provider,
|
|
457
|
+
__sandboxWorkflow: raw.__smithersSandboxWorkflow ?? raw.workflow,
|
|
455
458
|
__sandboxInput: raw.__smithersSandboxInput ?? raw.input,
|
|
459
|
+
__sandboxAllowNetwork: Boolean(raw.allowNetwork),
|
|
460
|
+
__sandboxReviewDiffs: raw.reviewDiffs,
|
|
461
|
+
__sandboxAutoAcceptDiffs: raw.autoAcceptDiffs,
|
|
462
|
+
__sandboxAllowNested: raw.__smithersSandboxAllowNested ?? raw.allowNested,
|
|
463
|
+
__sandboxConfig: {
|
|
464
|
+
image: raw.image,
|
|
465
|
+
env: raw.env,
|
|
466
|
+
ports: raw.ports,
|
|
467
|
+
volumes: raw.volumes,
|
|
468
|
+
memoryLimit: raw.memoryLimit,
|
|
469
|
+
cpuLimit: raw.cpuLimit,
|
|
470
|
+
command: raw.command,
|
|
471
|
+
workspace: raw.workspace,
|
|
472
|
+
},
|
|
456
473
|
},
|
|
457
474
|
});
|
|
458
475
|
return;
|
|
@@ -463,7 +480,7 @@ export function extractGraph(root, opts) {
|
|
|
463
480
|
requireOutput(raw, nodeId, "WaitForEvent");
|
|
464
481
|
const output = resolveOutput(raw);
|
|
465
482
|
const onTimeout = raw.__smithersOnTimeout ?? raw.onTimeout ?? "fail";
|
|
466
|
-
addDescriptor(
|
|
483
|
+
addDescriptor(nodeId, "WaitForEvent", {
|
|
467
484
|
...common,
|
|
468
485
|
...output,
|
|
469
486
|
needsApproval: false,
|
|
@@ -508,7 +525,7 @@ export function extractGraph(root, opts) {
|
|
|
508
525
|
if (raw.every !== undefined) {
|
|
509
526
|
throw new SmithersError("INVALID_INPUT", `Timer ${nodeId} uses every=, but recurring timers are not supported yet.`, { nodeId, every: raw.every });
|
|
510
527
|
}
|
|
511
|
-
addDescriptor(
|
|
528
|
+
addDescriptor(nodeId, "Timer", {
|
|
512
529
|
...common,
|
|
513
530
|
outputTable: null,
|
|
514
531
|
outputTableName: "",
|
|
@@ -572,9 +589,10 @@ export function extractGraph(root, opts) {
|
|
|
572
589
|
if (prompt === "[object Object]") {
|
|
573
590
|
throw new SmithersError("MDX_PRELOAD_INACTIVE", `Task "${logicalNodeId}" prompt resolved to [object Object].`);
|
|
574
591
|
}
|
|
575
|
-
addDescriptor(
|
|
592
|
+
addDescriptor(nodeId, "Task", {
|
|
576
593
|
...common,
|
|
577
594
|
...output,
|
|
595
|
+
forkSource: typeof raw.fork === "string" && raw.fork ? raw.fork : undefined,
|
|
578
596
|
needsApproval: Boolean(raw.needsApproval),
|
|
579
597
|
waitAsync: Boolean(raw.waitAsync),
|
|
580
598
|
approvalMode,
|
|
@@ -636,6 +654,7 @@ export function extractGraph(root, opts) {
|
|
|
636
654
|
worktreeStack: [],
|
|
637
655
|
loopStack: [],
|
|
638
656
|
});
|
|
657
|
+
validateForkSources(tasks);
|
|
639
658
|
return { xml: toXmlNode(root), tasks, mountedTaskIds };
|
|
640
659
|
}
|
|
641
660
|
export const extractFromHost = extractGraph;
|
package/src/types.ts
CHANGED
|
@@ -133,6 +133,8 @@ export type TaskDescriptor = {
|
|
|
133
133
|
ralphId?: string;
|
|
134
134
|
dependsOn?: string[];
|
|
135
135
|
needs?: Record<string, string>;
|
|
136
|
+
/** Logical id of the task whose final agent session this task forks. Gates execution and seeds the session. */
|
|
137
|
+
forkSource?: string;
|
|
136
138
|
worktreeId?: string;
|
|
137
139
|
worktreePath?: string;
|
|
138
140
|
worktreeBranch?: string;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
2
|
+
|
|
3
|
+
/** @typedef {import("./TaskDescriptor.ts").TaskDescriptor} TaskDescriptor */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Strip the loop-scope suffix (`@@ralph=0,...`) from a node id to recover the
|
|
7
|
+
* logical task id authored in JSX. `fork` and `dependsOn` reference logical ids.
|
|
8
|
+
* @param {string} nodeId
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
function logicalId(nodeId) {
|
|
12
|
+
const atIdx = nodeId.indexOf("@@");
|
|
13
|
+
return atIdx === -1 ? nodeId : nodeId.slice(0, atIdx);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validate `<Task fork>` references across the extracted task list. Throws a
|
|
18
|
+
* typed SmithersError for fork sources that can never resolve to a usable
|
|
19
|
+
* session snapshot, so authoring mistakes fail fast at graph-build time rather
|
|
20
|
+
* than deadlocking the scheduler.
|
|
21
|
+
*
|
|
22
|
+
* Detects:
|
|
23
|
+
* - TASK_FORK_SOURCE_NOT_FOUND — fork id absent from the graph (covers a
|
|
24
|
+
* source that only exists in an unselected branch).
|
|
25
|
+
* - TASK_FORK_SESSION_UNAVAILABLE — the forking task is not an agent task and
|
|
26
|
+
* therefore has no session to seed.
|
|
27
|
+
* - TASK_FORK_CYCLE — the fork edge closes a dependency cycle, directly or
|
|
28
|
+
* indirectly (via `dependsOn` and/or other fork edges).
|
|
29
|
+
*
|
|
30
|
+
* Loop semantics are intentionally not validated here: `fork` resolves to the
|
|
31
|
+
* latest completed snapshot for a task id at execution time, so a source inside
|
|
32
|
+
* a loop is valid as long as its logical id appears in the graph.
|
|
33
|
+
*
|
|
34
|
+
* @param {readonly TaskDescriptor[]} tasks
|
|
35
|
+
* @returns {void}
|
|
36
|
+
*/
|
|
37
|
+
export function validateForkSources(tasks) {
|
|
38
|
+
const forks = tasks.filter((task) => typeof task.forkSource === "string" && task.forkSource);
|
|
39
|
+
if (forks.length === 0) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const presentLogicalIds = new Set(tasks.map((task) => logicalId(task.nodeId)));
|
|
43
|
+
// Union of dependency edges keyed by logical id: dependsOn entries plus the
|
|
44
|
+
// fork edge. Used only for cycle detection.
|
|
45
|
+
/** @type {Map<string, Set<string>>} */
|
|
46
|
+
const adjacency = new Map();
|
|
47
|
+
const addEdge = (from, to) => {
|
|
48
|
+
const cleanedFrom = logicalId(from);
|
|
49
|
+
const cleanedTo = logicalId(to);
|
|
50
|
+
let set = adjacency.get(cleanedFrom);
|
|
51
|
+
if (!set) {
|
|
52
|
+
set = new Set();
|
|
53
|
+
adjacency.set(cleanedFrom, set);
|
|
54
|
+
}
|
|
55
|
+
set.add(cleanedTo);
|
|
56
|
+
};
|
|
57
|
+
for (const task of tasks) {
|
|
58
|
+
const from = logicalId(task.nodeId);
|
|
59
|
+
for (const dep of task.dependsOn ?? []) {
|
|
60
|
+
addEdge(from, dep);
|
|
61
|
+
}
|
|
62
|
+
if (typeof task.forkSource === "string" && task.forkSource) {
|
|
63
|
+
addEdge(from, task.forkSource);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} from
|
|
68
|
+
* @param {string} target
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
const canReach = (from, target) => {
|
|
72
|
+
const stack = [from];
|
|
73
|
+
const visited = new Set();
|
|
74
|
+
while (stack.length > 0) {
|
|
75
|
+
const current = /** @type {string} */ (stack.pop());
|
|
76
|
+
if (current === target) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (visited.has(current)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
visited.add(current);
|
|
83
|
+
const neighbors = adjacency.get(current);
|
|
84
|
+
if (neighbors) {
|
|
85
|
+
for (const next of neighbors) {
|
|
86
|
+
stack.push(next);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
};
|
|
92
|
+
for (const task of forks) {
|
|
93
|
+
const forkSource = /** @type {string} */ (task.forkSource);
|
|
94
|
+
const nodeLogicalId = logicalId(task.nodeId);
|
|
95
|
+
if (!presentLogicalIds.has(forkSource)) {
|
|
96
|
+
throw new SmithersError("TASK_FORK_SOURCE_NOT_FOUND", `Task "${task.nodeId}" forks "${forkSource}", which is not present in the workflow graph.`, { nodeId: task.nodeId, forkSource });
|
|
97
|
+
}
|
|
98
|
+
if (!task.agent) {
|
|
99
|
+
throw new SmithersError("TASK_FORK_SESSION_UNAVAILABLE", `Task "${task.nodeId}" uses fork but is not an agent task. Only agent tasks can fork a session.`, { nodeId: task.nodeId, forkSource });
|
|
100
|
+
}
|
|
101
|
+
// The fork edge is nodeLogicalId -> forkSource. A cycle exists iff
|
|
102
|
+
// forkSource can already reach nodeLogicalId through the dependency graph.
|
|
103
|
+
if (canReach(forkSource, nodeLogicalId)) {
|
|
104
|
+
throw new SmithersError("TASK_FORK_CYCLE", `Task "${task.nodeId}" forks "${forkSource}", which creates a dependency cycle.`, { nodeId: task.nodeId, forkSource });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|