@smithers-orchestrator/graph 0.16.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/LICENSE +21 -0
- package/package.json +35 -0
- package/src/AgentLike.ts +1 -0
- package/src/ApprovalOption.ts +1 -0
- package/src/CachePolicy.ts +1 -0
- package/src/ExtractGraph.ts +1 -0
- package/src/ExtractOptions.ts +1 -0
- package/src/ExtractResult.ts +8 -0
- package/src/GraphSnapshot.ts +9 -0
- package/src/HostElement.ts +1 -0
- package/src/HostNode.ts +1 -0
- package/src/HostText.ts +1 -0
- package/src/MemoryNamespace.ts +1 -0
- package/src/MemoryNamespaceKind.ts +1 -0
- package/src/RetryPolicy.ts +1 -0
- package/src/SamplingConfig.ts +1 -0
- package/src/ScoreResult.ts +1 -0
- package/src/Scorer.ts +1 -0
- package/src/ScorerBinding.ts +1 -0
- package/src/ScorerFn.ts +1 -0
- package/src/ScorerInput.ts +1 -0
- package/src/ScorersMap.ts +1 -0
- package/src/TaskDescriptor.ts +1 -0
- package/src/TaskMemoryConfig.ts +1 -0
- package/src/WorkflowGraph.ts +1 -0
- package/src/XmlElement.ts +1 -0
- package/src/XmlNode.ts +1 -0
- package/src/XmlText.ts +1 -0
- package/src/constants.js +5 -0
- package/src/dom/extract.js +796 -0
- package/src/extract.js +616 -0
- package/src/index.d.ts +212 -0
- package/src/index.js +32 -0
- package/src/types.ts +192 -0
- package/src/utils/tree-ids.js +22 -0
- package/src/utils/xml.js +42 -0
package/src/extract.js
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
2
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
3
|
+
/** @typedef {import("./TaskDescriptor.ts").TaskDescriptor} TaskDescriptor */
|
|
4
|
+
/** @typedef {import("./XmlNode.ts").XmlNode} XmlNode */
|
|
5
|
+
/** @typedef {import("./ExtractOptions.ts").ExtractOptions} ExtractOptions */
|
|
6
|
+
/** @typedef {import("./HostNode.ts").HostNode} HostNode */
|
|
7
|
+
/** @typedef {import("./WorkflowGraph.ts").WorkflowGraph} WorkflowGraph */
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MERGE_QUEUE_CONCURRENCY = 1;
|
|
10
|
+
const WORKTREE_EMPTY_PATH_ERROR = "<Worktree> requires a non-empty path prop";
|
|
11
|
+
const DEFAULT_LOCAL_TASK_HEARTBEAT_TIMEOUT_MS = 300_000;
|
|
12
|
+
const DEFAULT_SANDBOX_TASK_HEARTBEAT_TIMEOUT_MS = 300_000;
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} prefix
|
|
15
|
+
* @param {readonly number[]} path
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
function stablePathId(prefix, path) {
|
|
19
|
+
if (path.length === 0)
|
|
20
|
+
return `${prefix}:root`;
|
|
21
|
+
return `${prefix}:${path.join(".")}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* @param {unknown} explicitId
|
|
25
|
+
* @param {string} prefix
|
|
26
|
+
* @param {readonly number[]} path
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
function resolveStableId(explicitId, prefix, path) {
|
|
30
|
+
if (typeof explicitId === "string" && explicitId.trim().length > 0) {
|
|
31
|
+
return explicitId;
|
|
32
|
+
}
|
|
33
|
+
return stablePathId(prefix, path);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* @param {unknown} value
|
|
37
|
+
* @returns {value is import("zod").ZodObject<any>}
|
|
38
|
+
*/
|
|
39
|
+
function isZodObject(value) {
|
|
40
|
+
return Boolean(value && typeof value === "object" && "shape" in value);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* @param {unknown} value
|
|
44
|
+
* @returns {string | undefined}
|
|
45
|
+
*/
|
|
46
|
+
function maybeTableName(value) {
|
|
47
|
+
if (!value || typeof value !== "object")
|
|
48
|
+
return undefined;
|
|
49
|
+
const symbols = Object.getOwnPropertySymbols(value);
|
|
50
|
+
for (const symbol of symbols) {
|
|
51
|
+
const key = String(symbol);
|
|
52
|
+
if (key.includes("drizzle") || key.includes("Name")) {
|
|
53
|
+
const symbolValue = value[symbol];
|
|
54
|
+
if (typeof symbolValue === "string" && symbolValue.length > 0) {
|
|
55
|
+
return symbolValue;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const named = value.name;
|
|
60
|
+
return typeof named === "string" && named.length > 0 ? named : undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* @param {Record<string, unknown>} raw
|
|
64
|
+
* @returns {{ outputTable: unknown | null; outputTableName: string; outputRef: import("zod").ZodObject<any> | undefined; outputSchema: import("zod").ZodObject<any> | undefined; }}
|
|
65
|
+
*/
|
|
66
|
+
function resolveOutput(raw) {
|
|
67
|
+
const outputRaw = raw.output;
|
|
68
|
+
if (!outputRaw) {
|
|
69
|
+
return {
|
|
70
|
+
outputTable: null,
|
|
71
|
+
outputTableName: "",
|
|
72
|
+
outputRef: undefined,
|
|
73
|
+
outputSchema: undefined,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const outputRef = isZodObject(outputRaw) ? outputRaw : undefined;
|
|
77
|
+
const tableName = typeof outputRaw === "string" ? outputRaw : maybeTableName(outputRaw) ?? "";
|
|
78
|
+
const outputTable = outputRef ? null : typeof outputRaw === "string" ? null : outputRaw;
|
|
79
|
+
const outputSchema = isZodObject(raw.outputSchema) ? raw.outputSchema : outputRef;
|
|
80
|
+
return {
|
|
81
|
+
outputTable,
|
|
82
|
+
outputTableName: tableName,
|
|
83
|
+
outputRef,
|
|
84
|
+
outputSchema,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* @param {Record<string, unknown>} raw
|
|
89
|
+
* @returns {number | null}
|
|
90
|
+
*/
|
|
91
|
+
function parseHeartbeatTimeoutMs(raw) {
|
|
92
|
+
const candidate = typeof raw.heartbeatTimeoutMs === "number"
|
|
93
|
+
? raw.heartbeatTimeoutMs
|
|
94
|
+
: typeof raw.heartbeatTimeout === "number"
|
|
95
|
+
? raw.heartbeatTimeout
|
|
96
|
+
: null;
|
|
97
|
+
if (candidate == null || !Number.isFinite(candidate) || candidate <= 0) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return Math.floor(candidate);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* @param {Record<string, unknown>} raw
|
|
104
|
+
*/
|
|
105
|
+
function resolveRetryConfig(raw) {
|
|
106
|
+
const noRetry = Boolean(raw.noRetry);
|
|
107
|
+
const continueOnFail = Boolean(raw.continueOnFail);
|
|
108
|
+
const hasExplicitRetries = typeof raw.retries === "number" && !Number.isNaN(raw.retries);
|
|
109
|
+
const hasExplicitRetryPolicy = Boolean(raw.retryPolicy && typeof raw.retryPolicy === "object");
|
|
110
|
+
const defaultNoRetryForContinueOnFail = continueOnFail && !hasExplicitRetries && !hasExplicitRetryPolicy;
|
|
111
|
+
const retries = noRetry || defaultNoRetryForContinueOnFail
|
|
112
|
+
? 0
|
|
113
|
+
: hasExplicitRetries
|
|
114
|
+
? raw.retries
|
|
115
|
+
: Infinity;
|
|
116
|
+
const retryPolicy = hasExplicitRetryPolicy
|
|
117
|
+
? raw.retryPolicy
|
|
118
|
+
: retries > 0
|
|
119
|
+
? { backoff: "exponential", initialDelayMs: 1000 }
|
|
120
|
+
: undefined;
|
|
121
|
+
return { retries, retryPolicy };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* @param {HostNode} node
|
|
125
|
+
* @returns {XmlNode}
|
|
126
|
+
*/
|
|
127
|
+
function toXmlNode(node) {
|
|
128
|
+
if (node.kind === "text") {
|
|
129
|
+
return { kind: "text", text: node.text };
|
|
130
|
+
}
|
|
131
|
+
const element = {
|
|
132
|
+
kind: "element",
|
|
133
|
+
tag: node.tag,
|
|
134
|
+
props: node.props ?? {},
|
|
135
|
+
children: node.children.map(toXmlNode),
|
|
136
|
+
};
|
|
137
|
+
return element;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* @param {ExtractOptions | undefined} opts
|
|
141
|
+
* @param {string} id
|
|
142
|
+
* @returns {number}
|
|
143
|
+
*/
|
|
144
|
+
function getRalphIteration(opts, id) {
|
|
145
|
+
const map = opts?.ralphIterations;
|
|
146
|
+
const fallback = typeof opts?.defaultIteration === "number" ? opts.defaultIteration : 0;
|
|
147
|
+
if (!map)
|
|
148
|
+
return fallback;
|
|
149
|
+
if (map instanceof Map) {
|
|
150
|
+
return map.get(id) ?? fallback;
|
|
151
|
+
}
|
|
152
|
+
const value = map[id];
|
|
153
|
+
return typeof value === "number" ? value : fallback;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* @param {readonly { readonly ralphId: string; readonly iteration: number }[]} loopStack
|
|
157
|
+
* @returns {string}
|
|
158
|
+
*/
|
|
159
|
+
function buildLoopScope(loopStack) {
|
|
160
|
+
if (loopStack.length === 0)
|
|
161
|
+
return "";
|
|
162
|
+
return `@@${loopStack.map((entry) => `${entry.ralphId}=${entry.iteration}`).join(",")}`;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* @param {unknown} value
|
|
166
|
+
* @returns {string[] | undefined}
|
|
167
|
+
*/
|
|
168
|
+
function strings(value) {
|
|
169
|
+
if (!Array.isArray(value))
|
|
170
|
+
return undefined;
|
|
171
|
+
const filtered = value.filter((entry) => typeof entry === "string");
|
|
172
|
+
return filtered.length > 0 ? filtered : undefined;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* @param {unknown} value
|
|
176
|
+
* @returns {Record<string, string> | undefined}
|
|
177
|
+
*/
|
|
178
|
+
function needs(value) {
|
|
179
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
const filtered = Object.entries(value).filter((entry) => typeof entry[1] === "string");
|
|
183
|
+
return filtered.length > 0 ? Object.fromEntries(filtered) : undefined;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* @param {unknown} value
|
|
187
|
+
* @returns {TaskDescriptor["approvalOptions"]}
|
|
188
|
+
*/
|
|
189
|
+
function approvalOptions(value) {
|
|
190
|
+
if (!Array.isArray(value))
|
|
191
|
+
return undefined;
|
|
192
|
+
const options = value
|
|
193
|
+
.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)))
|
|
194
|
+
.map((entry) => ({
|
|
195
|
+
key: typeof entry.key === "string" ? entry.key : "",
|
|
196
|
+
label: typeof entry.label === "string" ? entry.label : "",
|
|
197
|
+
...(typeof entry.summary === "string" ? { summary: entry.summary } : {}),
|
|
198
|
+
...(entry.metadata &&
|
|
199
|
+
typeof entry.metadata === "object" &&
|
|
200
|
+
!Array.isArray(entry.metadata)
|
|
201
|
+
? { metadata: entry.metadata }
|
|
202
|
+
: {}),
|
|
203
|
+
}))
|
|
204
|
+
.filter((entry) => entry.key.length > 0 && entry.label.length > 0);
|
|
205
|
+
return options.length > 0 ? options : undefined;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* @param {unknown} value
|
|
209
|
+
* @returns {TaskDescriptor["approvalAutoApprove"]}
|
|
210
|
+
*/
|
|
211
|
+
function approvalAutoApprove(value) {
|
|
212
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
const raw = value;
|
|
216
|
+
return {
|
|
217
|
+
...(typeof raw.after === "number" ? { after: raw.after } : {}),
|
|
218
|
+
...(typeof raw.audit === "boolean" ? { audit: raw.audit } : {}),
|
|
219
|
+
...(typeof raw.conditionMet === "boolean"
|
|
220
|
+
? { conditionMet: raw.conditionMet }
|
|
221
|
+
: {}),
|
|
222
|
+
...(typeof raw.revertOnMet === "boolean"
|
|
223
|
+
? { revertOnMet: raw.revertOnMet }
|
|
224
|
+
: {}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* @param {"parallel" | "merge-queue"} tag
|
|
229
|
+
* @param {Record<string, unknown>} raw
|
|
230
|
+
* @param {readonly number[]} path
|
|
231
|
+
* @param {readonly { readonly id: string; readonly max?: number }[]} stack
|
|
232
|
+
*/
|
|
233
|
+
function pushGroup(tag, raw, path, stack) {
|
|
234
|
+
const id = resolveStableId(raw.id, tag, path);
|
|
235
|
+
const parsed = Number(raw.maxConcurrency);
|
|
236
|
+
const rawMax = Number.isFinite(parsed) ? Math.floor(parsed) : undefined;
|
|
237
|
+
let max;
|
|
238
|
+
if (tag === "merge-queue") {
|
|
239
|
+
max = Math.max(1, rawMax ?? DEFAULT_MERGE_QUEUE_CONCURRENCY);
|
|
240
|
+
}
|
|
241
|
+
else if (rawMax == null || rawMax <= 0) {
|
|
242
|
+
max = undefined;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
max = rawMax;
|
|
246
|
+
}
|
|
247
|
+
return [...stack, { id, max }];
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* @param {Record<string, unknown>} raw
|
|
251
|
+
* @param {string} kind
|
|
252
|
+
* @returns {string}
|
|
253
|
+
*/
|
|
254
|
+
function requireTaskId(raw, kind) {
|
|
255
|
+
if (!raw.id || typeof raw.id !== "string") {
|
|
256
|
+
throw new SmithersError("TASK_ID_REQUIRED", `${kind} id is required and must be a string.`);
|
|
257
|
+
}
|
|
258
|
+
return raw.id;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* @param {Record<string, unknown>} raw
|
|
262
|
+
* @param {string} nodeId
|
|
263
|
+
* @param {string} kind
|
|
264
|
+
*/
|
|
265
|
+
function requireOutput(raw, nodeId, kind) {
|
|
266
|
+
if (!raw.output) {
|
|
267
|
+
throw new SmithersError("TASK_MISSING_OUTPUT", `${kind} ${nodeId} is missing output.`, { nodeId });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* @param {HostNode | null} root
|
|
272
|
+
* @param {ExtractOptions} [opts]
|
|
273
|
+
* @returns {WorkflowGraph}
|
|
274
|
+
*/
|
|
275
|
+
export function extractGraph(root, opts) {
|
|
276
|
+
if (!root) {
|
|
277
|
+
return { xml: null, tasks: [], mountedTaskIds: [] };
|
|
278
|
+
}
|
|
279
|
+
const tasks = [];
|
|
280
|
+
const mountedTaskIds = [];
|
|
281
|
+
const seen = new Set();
|
|
282
|
+
const seenRalph = new Set();
|
|
283
|
+
const seenWorktree = new Set();
|
|
284
|
+
const seenSaga = new Set();
|
|
285
|
+
const seenTcf = new Set();
|
|
286
|
+
let ordinal = 0;
|
|
287
|
+
/**
|
|
288
|
+
* @param {Record<string, unknown>} raw
|
|
289
|
+
* @param {string} nodeId
|
|
290
|
+
* @param {Omit<TaskDescriptor, "ordinal" | "nodeId">} descriptor
|
|
291
|
+
*/
|
|
292
|
+
function addDescriptor(raw, nodeId, descriptor) {
|
|
293
|
+
if (seen.has(nodeId)) {
|
|
294
|
+
throw new SmithersError("DUPLICATE_ID", `Duplicate ${String(raw.__smithersKind ?? "Task")} id detected: ${nodeId}`, { id: nodeId });
|
|
295
|
+
}
|
|
296
|
+
seen.add(nodeId);
|
|
297
|
+
tasks.push({ nodeId, ordinal: ordinal++, ...descriptor });
|
|
298
|
+
mountedTaskIds.push(`${nodeId}::${descriptor.iteration}`);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* @param {HostNode} node
|
|
302
|
+
* @param {{ readonly path: readonly number[]; readonly iteration: number; readonly ralphId?: string; readonly parentIsRalph: boolean; readonly parallelStack: readonly { readonly id: string; readonly max?: number }[]; readonly worktreeStack: readonly { readonly id: string; readonly path: string; readonly branch?: string; readonly baseBranch?: string; }[]; readonly loopStack: readonly { readonly ralphId: string; readonly iteration: number }[]; }} ctx
|
|
303
|
+
*/
|
|
304
|
+
function walk(node, ctx) {
|
|
305
|
+
if (node.kind === "text")
|
|
306
|
+
return;
|
|
307
|
+
const raw = node.rawProps ?? {};
|
|
308
|
+
let iteration = ctx.iteration;
|
|
309
|
+
let ralphId = ctx.ralphId;
|
|
310
|
+
let loopStack = ctx.loopStack;
|
|
311
|
+
let nextParallelStack = ctx.parallelStack;
|
|
312
|
+
let nextWorktreeStack = ctx.worktreeStack;
|
|
313
|
+
if (node.tag === "smithers:ralph") {
|
|
314
|
+
if (ctx.parentIsRalph) {
|
|
315
|
+
throw new SmithersError("NESTED_LOOP", "Nested <Ralph> is not supported.");
|
|
316
|
+
}
|
|
317
|
+
const logicalId = resolveStableId(raw.id, "ralph", ctx.path);
|
|
318
|
+
const id = logicalId + buildLoopScope(loopStack);
|
|
319
|
+
if (seenRalph.has(id)) {
|
|
320
|
+
throw new SmithersError("DUPLICATE_ID", `Duplicate Ralph id detected: ${id}`, { kind: "ralph", id });
|
|
321
|
+
}
|
|
322
|
+
seenRalph.add(id);
|
|
323
|
+
ralphId = id;
|
|
324
|
+
iteration = getRalphIteration(opts, id);
|
|
325
|
+
loopStack = [...loopStack, { ralphId: logicalId, iteration }];
|
|
326
|
+
}
|
|
327
|
+
if (node.tag === "smithers:parallel") {
|
|
328
|
+
nextParallelStack = pushGroup("parallel", raw, ctx.path, ctx.parallelStack);
|
|
329
|
+
}
|
|
330
|
+
if (node.tag === "smithers:merge-queue") {
|
|
331
|
+
nextParallelStack = pushGroup("merge-queue", raw, ctx.path, nextParallelStack);
|
|
332
|
+
}
|
|
333
|
+
if (node.tag === "smithers:worktree") {
|
|
334
|
+
const id = resolveStableId(raw.id, "worktree", ctx.path);
|
|
335
|
+
if (seenWorktree.has(id)) {
|
|
336
|
+
throw new SmithersError("DUPLICATE_ID", `Duplicate Worktree id detected: ${id}`, { kind: "worktree", id });
|
|
337
|
+
}
|
|
338
|
+
seenWorktree.add(id);
|
|
339
|
+
const pathVal = String(raw.path ?? "").trim();
|
|
340
|
+
if (!pathVal) {
|
|
341
|
+
throw new SmithersError("WORKTREE_EMPTY_PATH", WORKTREE_EMPTY_PATH_ERROR);
|
|
342
|
+
}
|
|
343
|
+
const base = typeof opts?.baseRootDir === "string" && opts.baseRootDir.length > 0
|
|
344
|
+
? opts.baseRootDir
|
|
345
|
+
: process.cwd();
|
|
346
|
+
nextWorktreeStack = [
|
|
347
|
+
...ctx.worktreeStack,
|
|
348
|
+
{
|
|
349
|
+
id,
|
|
350
|
+
path: isAbsolute(pathVal) ? resolvePath(pathVal) : resolvePath(base, pathVal),
|
|
351
|
+
...(raw.branch ? { branch: String(raw.branch) } : {}),
|
|
352
|
+
...(raw.baseBranch ? { baseBranch: String(raw.baseBranch) } : {}),
|
|
353
|
+
},
|
|
354
|
+
];
|
|
355
|
+
}
|
|
356
|
+
const ancestorScope = loopStack.length > 1 ? buildLoopScope(loopStack.slice(0, -1)) : "";
|
|
357
|
+
const parallelGroup = nextParallelStack[nextParallelStack.length - 1];
|
|
358
|
+
const topWorktree = nextWorktreeStack[nextWorktreeStack.length - 1];
|
|
359
|
+
const common = {
|
|
360
|
+
iteration,
|
|
361
|
+
ralphId,
|
|
362
|
+
worktreeId: topWorktree?.id,
|
|
363
|
+
worktreePath: topWorktree?.path,
|
|
364
|
+
worktreeBranch: topWorktree?.branch,
|
|
365
|
+
worktreeBaseBranch: topWorktree?.baseBranch,
|
|
366
|
+
dependsOn: strings(raw.dependsOn),
|
|
367
|
+
needs: needs(raw.needs),
|
|
368
|
+
parallelGroupId: parallelGroup?.id,
|
|
369
|
+
parallelMaxConcurrency: parallelGroup?.max,
|
|
370
|
+
};
|
|
371
|
+
if (node.tag === "smithers:subflow") {
|
|
372
|
+
const logicalNodeId = requireTaskId(raw, "Subflow");
|
|
373
|
+
const mode = raw.__smithersSubflowMode ?? raw.mode ?? "childRun";
|
|
374
|
+
if (mode !== "inline") {
|
|
375
|
+
const nodeId = logicalNodeId + ancestorScope;
|
|
376
|
+
requireOutput(raw, nodeId, "Subflow");
|
|
377
|
+
const { retries, retryPolicy } = resolveRetryConfig(raw);
|
|
378
|
+
const output = resolveOutput(raw);
|
|
379
|
+
addDescriptor(raw, nodeId, {
|
|
380
|
+
...common,
|
|
381
|
+
...output,
|
|
382
|
+
needsApproval: false,
|
|
383
|
+
skipIf: Boolean(raw.skipIf),
|
|
384
|
+
retries,
|
|
385
|
+
retryPolicy,
|
|
386
|
+
timeoutMs: typeof raw.timeoutMs === "number" ? raw.timeoutMs : null,
|
|
387
|
+
heartbeatTimeoutMs: parseHeartbeatTimeoutMs(raw),
|
|
388
|
+
continueOnFail: Boolean(raw.continueOnFail),
|
|
389
|
+
cachePolicy: raw.cache && typeof raw.cache === "object"
|
|
390
|
+
? raw.cache
|
|
391
|
+
: undefined,
|
|
392
|
+
label: typeof raw.label === "string" ? raw.label : undefined,
|
|
393
|
+
meta: {
|
|
394
|
+
...(raw.meta && typeof raw.meta === "object" && !Array.isArray(raw.meta)
|
|
395
|
+
? raw.meta
|
|
396
|
+
: {}),
|
|
397
|
+
__subflow: true,
|
|
398
|
+
__subflowMode: mode,
|
|
399
|
+
__subflowInput: raw.__smithersSubflowInput,
|
|
400
|
+
__subflowWorkflow: raw.__smithersSubflowWorkflow,
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (node.tag === "smithers:sandbox") {
|
|
406
|
+
const logicalNodeId = requireTaskId(raw, "Sandbox");
|
|
407
|
+
const nodeId = logicalNodeId + ancestorScope;
|
|
408
|
+
requireOutput(raw, nodeId, "Sandbox");
|
|
409
|
+
const { retries, retryPolicy } = resolveRetryConfig(raw);
|
|
410
|
+
const output = resolveOutput(raw);
|
|
411
|
+
const runtime = raw.__smithersSandboxRuntime ?? raw.runtime ?? "bubblewrap";
|
|
412
|
+
addDescriptor(raw, nodeId, {
|
|
413
|
+
...common,
|
|
414
|
+
...output,
|
|
415
|
+
needsApproval: false,
|
|
416
|
+
skipIf: Boolean(raw.skipIf),
|
|
417
|
+
retries,
|
|
418
|
+
retryPolicy,
|
|
419
|
+
timeoutMs: typeof raw.timeoutMs === "number" ? raw.timeoutMs : null,
|
|
420
|
+
heartbeatTimeoutMs: parseHeartbeatTimeoutMs(raw) ?? DEFAULT_SANDBOX_TASK_HEARTBEAT_TIMEOUT_MS,
|
|
421
|
+
continueOnFail: Boolean(raw.continueOnFail),
|
|
422
|
+
cachePolicy: raw.cache && typeof raw.cache === "object"
|
|
423
|
+
? raw.cache
|
|
424
|
+
: undefined,
|
|
425
|
+
label: typeof raw.label === "string" ? raw.label : undefined,
|
|
426
|
+
meta: {
|
|
427
|
+
...(raw.meta && typeof raw.meta === "object" && !Array.isArray(raw.meta)
|
|
428
|
+
? raw.meta
|
|
429
|
+
: {}),
|
|
430
|
+
__sandbox: true,
|
|
431
|
+
__sandboxRuntime: runtime,
|
|
432
|
+
__sandboxInput: raw.__smithersSandboxInput ?? raw.input,
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (node.tag === "smithers:wait-for-event") {
|
|
438
|
+
const logicalNodeId = requireTaskId(raw, "WaitForEvent");
|
|
439
|
+
const nodeId = logicalNodeId + ancestorScope;
|
|
440
|
+
requireOutput(raw, nodeId, "WaitForEvent");
|
|
441
|
+
const output = resolveOutput(raw);
|
|
442
|
+
const onTimeout = raw.__smithersOnTimeout ?? raw.onTimeout ?? "fail";
|
|
443
|
+
addDescriptor(raw, nodeId, {
|
|
444
|
+
...common,
|
|
445
|
+
...output,
|
|
446
|
+
needsApproval: false,
|
|
447
|
+
waitAsync: Boolean(raw.waitAsync),
|
|
448
|
+
skipIf: Boolean(raw.skipIf),
|
|
449
|
+
retries: 0,
|
|
450
|
+
timeoutMs: typeof raw.timeoutMs === "number" ? raw.timeoutMs : null,
|
|
451
|
+
heartbeatTimeoutMs: parseHeartbeatTimeoutMs(raw),
|
|
452
|
+
continueOnFail: onTimeout === "continue" || onTimeout === "skip",
|
|
453
|
+
label: typeof raw.label === "string" ? raw.label : undefined,
|
|
454
|
+
meta: {
|
|
455
|
+
...(raw.meta && typeof raw.meta === "object" && !Array.isArray(raw.meta)
|
|
456
|
+
? raw.meta
|
|
457
|
+
: {}),
|
|
458
|
+
__waitForEvent: true,
|
|
459
|
+
__eventName: raw.__smithersEventName ?? raw.event,
|
|
460
|
+
__correlationId: raw.__smithersCorrelationId ?? raw.correlationId,
|
|
461
|
+
__onTimeout: onTimeout,
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (node.tag === "smithers:timer") {
|
|
466
|
+
const logicalNodeId = requireTaskId(raw, "Timer");
|
|
467
|
+
if (logicalNodeId.length > 256) {
|
|
468
|
+
throw new SmithersError("INVALID_INPUT", `Timer id must be 256 characters or fewer (received ${logicalNodeId.length}).`, { nodeId: logicalNodeId, maxLength: 256 });
|
|
469
|
+
}
|
|
470
|
+
const nodeId = logicalNodeId + ancestorScope;
|
|
471
|
+
const duration = typeof (raw.__smithersTimerDuration ?? raw.duration) === "string"
|
|
472
|
+
? String(raw.__smithersTimerDuration ?? raw.duration).trim()
|
|
473
|
+
: "";
|
|
474
|
+
const untilRaw = raw.__smithersTimerUntil ?? raw.until;
|
|
475
|
+
const until = typeof untilRaw === "string"
|
|
476
|
+
? untilRaw.trim()
|
|
477
|
+
: untilRaw instanceof Date
|
|
478
|
+
? untilRaw.toISOString()
|
|
479
|
+
: "";
|
|
480
|
+
const hasDuration = duration.length > 0;
|
|
481
|
+
const hasUntil = until.length > 0;
|
|
482
|
+
if ((hasDuration ? 1 : 0) + (hasUntil ? 1 : 0) !== 1) {
|
|
483
|
+
throw new SmithersError("INVALID_INPUT", `Timer ${nodeId} must define exactly one of duration or until.`, { nodeId, duration: raw.duration, until: raw.until });
|
|
484
|
+
}
|
|
485
|
+
if (raw.every !== undefined) {
|
|
486
|
+
throw new SmithersError("INVALID_INPUT", `Timer ${nodeId} uses every=, but recurring timers are not supported yet.`, { nodeId, every: raw.every });
|
|
487
|
+
}
|
|
488
|
+
addDescriptor(raw, nodeId, {
|
|
489
|
+
...common,
|
|
490
|
+
outputTable: null,
|
|
491
|
+
outputTableName: "",
|
|
492
|
+
outputRef: undefined,
|
|
493
|
+
outputSchema: undefined,
|
|
494
|
+
needsApproval: false,
|
|
495
|
+
skipIf: Boolean(raw.skipIf),
|
|
496
|
+
retries: 0,
|
|
497
|
+
timeoutMs: null,
|
|
498
|
+
heartbeatTimeoutMs: null,
|
|
499
|
+
continueOnFail: false,
|
|
500
|
+
label: typeof raw.label === "string" ? raw.label : `timer:${nodeId}`,
|
|
501
|
+
meta: {
|
|
502
|
+
...(raw.meta && typeof raw.meta === "object" && !Array.isArray(raw.meta)
|
|
503
|
+
? raw.meta
|
|
504
|
+
: {}),
|
|
505
|
+
__timer: true,
|
|
506
|
+
__timerType: hasDuration ? "duration" : "absolute",
|
|
507
|
+
...(hasDuration ? { __timerDuration: duration } : {}),
|
|
508
|
+
...(hasUntil ? { __timerUntil: until } : {}),
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
if (node.tag === "smithers:saga") {
|
|
513
|
+
const id = resolveStableId(raw.id, "saga", ctx.path);
|
|
514
|
+
if (seenSaga.has(id)) {
|
|
515
|
+
throw new SmithersError("DUPLICATE_ID", `Duplicate Saga id detected: ${id}`, { kind: "saga", id });
|
|
516
|
+
}
|
|
517
|
+
seenSaga.add(id);
|
|
518
|
+
}
|
|
519
|
+
if (node.tag === "smithers:try-catch-finally") {
|
|
520
|
+
const id = resolveStableId(raw.id, "tcf", ctx.path);
|
|
521
|
+
if (seenTcf.has(id)) {
|
|
522
|
+
throw new SmithersError("DUPLICATE_ID", `Duplicate TryCatchFinally id detected: ${id}`, { kind: "try-catch-finally", id });
|
|
523
|
+
}
|
|
524
|
+
seenTcf.add(id);
|
|
525
|
+
}
|
|
526
|
+
if (node.tag === "smithers:task") {
|
|
527
|
+
const logicalNodeId = requireTaskId(raw, "Task");
|
|
528
|
+
const nodeId = logicalNodeId + ancestorScope;
|
|
529
|
+
requireOutput(raw, nodeId, "Task");
|
|
530
|
+
const output = resolveOutput(raw);
|
|
531
|
+
const approvalMode = raw.approvalMode === "decision" ||
|
|
532
|
+
raw.approvalMode === "select" ||
|
|
533
|
+
raw.approvalMode === "rank"
|
|
534
|
+
? raw.approvalMode
|
|
535
|
+
: "gate";
|
|
536
|
+
const approvalOnDeny = raw.approvalOnDeny === "continue" ||
|
|
537
|
+
raw.approvalOnDeny === "skip" ||
|
|
538
|
+
raw.approvalOnDeny === "fail"
|
|
539
|
+
? raw.approvalOnDeny
|
|
540
|
+
: undefined;
|
|
541
|
+
const { retries, retryPolicy } = resolveRetryConfig(raw);
|
|
542
|
+
const kind = raw.__smithersKind;
|
|
543
|
+
const isAgent = kind === "agent" || Boolean(raw.agent);
|
|
544
|
+
const isCompute = kind === "compute" && typeof raw.__smithersComputeFn === "function";
|
|
545
|
+
const parsedHeartbeatTimeoutMs = parseHeartbeatTimeoutMs(raw);
|
|
546
|
+
const heartbeatTimeoutMs = parsedHeartbeatTimeoutMs ??
|
|
547
|
+
(isAgent ? DEFAULT_LOCAL_TASK_HEARTBEAT_TIMEOUT_MS : null);
|
|
548
|
+
const prompt = isAgent ? String(raw.children ?? "") : undefined;
|
|
549
|
+
if (prompt === "[object Object]") {
|
|
550
|
+
throw new SmithersError("MDX_PRELOAD_INACTIVE", `Task "${logicalNodeId}" prompt resolved to [object Object].`);
|
|
551
|
+
}
|
|
552
|
+
addDescriptor(raw, nodeId, {
|
|
553
|
+
...common,
|
|
554
|
+
...output,
|
|
555
|
+
needsApproval: Boolean(raw.needsApproval),
|
|
556
|
+
waitAsync: Boolean(raw.waitAsync),
|
|
557
|
+
approvalMode,
|
|
558
|
+
approvalOnDeny,
|
|
559
|
+
approvalOptions: approvalOptions(raw.approvalOptions),
|
|
560
|
+
approvalAllowedScopes: strings(raw.approvalAllowedScopes),
|
|
561
|
+
approvalAllowedUsers: strings(raw.approvalAllowedUsers),
|
|
562
|
+
approvalAutoApprove: approvalAutoApprove(raw.approvalAutoApprove),
|
|
563
|
+
skipIf: Boolean(raw.skipIf),
|
|
564
|
+
retries,
|
|
565
|
+
retryPolicy,
|
|
566
|
+
timeoutMs: typeof raw.timeoutMs === "number" ? raw.timeoutMs : null,
|
|
567
|
+
heartbeatTimeoutMs,
|
|
568
|
+
continueOnFail: Boolean(raw.continueOnFail),
|
|
569
|
+
cachePolicy: raw.cache && typeof raw.cache === "object"
|
|
570
|
+
? raw.cache
|
|
571
|
+
: undefined,
|
|
572
|
+
agent: raw.agent,
|
|
573
|
+
prompt,
|
|
574
|
+
staticPayload: isAgent || isCompute
|
|
575
|
+
? undefined
|
|
576
|
+
: (raw.__smithersPayload ?? raw.__payload ?? raw.children),
|
|
577
|
+
computeFn: isCompute
|
|
578
|
+
? raw.__smithersComputeFn
|
|
579
|
+
: undefined,
|
|
580
|
+
label: typeof raw.label === "string" ? raw.label : undefined,
|
|
581
|
+
meta: raw.meta && typeof raw.meta === "object" && !Array.isArray(raw.meta)
|
|
582
|
+
? raw.meta
|
|
583
|
+
: undefined,
|
|
584
|
+
scorers: raw.scorers && typeof raw.scorers === "object" && !Array.isArray(raw.scorers)
|
|
585
|
+
? raw.scorers
|
|
586
|
+
: undefined,
|
|
587
|
+
memoryConfig: raw.memory && typeof raw.memory === "object" && !Array.isArray(raw.memory)
|
|
588
|
+
? raw.memory
|
|
589
|
+
: undefined,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
let elementIndex = 0;
|
|
593
|
+
for (const child of node.children) {
|
|
594
|
+
const nextPath = child.kind === "element" ? [...ctx.path, elementIndex++] : ctx.path;
|
|
595
|
+
walk(child, {
|
|
596
|
+
path: nextPath,
|
|
597
|
+
iteration,
|
|
598
|
+
ralphId,
|
|
599
|
+
parentIsRalph: node.tag === "smithers:ralph",
|
|
600
|
+
parallelStack: nextParallelStack,
|
|
601
|
+
worktreeStack: nextWorktreeStack,
|
|
602
|
+
loopStack,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
walk(root, {
|
|
607
|
+
path: [],
|
|
608
|
+
iteration: 0,
|
|
609
|
+
parentIsRalph: false,
|
|
610
|
+
parallelStack: [],
|
|
611
|
+
worktreeStack: [],
|
|
612
|
+
loopStack: [],
|
|
613
|
+
});
|
|
614
|
+
return { xml: toXmlNode(root), tasks, mountedTaskIds };
|
|
615
|
+
}
|
|
616
|
+
export const extractFromHost = extractGraph;
|