botmux-workflow-core 3.10.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/README.md +100 -0
- package/dist/cjs/control.cjs +80 -0
- package/dist/cjs/control.cjs.map +7 -0
- package/dist/cjs/engine.cjs +380 -0
- package/dist/cjs/engine.cjs.map +7 -0
- package/dist/cjs/events.cjs +19 -0
- package/dist/cjs/events.cjs.map +7 -0
- package/dist/cjs/gate-policy.cjs +55 -0
- package/dist/cjs/gate-policy.cjs.map +7 -0
- package/dist/cjs/host-bindings.cjs +285 -0
- package/dist/cjs/host-bindings.cjs.map +7 -0
- package/dist/cjs/host-contract.cjs +19 -0
- package/dist/cjs/host-contract.cjs.map +7 -0
- package/dist/cjs/index.cjs +1523 -0
- package/dist/cjs/index.cjs.map +7 -0
- package/dist/cjs/runtime.cjs +6393 -0
- package/dist/cjs/runtime.cjs.map +7 -0
- package/dist/cjs/schema.cjs +1190 -0
- package/dist/cjs/schema.cjs.map +7 -0
- package/dist/esm/control.js +52 -0
- package/dist/esm/control.js.map +7 -0
- package/dist/esm/engine.js +352 -0
- package/dist/esm/engine.js.map +7 -0
- package/dist/esm/events.js +1 -0
- package/dist/esm/events.js.map +7 -0
- package/dist/esm/gate-policy.js +26 -0
- package/dist/esm/gate-policy.js.map +7 -0
- package/dist/esm/host-bindings.js +252 -0
- package/dist/esm/host-bindings.js.map +7 -0
- package/dist/esm/host-contract.js +1 -0
- package/dist/esm/host-contract.js.map +7 -0
- package/dist/esm/index.js +1481 -0
- package/dist/esm/index.js.map +7 -0
- package/dist/esm/runtime.js +6426 -0
- package/dist/esm/runtime.js.map +7 -0
- package/dist/esm/schema.js +1140 -0
- package/dist/esm/schema.js.map +7 -0
- package/dist/types/packages/workflow-core/src/control.d.ts +1 -0
- package/dist/types/packages/workflow-core/src/engine.d.ts +1 -0
- package/dist/types/packages/workflow-core/src/events.d.ts +1 -0
- package/dist/types/packages/workflow-core/src/gate-policy.d.ts +1 -0
- package/dist/types/packages/workflow-core/src/host-bindings.d.ts +1 -0
- package/dist/types/packages/workflow-core/src/host-contract.d.ts +1 -0
- package/dist/types/packages/workflow-core/src/index.d.ts +1 -0
- package/dist/types/packages/workflow-core/src/runtime.d.ts +7 -0
- package/dist/types/packages/workflow-core/src/schema.d.ts +1 -0
- package/dist/types/src/workflows/v3/artifact-contract.d.ts +40 -0
- package/dist/types/src/workflows/v3/core-control.d.ts +15 -0
- package/dist/types/src/workflows/v3/dag.d.ts +341 -0
- package/dist/types/src/workflows/v3/event-contract.d.ts +272 -0
- package/dist/types/src/workflows/v3/gate-policy.d.ts +11 -0
- package/dist/types/src/workflows/v3/host-bindings.d.ts +39 -0
- package/dist/types/src/workflows/v3/in-process-attempt-lease.d.ts +9 -0
- package/dist/types/src/workflows/v3/orchestrator.d.ts +174 -0
- package/dist/types/src/workflows/v3/portable-final-outputs.d.ts +36 -0
- package/dist/types/src/workflows/v3/portable-runtime.d.ts +82 -0
- package/dist/types/src/workflows/v3/runtime-host-contract.d.ts +181 -0
- package/dist/types/src/workflows/v3/shared-runtime.d.ts +16 -0
- package/package.json +83 -0
|
@@ -0,0 +1,1190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// packages/workflow-core/src/schema.ts
|
|
21
|
+
var schema_exports = {};
|
|
22
|
+
__export(schema_exports, {
|
|
23
|
+
DEFAULT_HUMAN_GATE_OPTIONS: () => DEFAULT_HUMAN_GATE_OPTIONS,
|
|
24
|
+
DEFAULT_NODE_TIMEOUT_SEC: () => DEFAULT_NODE_TIMEOUT_SEC,
|
|
25
|
+
DEFAULT_REVISIT_BUDGET_PER_PAIR: () => DEFAULT_REVISIT_BUDGET_PER_PAIR,
|
|
26
|
+
DEFAULT_REVISIT_BUDGET_PER_RUN: () => DEFAULT_REVISIT_BUDGET_PER_RUN,
|
|
27
|
+
DagValidationError: () => DagValidationError,
|
|
28
|
+
MAX_HUMAN_GATE_OPTIONS: () => MAX_HUMAN_GATE_OPTIONS,
|
|
29
|
+
MAX_HUMAN_GATE_OPTION_LENGTH: () => MAX_HUMAN_GATE_OPTION_LENGTH,
|
|
30
|
+
MAX_LOOP_ITERATIONS: () => MAX_LOOP_ITERATIONS,
|
|
31
|
+
MAX_NODE_TIMEOUT_SEC: () => MAX_NODE_TIMEOUT_SEC,
|
|
32
|
+
MAX_OVERRIDE_MODEL_LENGTH: () => MAX_OVERRIDE_MODEL_LENGTH,
|
|
33
|
+
MAX_OVERRIDE_SYSTEM_PROMPT_APPEND: () => MAX_OVERRIDE_SYSTEM_PROMPT_APPEND,
|
|
34
|
+
NODE_KINDS: () => NODE_KINDS,
|
|
35
|
+
RESULT_ENUM_MAX_VALUES: () => RESULT_ENUM_MAX_VALUES,
|
|
36
|
+
RESULT_ENUM_MAX_VALUE_LENGTH: () => RESULT_ENUM_MAX_VALUE_LENGTH,
|
|
37
|
+
RESULT_SCHEMA_MAX_BYTES: () => RESULT_SCHEMA_MAX_BYTES,
|
|
38
|
+
RESULT_SCHEMA_MAX_PROPERTIES: () => RESULT_SCHEMA_MAX_PROPERTIES,
|
|
39
|
+
V3_DAG_SEGMENT_RE: () => V3_DAG_SEGMENT_RE,
|
|
40
|
+
V3_HOST_EXECUTORS: () => V3_HOST_EXECUTORS,
|
|
41
|
+
isGoalNode: () => isGoalNode,
|
|
42
|
+
isHostNode: () => isHostNode,
|
|
43
|
+
isLoopNode: () => isLoopNode,
|
|
44
|
+
loopInstanceId: () => loopInstanceId,
|
|
45
|
+
topologicalOrder: () => topologicalOrder,
|
|
46
|
+
validateDag: () => validateDag
|
|
47
|
+
});
|
|
48
|
+
module.exports = __toCommonJS(schema_exports);
|
|
49
|
+
|
|
50
|
+
// src/workflows/v3/host-bindings.ts
|
|
51
|
+
var V3HostBindingError = class extends Error {
|
|
52
|
+
constructor(message) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "V3HostBindingError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var RESULT_MARKER = ".result.";
|
|
58
|
+
var PATH_SEGMENT_RE = /^[A-Za-z0-9_-]+$/;
|
|
59
|
+
var FORBIDDEN_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
60
|
+
var STRING_MARKER_SOURCE = "\\$\\{([^{}]+)\\}";
|
|
61
|
+
var stringMarkerRe = () => new RegExp(STRING_MARKER_SOURCE, "g");
|
|
62
|
+
function parseV3HostBindingRef(ref) {
|
|
63
|
+
if (ref.startsWith("params.")) {
|
|
64
|
+
return { kind: "params", path: parsePath(ref.slice("params.".length), ref) };
|
|
65
|
+
}
|
|
66
|
+
if (ref.startsWith("context.")) {
|
|
67
|
+
return { kind: "context", path: parsePath(ref.slice("context.".length), ref) };
|
|
68
|
+
}
|
|
69
|
+
const resultAt = ref.indexOf(RESULT_MARKER);
|
|
70
|
+
if (resultAt <= 0) {
|
|
71
|
+
throw new V3HostBindingError(
|
|
72
|
+
`unsupported host binding ${JSON.stringify(ref)}; expected params.<path>, context.<path>, or <nodeId>.result.<path>`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const nodeId = ref.slice(0, resultAt);
|
|
76
|
+
if (!/^[A-Za-z0-9._-]+$/.test(nodeId)) {
|
|
77
|
+
throw new V3HostBindingError(`host binding ${JSON.stringify(ref)} has an invalid node id`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
kind: "result",
|
|
81
|
+
nodeId,
|
|
82
|
+
path: parsePath(ref.slice(resultAt + RESULT_MARKER.length), ref)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function parsePath(raw, ref) {
|
|
86
|
+
if (!raw) throw new V3HostBindingError(`host binding ${JSON.stringify(ref)} has an empty path`);
|
|
87
|
+
const path = raw.split(".");
|
|
88
|
+
for (const segment of path) {
|
|
89
|
+
if (!PATH_SEGMENT_RE.test(segment) || FORBIDDEN_SEGMENTS.has(segment)) {
|
|
90
|
+
throw new V3HostBindingError(
|
|
91
|
+
`host binding ${JSON.stringify(ref)} has unsafe/invalid path segment ${JSON.stringify(segment)}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return path;
|
|
96
|
+
}
|
|
97
|
+
function isExactRefObject(value) {
|
|
98
|
+
if (!isRecord(value)) return false;
|
|
99
|
+
const keys = Object.keys(value);
|
|
100
|
+
return keys.length === 1 && keys[0] === "$ref" && typeof value.$ref === "string";
|
|
101
|
+
}
|
|
102
|
+
function collectV3HostBindingRefs(template) {
|
|
103
|
+
const refs = [];
|
|
104
|
+
const seen = /* @__PURE__ */ new Set();
|
|
105
|
+
const add = (raw) => {
|
|
106
|
+
const parsed = parseV3HostBindingRef(raw);
|
|
107
|
+
const key = JSON.stringify(parsed);
|
|
108
|
+
if (!seen.has(key)) {
|
|
109
|
+
seen.add(key);
|
|
110
|
+
refs.push(parsed);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const walk = (value, path) => {
|
|
114
|
+
if (typeof value === "string") {
|
|
115
|
+
if (!value.includes("${")) return;
|
|
116
|
+
const covered = new Array(value.length).fill(false);
|
|
117
|
+
const markerRe = stringMarkerRe();
|
|
118
|
+
for (let match = markerRe.exec(value); match; match = markerRe.exec(value)) {
|
|
119
|
+
add(match[1]);
|
|
120
|
+
for (let i = match.index; i < match.index + match[0].length; i++) covered[i] = true;
|
|
121
|
+
}
|
|
122
|
+
for (let i = value.indexOf("${"); i >= 0; i = value.indexOf("${", i + 2)) {
|
|
123
|
+
if (!covered[i]) throw new V3HostBindingError(`malformed host binding marker at ${path}`);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (Array.isArray(value)) {
|
|
128
|
+
value.forEach((child, index) => walk(child, `${path}[${index}]`));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!isRecord(value)) {
|
|
132
|
+
if (value === null || typeof value === "number" || typeof value === "boolean") return;
|
|
133
|
+
throw new V3HostBindingError(`host input at ${path} must be finite JSON`);
|
|
134
|
+
}
|
|
135
|
+
if (Object.prototype.hasOwnProperty.call(value, "$ref")) {
|
|
136
|
+
if (!isExactRefObject(value)) {
|
|
137
|
+
throw new V3HostBindingError(
|
|
138
|
+
`host input at ${path} must use $ref as the exact object { "$ref": "..." }`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
add(value.$ref);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
for (const [key, child] of Object.entries(value)) {
|
|
145
|
+
if (FORBIDDEN_SEGMENTS.has(key)) {
|
|
146
|
+
throw new V3HostBindingError(`host input at ${path} uses forbidden key ${JSON.stringify(key)}`);
|
|
147
|
+
}
|
|
148
|
+
walk(child, `${path}.${key}`);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
walk(template, "input");
|
|
152
|
+
return refs;
|
|
153
|
+
}
|
|
154
|
+
function isRecord(value) {
|
|
155
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/workflows/v3/dag.ts
|
|
159
|
+
var NODE_KINDS = ["goal", "host", "loop"];
|
|
160
|
+
var V3_HOST_EXECUTORS = ["feishu-send", "feishu-reply", "botmux-schedule"];
|
|
161
|
+
var DEFAULT_NODE_TIMEOUT_SEC = 1800;
|
|
162
|
+
var MAX_NODE_TIMEOUT_SEC = 14400;
|
|
163
|
+
var DEFAULT_HUMAN_GATE_OPTIONS = ["approve", "reject"];
|
|
164
|
+
var MAX_HUMAN_GATE_OPTIONS = 8;
|
|
165
|
+
var MAX_HUMAN_GATE_OPTION_LENGTH = 32;
|
|
166
|
+
var MAX_OVERRIDE_MODEL_LENGTH = 64;
|
|
167
|
+
var MAX_OVERRIDE_SYSTEM_PROMPT_APPEND = 8e3;
|
|
168
|
+
var RESULT_FIELD_TYPES = ["string", "number", "boolean", "array", "object"];
|
|
169
|
+
var RESULT_SCHEMA_MAX_PROPERTIES = 32;
|
|
170
|
+
var RESULT_SCHEMA_MAX_BYTES = 4096;
|
|
171
|
+
var RESULT_ENUM_MAX_VALUES = 16;
|
|
172
|
+
var RESULT_ENUM_MAX_VALUE_LENGTH = 64;
|
|
173
|
+
var MAX_LOOP_ITERATIONS = 20;
|
|
174
|
+
var DEFAULT_REVISIT_BUDGET_PER_PAIR = 1;
|
|
175
|
+
var DEFAULT_REVISIT_BUDGET_PER_RUN = 8;
|
|
176
|
+
var LOOP_WHEN_OPERATORS = ["equals", "notEquals", "gt", "gte", "lt", "lte"];
|
|
177
|
+
function isGoalNode(node) {
|
|
178
|
+
return node.type === "goal" && typeof node.goal === "string" && node.goal.length > 0;
|
|
179
|
+
}
|
|
180
|
+
function isHostNode(node) {
|
|
181
|
+
return node.type === "host" && typeof node.executor === "string" && V3_HOST_EXECUTORS.includes(node.executor) && node.humanGate !== null;
|
|
182
|
+
}
|
|
183
|
+
function isLoopNode(node) {
|
|
184
|
+
return node.type === "loop";
|
|
185
|
+
}
|
|
186
|
+
function loopInstanceId(loopId, iteration, bodyNodeId) {
|
|
187
|
+
return `${loopId}.i${String(iteration).padStart(3, "0")}.${bodyNodeId}`;
|
|
188
|
+
}
|
|
189
|
+
var DagValidationError = class extends Error {
|
|
190
|
+
constructor(problems) {
|
|
191
|
+
super(`Invalid v3 dag.json:
|
|
192
|
+
- ${problems.join("\n - ")}`);
|
|
193
|
+
this.problems = problems;
|
|
194
|
+
this.name = "DagValidationError";
|
|
195
|
+
}
|
|
196
|
+
problems;
|
|
197
|
+
};
|
|
198
|
+
var V3_DAG_SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
|
|
199
|
+
var SEGMENT_RE = V3_DAG_SEGMENT_RE;
|
|
200
|
+
function isObject(v) {
|
|
201
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
202
|
+
}
|
|
203
|
+
function validateDag(raw) {
|
|
204
|
+
const problems = [];
|
|
205
|
+
if (!isObject(raw)) {
|
|
206
|
+
throw new DagValidationError(["root must be a JSON object"]);
|
|
207
|
+
}
|
|
208
|
+
if (typeof raw.runId !== "string" || !SEGMENT_RE.test(raw.runId)) {
|
|
209
|
+
problems.push(`runId must be a path-safe string matching ${SEGMENT_RE} (got ${JSON.stringify(raw.runId)})`);
|
|
210
|
+
}
|
|
211
|
+
if (!Array.isArray(raw.nodes) || raw.nodes.length === 0) {
|
|
212
|
+
throw new DagValidationError([...problems, "nodes must be a non-empty array"]);
|
|
213
|
+
}
|
|
214
|
+
const ids = /* @__PURE__ */ new Set();
|
|
215
|
+
const nodes = [];
|
|
216
|
+
const pendingWhens = [];
|
|
217
|
+
for (let i = 0; i < raw.nodes.length; i++) {
|
|
218
|
+
const n = raw.nodes[i];
|
|
219
|
+
const where = `nodes[${i}]`;
|
|
220
|
+
if (!isObject(n)) {
|
|
221
|
+
problems.push(`${where} must be an object`);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const id = n.id;
|
|
225
|
+
if (typeof id !== "string" || !SEGMENT_RE.test(id)) {
|
|
226
|
+
problems.push(`${where}.id must be a path-safe string matching ${SEGMENT_RE} (got ${JSON.stringify(id)})`);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (ids.has(id)) {
|
|
230
|
+
problems.push(`duplicate node id "${id}"`);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
ids.add(id);
|
|
234
|
+
const type = n.type;
|
|
235
|
+
if (type !== "goal" && type !== "host" && type !== "loop") {
|
|
236
|
+
problems.push(`node "${id}".type must be one of ${NODE_KINDS.join(" | ")} (got ${JSON.stringify(type)})`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const depends = normDepends(n.depends, `node "${id}"`, problems, { ownerId: id, list: pendingWhens });
|
|
240
|
+
const fromList = depends.map((d) => d.from);
|
|
241
|
+
if (fromList.includes(id)) problems.push(`node "${id}" depends on itself`);
|
|
242
|
+
if (new Set(fromList).size !== fromList.length) problems.push(`node "${id}".depends has duplicates`);
|
|
243
|
+
const triggerRule = normTriggerRule(n.triggerRule, depends.length, `node "${id}"`, problems);
|
|
244
|
+
const inputs = normInputs(n.inputs, id, problems);
|
|
245
|
+
if (type === "loop") {
|
|
246
|
+
const loopFields = normLoopFields(n, id, problems);
|
|
247
|
+
if (loopFields) {
|
|
248
|
+
nodes.push({
|
|
249
|
+
id,
|
|
250
|
+
type,
|
|
251
|
+
goal: typeof n.goal === "string" ? n.goal : void 0,
|
|
252
|
+
bot: typeof n.bot === "string" ? n.bot : void 0,
|
|
253
|
+
depends,
|
|
254
|
+
triggerRule,
|
|
255
|
+
inputs,
|
|
256
|
+
humanGate: null,
|
|
257
|
+
...loopFields
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (type === "host") {
|
|
263
|
+
if (n.goal !== void 0) problems.push(`host node "${id}".goal is not supported`);
|
|
264
|
+
if (n.bot !== void 0) problems.push(`host node "${id}".bot is not supported \u2014 host nodes do not spawn a CLI`);
|
|
265
|
+
if (n.override !== void 0) problems.push(`host node "${id}".override is not supported`);
|
|
266
|
+
if (n.revisitTo !== void 0) problems.push(`host node "${id}".revisitTo is not supported`);
|
|
267
|
+
if (n.resultSchema !== void 0) {
|
|
268
|
+
problems.push(`host node "${id}".resultSchema is not supported \u2014 host output uses the trusted executor result contract`);
|
|
269
|
+
}
|
|
270
|
+
if (inputs.length > 0) {
|
|
271
|
+
problems.push(`host node "${id}".inputs must be empty \u2014 use typed bindings in host input`);
|
|
272
|
+
}
|
|
273
|
+
if (triggerRule !== void 0 && triggerRule !== "all_success") {
|
|
274
|
+
problems.push(
|
|
275
|
+
`host node "${id}".triggerRule must be "all_success"; P0 host bindings do not accept skipped/omitted dependencies`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const executor = typeof n.executor === "string" ? n.executor : "";
|
|
279
|
+
if (!V3_HOST_EXECUTORS.includes(executor)) {
|
|
280
|
+
problems.push(
|
|
281
|
+
`host node "${id}".executor must be one of ${V3_HOST_EXECUTORS.join(" | ")} (got ${JSON.stringify(n.executor)})`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
if (!Object.prototype.hasOwnProperty.call(n, "input")) {
|
|
285
|
+
problems.push(`host node "${id}".input is required`);
|
|
286
|
+
} else {
|
|
287
|
+
validateHostInputShape(executor, n.input, id, problems);
|
|
288
|
+
try {
|
|
289
|
+
collectV3HostBindingRefs(n.input);
|
|
290
|
+
} catch (err) {
|
|
291
|
+
problems.push(
|
|
292
|
+
`host node "${id}".input is invalid: ${err instanceof V3HostBindingError ? err.message : String(err)}`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (n.timeoutSec !== void 0) {
|
|
297
|
+
problems.push(
|
|
298
|
+
`host node "${id}".timeoutSec is not supported \u2014 abandoning an in-flight provider call would make its effect outcome unknown`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
const humanGate2 = normHumanGate(n.humanGate, `host node "${id}"`, problems);
|
|
302
|
+
if (!humanGate2) {
|
|
303
|
+
problems.push(
|
|
304
|
+
`host node "${id}" must declare a humanGate; v3 P0 does not allow ungated external side effects`
|
|
305
|
+
);
|
|
306
|
+
} else {
|
|
307
|
+
if (!humanGate2.options?.includes("approve")) {
|
|
308
|
+
problems.push(
|
|
309
|
+
`host node "${id}".humanGate.options must include "approve" explicitly; host side effects cannot use an implicit first-option approval`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
if (humanGate2.approveOptions?.length !== 1 || humanGate2.approveOptions[0] !== "approve") {
|
|
313
|
+
problems.push(
|
|
314
|
+
`host node "${id}".humanGate.approveOptions must be exactly ["approve"]; custom-labelled choices cannot authorize a host side effect`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
nodes.push({
|
|
319
|
+
id,
|
|
320
|
+
type,
|
|
321
|
+
executor,
|
|
322
|
+
input: n.input,
|
|
323
|
+
depends,
|
|
324
|
+
triggerRule,
|
|
325
|
+
inputs: [],
|
|
326
|
+
humanGate: humanGate2
|
|
327
|
+
});
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (typeof n.goal !== "string" || n.goal.trim() === "") {
|
|
331
|
+
problems.push(`goal node "${id}".goal must be a non-empty string`);
|
|
332
|
+
}
|
|
333
|
+
const timeoutSec = normTimeoutSec(n.timeoutSec, `node "${id}"`, problems);
|
|
334
|
+
const resultSchema = normResultSchema(n.resultSchema, id, problems);
|
|
335
|
+
const humanGate = normHumanGate(n.humanGate, `node "${id}"`, problems);
|
|
336
|
+
const override = normOverride(n.override, `node "${id}"`, problems);
|
|
337
|
+
const revisitTo = normRevisitTo(n.revisitTo, id, problems);
|
|
338
|
+
nodes.push({
|
|
339
|
+
id,
|
|
340
|
+
type,
|
|
341
|
+
goal: typeof n.goal === "string" ? n.goal : void 0,
|
|
342
|
+
bot: typeof n.bot === "string" ? n.bot : void 0,
|
|
343
|
+
depends,
|
|
344
|
+
triggerRule,
|
|
345
|
+
override,
|
|
346
|
+
inputs,
|
|
347
|
+
timeoutSec,
|
|
348
|
+
humanGate,
|
|
349
|
+
resultSchema,
|
|
350
|
+
...revisitTo ? { revisitTo } : {}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
for (const node of nodes) {
|
|
354
|
+
for (const dep of node.depends) {
|
|
355
|
+
if (!ids.has(dep.from)) problems.push(`node "${node.id}" depends on unknown node "${dep.from}"`);
|
|
356
|
+
}
|
|
357
|
+
for (const inp of node.inputs) {
|
|
358
|
+
if (!ids.has(inp.from)) {
|
|
359
|
+
problems.push(`node "${node.id}".inputs references unknown node "${inp.from}"`);
|
|
360
|
+
} else if (!node.depends.some((d) => d.from === inp.from)) {
|
|
361
|
+
problems.push(`node "${node.id}".inputs.from "${inp.from}" must also be in depends`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (node.type === "host") {
|
|
365
|
+
try {
|
|
366
|
+
for (const ref of collectV3HostBindingRefs(node.input)) {
|
|
367
|
+
if (ref.kind !== "result") continue;
|
|
368
|
+
if (!ids.has(ref.nodeId)) {
|
|
369
|
+
problems.push(`host node "${node.id}".input references unknown result node "${ref.nodeId}"`);
|
|
370
|
+
} else if (!node.depends.some((dep) => dep.from === ref.nodeId)) {
|
|
371
|
+
problems.push(
|
|
372
|
+
`host node "${node.id}".input result source "${ref.nodeId}" must also be in depends`
|
|
373
|
+
);
|
|
374
|
+
} else if (!node.depends.some((dep) => dep.from === ref.nodeId && dep.when === void 0)) {
|
|
375
|
+
problems.push(
|
|
376
|
+
`host node "${node.id}".input result source "${ref.nodeId}" must use an unconditional depends edge; P0 host bindings do not accept omitted conditional inputs`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
} catch {
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (node.revisitTo && node.revisitTo.length > 0) {
|
|
384
|
+
const ancestors = ancestorsOf(node.id, nodes);
|
|
385
|
+
for (const target of node.revisitTo) {
|
|
386
|
+
if (!ids.has(target)) {
|
|
387
|
+
problems.push(`node "${node.id}".revisitTo references unknown node "${target}"`);
|
|
388
|
+
} else if (target === node.id) {
|
|
389
|
+
problems.push(`node "${node.id}".revisitTo cannot point at itself`);
|
|
390
|
+
} else if (!ancestors.has(target)) {
|
|
391
|
+
problems.push(
|
|
392
|
+
`node "${node.id}".revisitTo "${target}" must be an ancestor (reachable via depends) \u2014 revisit only jumps backward`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
for (const requester of nodes) {
|
|
399
|
+
for (const target of requester.revisitTo ?? []) {
|
|
400
|
+
const cone = downstreamCone(target, nodes);
|
|
401
|
+
for (const nodeId of cone) {
|
|
402
|
+
if (nodeByIdUnsafe(nodes, nodeId)?.type === "host") {
|
|
403
|
+
problems.push(
|
|
404
|
+
`node "${requester.id}".revisitTo "${target}" would replay host node "${nodeId}"; host nodes are not allowed in a revisit cone`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const nodeById = new Map(nodes.map((nn) => [nn.id, nn]));
|
|
411
|
+
for (const pw of pendingWhens) {
|
|
412
|
+
const source = nodeById.get(pw.ref.from);
|
|
413
|
+
if (!source) continue;
|
|
414
|
+
if (source.type !== "goal") {
|
|
415
|
+
problems.push(
|
|
416
|
+
`${pw.where}: conditional edge source "${pw.ref.from}" must be a goal node (P0 forbids loop sources \u2014 add a verifier goal after the loop and branch on ITS result)`
|
|
417
|
+
);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (!source.resultSchema) {
|
|
421
|
+
problems.push(
|
|
422
|
+
`${pw.where}: conditional edge source "${pw.ref.from}" must declare a resultSchema \u2014 the predicate reads its structured result`
|
|
423
|
+
);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const when = normLoopExitWhen(pw.raw, source.resultSchema, pw.where, problems);
|
|
427
|
+
if (when) pw.ref.when = when;
|
|
428
|
+
}
|
|
429
|
+
for (const node of nodes) {
|
|
430
|
+
if (node.type !== "loop") continue;
|
|
431
|
+
for (const other of ids) {
|
|
432
|
+
if (other !== node.id && other.startsWith(`${node.id}.`)) {
|
|
433
|
+
problems.push(
|
|
434
|
+
`node id "${other}" collides with loop "${node.id}" expansion namespace ("${node.id}.*")`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (problems.length > 0) throw new DagValidationError(problems);
|
|
440
|
+
const dag = { runId: raw.runId, nodes };
|
|
441
|
+
topologicalOrder(dag);
|
|
442
|
+
return dag;
|
|
443
|
+
}
|
|
444
|
+
function validateHostInputShape(executor, input, nodeId, problems) {
|
|
445
|
+
if (!isObject(input) || Object.prototype.hasOwnProperty.call(input, "$ref")) {
|
|
446
|
+
problems.push(`host node "${nodeId}".input must be an object with explicit executor fields`);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const shape = {
|
|
450
|
+
"feishu-send": {
|
|
451
|
+
required: ["larkAppId", "chatId", "content"],
|
|
452
|
+
allowed: ["larkAppId", "chatId", "content", "msgType"]
|
|
453
|
+
},
|
|
454
|
+
"feishu-reply": {
|
|
455
|
+
required: ["larkAppId", "rootMessageId", "content"],
|
|
456
|
+
allowed: ["larkAppId", "rootMessageId", "content", "msgType", "replyInThread"]
|
|
457
|
+
},
|
|
458
|
+
"botmux-schedule": {
|
|
459
|
+
required: ["name", "schedule", "prompt", "workingDir", "chatId", "chatType", "larkAppId"],
|
|
460
|
+
allowed: [
|
|
461
|
+
"name",
|
|
462
|
+
"schedule",
|
|
463
|
+
"prompt",
|
|
464
|
+
"workingDir",
|
|
465
|
+
"chatId",
|
|
466
|
+
"chatType",
|
|
467
|
+
"rootMessageId",
|
|
468
|
+
"scope",
|
|
469
|
+
"larkAppId",
|
|
470
|
+
"repeat",
|
|
471
|
+
"deliver"
|
|
472
|
+
]
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
const expected = shape[executor];
|
|
476
|
+
if (!expected) return;
|
|
477
|
+
for (const field of expected.required) {
|
|
478
|
+
if (!Object.prototype.hasOwnProperty.call(input, field)) {
|
|
479
|
+
problems.push(`host node "${nodeId}".input.${field} is required for ${executor}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
for (const field of Object.keys(input)) {
|
|
483
|
+
if (!expected.allowed.includes(field)) {
|
|
484
|
+
problems.push(`host node "${nodeId}".input.${field} is not supported by ${executor}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const identity = executor === "feishu-send" ? { larkAppId: "larkAppId", chatId: "chatId" } : executor === "feishu-reply" ? { larkAppId: "larkAppId", rootMessageId: "rootMessageId" } : { larkAppId: "larkAppId", chatId: "chatId", chatType: "chatType" };
|
|
488
|
+
if (executor === "botmux-schedule" && Object.prototype.hasOwnProperty.call(input, "rootMessageId")) {
|
|
489
|
+
identity.rootMessageId = "rootMessageId";
|
|
490
|
+
}
|
|
491
|
+
if (executor === "botmux-schedule" && Object.prototype.hasOwnProperty.call(input, "deliver") && input.deliver !== "origin" && input.deliver !== "new-topic") {
|
|
492
|
+
problems.push(
|
|
493
|
+
`host node "${nodeId}".input.deliver must be "origin" or "new-topic"; v3 P0 does not support local-only schedule delivery`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
for (const [field, contextName] of Object.entries(identity)) {
|
|
497
|
+
const value = input[field];
|
|
498
|
+
if (!isObject(value) || Object.keys(value).length !== 1 || value.$ref !== `context.${contextName}`) {
|
|
499
|
+
problems.push(
|
|
500
|
+
`host node "${nodeId}".input.${field} must be exact { "$ref": "context.${contextName}" }; IM host effects cannot target another bot/chat`
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function normHumanGate(raw, where, problems) {
|
|
506
|
+
if (raw == null) return null;
|
|
507
|
+
if (!isObject(raw) || typeof raw.prompt !== "string" || raw.prompt.trim() === "") {
|
|
508
|
+
problems.push(`${where}.humanGate must be { prompt: <non-empty string>, options?, approveOptions?, approvers? } or null`);
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
let options = [...DEFAULT_HUMAN_GATE_OPTIONS];
|
|
512
|
+
if (raw.options !== void 0) {
|
|
513
|
+
if (!Array.isArray(raw.options)) {
|
|
514
|
+
problems.push(`${where}.humanGate.options must be a non-empty string array`);
|
|
515
|
+
} else {
|
|
516
|
+
const parsed = parseUniqueStringList(
|
|
517
|
+
raw.options,
|
|
518
|
+
`${where}.humanGate.options`,
|
|
519
|
+
problems,
|
|
520
|
+
{ allowEmptyList: false, maxItems: MAX_HUMAN_GATE_OPTIONS, maxLength: MAX_HUMAN_GATE_OPTION_LENGTH }
|
|
521
|
+
);
|
|
522
|
+
if (parsed) options = parsed;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
let approveOptions;
|
|
526
|
+
if (raw.approveOptions !== void 0) {
|
|
527
|
+
if (!Array.isArray(raw.approveOptions)) {
|
|
528
|
+
problems.push(`${where}.humanGate.approveOptions must be a non-empty string array`);
|
|
529
|
+
} else {
|
|
530
|
+
approveOptions = parseUniqueStringList(
|
|
531
|
+
raw.approveOptions,
|
|
532
|
+
`${where}.humanGate.approveOptions`,
|
|
533
|
+
problems,
|
|
534
|
+
{ allowEmptyList: false, maxLength: MAX_HUMAN_GATE_OPTION_LENGTH }
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
approveOptions ??= options.includes("approve") ? ["approve"] : [options[0]];
|
|
539
|
+
for (const opt of approveOptions) {
|
|
540
|
+
if (!options.includes(opt)) {
|
|
541
|
+
problems.push(`${where}.humanGate.approveOptions value ${JSON.stringify(opt)} must also appear in options`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
let approvers = [];
|
|
545
|
+
if (raw.approvers !== void 0) {
|
|
546
|
+
if (!Array.isArray(raw.approvers)) {
|
|
547
|
+
problems.push(`${where}.humanGate.approvers must be a string array`);
|
|
548
|
+
} else {
|
|
549
|
+
approvers = parseUniqueStringList(
|
|
550
|
+
raw.approvers,
|
|
551
|
+
`${where}.humanGate.approvers`,
|
|
552
|
+
problems,
|
|
553
|
+
{ allowEmptyList: true }
|
|
554
|
+
) ?? [];
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return { prompt: raw.prompt, options, approveOptions, approvers };
|
|
558
|
+
}
|
|
559
|
+
function parseUniqueStringList(raw, where, problems, opts) {
|
|
560
|
+
const values = [];
|
|
561
|
+
const seen = /* @__PURE__ */ new Set();
|
|
562
|
+
if (!opts.allowEmptyList && raw.length === 0) {
|
|
563
|
+
problems.push(`${where} must not be empty`);
|
|
564
|
+
}
|
|
565
|
+
if (opts.maxItems !== void 0 && raw.length > opts.maxItems) {
|
|
566
|
+
problems.push(`${where} supports at most ${opts.maxItems} entries`);
|
|
567
|
+
}
|
|
568
|
+
for (const item of raw) {
|
|
569
|
+
if (typeof item !== "string" || item.trim() === "") {
|
|
570
|
+
problems.push(`${where} entries must be non-empty strings`);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (opts.maxLength !== void 0 && item.length > opts.maxLength) {
|
|
574
|
+
problems.push(`${where} entry ${JSON.stringify(item)} exceeds ${opts.maxLength} characters`);
|
|
575
|
+
}
|
|
576
|
+
if (seen.has(item)) {
|
|
577
|
+
problems.push(`${where} has duplicate value ${JSON.stringify(item)}`);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
seen.add(item);
|
|
581
|
+
values.push(item);
|
|
582
|
+
}
|
|
583
|
+
return problems.some((p) => p.startsWith(where)) ? void 0 : values;
|
|
584
|
+
}
|
|
585
|
+
function normDepends(v, where, problems, whenSink) {
|
|
586
|
+
if (v === void 0) return [];
|
|
587
|
+
if (!Array.isArray(v)) {
|
|
588
|
+
problems.push(`${where}.depends must be an array of nodeId strings or { from, when? } objects`);
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
const out = [];
|
|
592
|
+
for (let j = 0; j < v.length; j++) {
|
|
593
|
+
const entry = v[j];
|
|
594
|
+
if (typeof entry === "string") {
|
|
595
|
+
out.push({ from: entry });
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (isObject(entry) && typeof entry.from === "string") {
|
|
599
|
+
const extra = Object.keys(entry).filter((k) => k !== "from" && k !== "when");
|
|
600
|
+
if (extra.length > 0) {
|
|
601
|
+
problems.push(`${where}.depends[${j}] has unsupported key(s): ${extra.join(", ")} (allowed: from, when)`);
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const ref = { from: entry.from };
|
|
605
|
+
if (entry.when !== void 0) {
|
|
606
|
+
if (!whenSink) {
|
|
607
|
+
problems.push(`${where}.depends[${j}].when: conditional edges are not supported inside a loop body (first cut)`);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (!isObject(entry.when)) {
|
|
611
|
+
problems.push(`${where}.depends[${j}].when must be an object`);
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
whenSink.list.push({
|
|
615
|
+
where: `${where}.depends[${j}].when (edge "${entry.from}" -> "${whenSink.ownerId}")`,
|
|
616
|
+
ref,
|
|
617
|
+
raw: entry.when
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
out.push(ref);
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
problems.push(`${where}.depends[${j}] must be a nodeId string or { from, when? }`);
|
|
624
|
+
}
|
|
625
|
+
return out;
|
|
626
|
+
}
|
|
627
|
+
function normTriggerRule(v, indegree, where, problems) {
|
|
628
|
+
if (v === void 0) return void 0;
|
|
629
|
+
if (v === "all_success" || v === "one_success") {
|
|
630
|
+
if (indegree === 0) {
|
|
631
|
+
problems.push(`${where}.triggerRule requires at least one incoming edge (depends is empty)`);
|
|
632
|
+
return void 0;
|
|
633
|
+
}
|
|
634
|
+
return v;
|
|
635
|
+
}
|
|
636
|
+
if (isObject(v)) {
|
|
637
|
+
const extra = Object.keys(v).filter((k) => k !== "quorum");
|
|
638
|
+
if (extra.length > 0) {
|
|
639
|
+
problems.push(`${where}.triggerRule object only supports { quorum: N } (got extra: ${extra.join(", ")})`);
|
|
640
|
+
return void 0;
|
|
641
|
+
}
|
|
642
|
+
if (indegree === 0) {
|
|
643
|
+
problems.push(`${where}.triggerRule requires at least one incoming edge (depends is empty)`);
|
|
644
|
+
return void 0;
|
|
645
|
+
}
|
|
646
|
+
const q = v.quorum;
|
|
647
|
+
if (typeof q !== "number" || !Number.isInteger(q) || q < 1 || q > indegree) {
|
|
648
|
+
problems.push(`${where}.triggerRule.quorum must be an integer in [1, ${indegree}] (got ${JSON.stringify(q)})`);
|
|
649
|
+
return void 0;
|
|
650
|
+
}
|
|
651
|
+
return { quorum: q };
|
|
652
|
+
}
|
|
653
|
+
problems.push(`${where}.triggerRule must be 'all_success' | 'one_success' | { quorum: N }`);
|
|
654
|
+
return void 0;
|
|
655
|
+
}
|
|
656
|
+
function normOverride(v, where, problems) {
|
|
657
|
+
if (v === void 0 || v === null) return void 0;
|
|
658
|
+
if (!isObject(v)) {
|
|
659
|
+
problems.push(`${where}.override must be an object`);
|
|
660
|
+
return void 0;
|
|
661
|
+
}
|
|
662
|
+
const known = /* @__PURE__ */ new Set(["model", "systemPromptAppend"]);
|
|
663
|
+
const extra = Object.keys(v).filter((k) => !known.has(k));
|
|
664
|
+
if (extra.length > 0) {
|
|
665
|
+
const hints = [];
|
|
666
|
+
if (extra.includes("toolsSubset")) hints.push("toolsSubset is deferred \u2014 P2b");
|
|
667
|
+
if (extra.includes("permissionMode")) {
|
|
668
|
+
hints.push("permissionMode was removed \u2014 v3 workflow workers always require CLI bypass; delete this key");
|
|
669
|
+
}
|
|
670
|
+
const hint = hints.length > 0 ? ` (${hints.join("; ")})` : "";
|
|
671
|
+
problems.push(`${where}.override has unsupported key(s): ${extra.join(", ")}${hint} (allowed: model, systemPromptAppend)`);
|
|
672
|
+
return void 0;
|
|
673
|
+
}
|
|
674
|
+
const out = {};
|
|
675
|
+
if (v.model !== void 0) {
|
|
676
|
+
if (typeof v.model !== "string" || v.model.trim() === "" || v.model.length > MAX_OVERRIDE_MODEL_LENGTH) {
|
|
677
|
+
problems.push(`${where}.override.model must be a non-empty string \u2264${MAX_OVERRIDE_MODEL_LENGTH} chars`);
|
|
678
|
+
return void 0;
|
|
679
|
+
}
|
|
680
|
+
out.model = v.model.trim();
|
|
681
|
+
}
|
|
682
|
+
if (v.systemPromptAppend !== void 0) {
|
|
683
|
+
if (typeof v.systemPromptAppend !== "string" || v.systemPromptAppend.trim() === "" || Buffer.byteLength(v.systemPromptAppend, "utf-8") > MAX_OVERRIDE_SYSTEM_PROMPT_APPEND) {
|
|
684
|
+
problems.push(`${where}.override.systemPromptAppend must be a non-empty string \u2264${MAX_OVERRIDE_SYSTEM_PROMPT_APPEND} bytes`);
|
|
685
|
+
return void 0;
|
|
686
|
+
}
|
|
687
|
+
out.systemPromptAppend = v.systemPromptAppend;
|
|
688
|
+
}
|
|
689
|
+
if (Object.keys(out).length === 0) {
|
|
690
|
+
problems.push(`${where}.override must set at least one of model / systemPromptAppend`);
|
|
691
|
+
return void 0;
|
|
692
|
+
}
|
|
693
|
+
return out;
|
|
694
|
+
}
|
|
695
|
+
function normTimeoutSec(v, where, problems) {
|
|
696
|
+
if (v === void 0) return void 0;
|
|
697
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) {
|
|
698
|
+
problems.push(`${where}.timeoutSec must be a positive number`);
|
|
699
|
+
return void 0;
|
|
700
|
+
}
|
|
701
|
+
if (v > MAX_NODE_TIMEOUT_SEC) {
|
|
702
|
+
problems.push(`${where}.timeoutSec ${v} exceeds the ${MAX_NODE_TIMEOUT_SEC}s (4h) ceiling`);
|
|
703
|
+
return void 0;
|
|
704
|
+
}
|
|
705
|
+
return v;
|
|
706
|
+
}
|
|
707
|
+
function normRevisitTo(v, id, problems) {
|
|
708
|
+
if (v === void 0) return void 0;
|
|
709
|
+
if (!Array.isArray(v)) {
|
|
710
|
+
problems.push(`node "${id}".revisitTo must be an array of node ids`);
|
|
711
|
+
return void 0;
|
|
712
|
+
}
|
|
713
|
+
const out = [];
|
|
714
|
+
for (const entry of v) {
|
|
715
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
716
|
+
problems.push(`node "${id}".revisitTo entries must be non-empty node-id strings`);
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
out.push(entry);
|
|
720
|
+
}
|
|
721
|
+
if (new Set(out).size !== out.length) problems.push(`node "${id}".revisitTo has duplicates`);
|
|
722
|
+
return out.length > 0 ? out : void 0;
|
|
723
|
+
}
|
|
724
|
+
function ancestorsOf(nodeId, nodes) {
|
|
725
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
726
|
+
const seen = /* @__PURE__ */ new Set();
|
|
727
|
+
const queue = [...byId.get(nodeId)?.depends.map((d) => d.from) ?? []];
|
|
728
|
+
while (queue.length > 0) {
|
|
729
|
+
const cur = queue.shift();
|
|
730
|
+
if (seen.has(cur)) continue;
|
|
731
|
+
seen.add(cur);
|
|
732
|
+
for (const dep of byId.get(cur)?.depends ?? []) queue.push(dep.from);
|
|
733
|
+
}
|
|
734
|
+
return seen;
|
|
735
|
+
}
|
|
736
|
+
function downstreamCone(nodeId, nodes) {
|
|
737
|
+
const seen = /* @__PURE__ */ new Set([nodeId]);
|
|
738
|
+
let changed = true;
|
|
739
|
+
while (changed) {
|
|
740
|
+
changed = false;
|
|
741
|
+
for (const node of nodes) {
|
|
742
|
+
if (seen.has(node.id)) continue;
|
|
743
|
+
if (node.depends.some((dep) => seen.has(dep.from))) {
|
|
744
|
+
seen.add(node.id);
|
|
745
|
+
changed = true;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return seen;
|
|
750
|
+
}
|
|
751
|
+
function nodeByIdUnsafe(nodes, nodeId) {
|
|
752
|
+
return nodes.find((node) => node.id === nodeId);
|
|
753
|
+
}
|
|
754
|
+
function normLoopFields(n, id, problems) {
|
|
755
|
+
const where = `loop node "${id}"`;
|
|
756
|
+
const before = problems.length;
|
|
757
|
+
if (n.timeoutSec !== void 0) {
|
|
758
|
+
problems.push(`${where}.timeoutSec is not supported \u2014 set timeoutSec on body nodes instead`);
|
|
759
|
+
}
|
|
760
|
+
if (n.resultSchema !== void 0) {
|
|
761
|
+
problems.push(`${where}.resultSchema is not supported \u2014 declare it on the exit body node`);
|
|
762
|
+
}
|
|
763
|
+
if (n.humanGate != null) {
|
|
764
|
+
problems.push(`${where}.humanGate is not supported (first cut) \u2014 gate an upstream node instead`);
|
|
765
|
+
}
|
|
766
|
+
if (n.onExhausted !== void 0 && n.onExhausted !== "blocked") {
|
|
767
|
+
problems.push(`${where}.onExhausted only supports "blocked"`);
|
|
768
|
+
}
|
|
769
|
+
if (n.sessionPolicy !== void 0 && n.sessionPolicy !== "fresh") {
|
|
770
|
+
problems.push(`${where}.sessionPolicy only supports "fresh" (resumeWithinLoop is deferred)`);
|
|
771
|
+
}
|
|
772
|
+
if (n.override !== void 0) {
|
|
773
|
+
problems.push(`${where}.override is not supported \u2014 a loop composite never spawns a worker; set override on body nodes instead`);
|
|
774
|
+
}
|
|
775
|
+
let maxIterations;
|
|
776
|
+
if (typeof n.maxIterations !== "number" || !Number.isInteger(n.maxIterations) || n.maxIterations < 1) {
|
|
777
|
+
problems.push(`${where}.maxIterations must be a positive integer`);
|
|
778
|
+
} else if (n.maxIterations > MAX_LOOP_ITERATIONS) {
|
|
779
|
+
problems.push(`${where}.maxIterations ${n.maxIterations} exceeds the ${MAX_LOOP_ITERATIONS} ceiling`);
|
|
780
|
+
} else {
|
|
781
|
+
maxIterations = n.maxIterations;
|
|
782
|
+
}
|
|
783
|
+
const bodyRaw = n.body;
|
|
784
|
+
if (!isObject(bodyRaw) || !Array.isArray(bodyRaw.nodes) || bodyRaw.nodes.length === 0) {
|
|
785
|
+
problems.push(`${where}.body.nodes must be a non-empty array`);
|
|
786
|
+
return void 0;
|
|
787
|
+
}
|
|
788
|
+
const bodyBefore = problems.length;
|
|
789
|
+
const bodyIds = /* @__PURE__ */ new Set();
|
|
790
|
+
const bodyNodes = [];
|
|
791
|
+
for (let j = 0; j < bodyRaw.nodes.length; j++) {
|
|
792
|
+
const b = bodyRaw.nodes[j];
|
|
793
|
+
const bwhere = `${where}.body.nodes[${j}]`;
|
|
794
|
+
if (!isObject(b)) {
|
|
795
|
+
problems.push(`${bwhere} must be an object`);
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const bid = b.id;
|
|
799
|
+
if (typeof bid !== "string" || !SEGMENT_RE.test(bid)) {
|
|
800
|
+
problems.push(`${bwhere}.id must be a path-safe string matching ${SEGMENT_RE} (got ${JSON.stringify(bid)})`);
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
if (bodyIds.has(bid)) {
|
|
804
|
+
problems.push(`${where}.body has duplicate node id "${bid}"`);
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
bodyIds.add(bid);
|
|
808
|
+
if (b.type !== "goal") {
|
|
809
|
+
problems.push(`${where}.body node "${bid}": only "goal" nodes are allowed in a loop body (no nested loops, no host)`);
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
if (typeof b.goal !== "string" || b.goal.trim() === "") {
|
|
813
|
+
problems.push(`${where}.body node "${bid}".goal must be a non-empty string`);
|
|
814
|
+
}
|
|
815
|
+
if (b.humanGate != null) {
|
|
816
|
+
problems.push(`${where}.body node "${bid}".humanGate is not supported inside a loop body (first cut)`);
|
|
817
|
+
}
|
|
818
|
+
if (b.triggerRule !== void 0) {
|
|
819
|
+
problems.push(`${where}.body node "${bid}".triggerRule is not supported inside a loop body (first cut)`);
|
|
820
|
+
}
|
|
821
|
+
const bdepends = normDepends(b.depends, `${where}.body node "${bid}"`, problems);
|
|
822
|
+
const bFromList = bdepends.map((d) => d.from);
|
|
823
|
+
if (bFromList.includes(bid)) problems.push(`${where}.body node "${bid}" depends on itself`);
|
|
824
|
+
if (new Set(bFromList).size !== bFromList.length) problems.push(`${where}.body node "${bid}".depends has duplicates`);
|
|
825
|
+
const binputs = normInputs(b.inputs, `${id}.body.${bid}`, problems);
|
|
826
|
+
const btimeout = normTimeoutSec(b.timeoutSec, `${where}.body node "${bid}"`, problems);
|
|
827
|
+
const bschema = normResultSchema(b.resultSchema, `${id}.body.${bid}`, problems);
|
|
828
|
+
const boverride = normOverride(b.override, `${where}.body node "${bid}"`, problems);
|
|
829
|
+
bodyNodes.push({
|
|
830
|
+
id: bid,
|
|
831
|
+
type: "goal",
|
|
832
|
+
goal: typeof b.goal === "string" ? b.goal : void 0,
|
|
833
|
+
bot: typeof b.bot === "string" ? b.bot : void 0,
|
|
834
|
+
depends: bdepends,
|
|
835
|
+
override: boverride,
|
|
836
|
+
inputs: binputs,
|
|
837
|
+
timeoutSec: btimeout,
|
|
838
|
+
humanGate: null,
|
|
839
|
+
resultSchema: bschema
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
for (const bn of bodyNodes) {
|
|
843
|
+
for (const dep of bn.depends) {
|
|
844
|
+
if (!bodyIds.has(dep.from)) problems.push(`${where}.body node "${bn.id}" depends on unknown body node "${dep.from}"`);
|
|
845
|
+
}
|
|
846
|
+
for (const inp of bn.inputs) {
|
|
847
|
+
if (!bodyIds.has(inp.from)) {
|
|
848
|
+
problems.push(`${where}.body node "${bn.id}".inputs references unknown body node "${inp.from}"`);
|
|
849
|
+
} else if (!bn.depends.some((d) => d.from === inp.from)) {
|
|
850
|
+
problems.push(`${where}.body node "${bn.id}".inputs.from "${inp.from}" must also be in depends`);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (problems.length === bodyBefore && bodyNodes.length === bodyRaw.nodes.length) {
|
|
855
|
+
try {
|
|
856
|
+
topologicalOrder({ runId: "body", nodes: bodyNodes });
|
|
857
|
+
} catch (err) {
|
|
858
|
+
if (err instanceof DagValidationError) {
|
|
859
|
+
for (const p of err.problems) problems.push(`${where}.body: ${p}`);
|
|
860
|
+
} else {
|
|
861
|
+
throw err;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
let exit;
|
|
866
|
+
const exitRaw = n.exit;
|
|
867
|
+
if (!isObject(exitRaw) || typeof exitRaw.node !== "string" || !isObject(exitRaw.when)) {
|
|
868
|
+
problems.push(`${where}.exit must be { node: <bodyId>, when: { path, <operator> } }`);
|
|
869
|
+
} else if (!bodyIds.has(exitRaw.node)) {
|
|
870
|
+
problems.push(`${where}.exit.node "${exitRaw.node}" is not a body node`);
|
|
871
|
+
} else {
|
|
872
|
+
const exitNode = bodyNodes.find((b) => b.id === exitRaw.node);
|
|
873
|
+
if (!exitNode?.resultSchema) {
|
|
874
|
+
problems.push(`${where}.exit.node "${exitRaw.node}" must declare a resultSchema \u2014 the exit decision reads its structured result`);
|
|
875
|
+
} else {
|
|
876
|
+
const when = normLoopExitWhen(exitRaw.when, exitNode.resultSchema, `${where}.exit.when`, problems);
|
|
877
|
+
if (when) exit = { node: exitRaw.node, when };
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
const feedback = [];
|
|
881
|
+
if (n.feedback !== void 0) {
|
|
882
|
+
if (!Array.isArray(n.feedback) || n.feedback.some((x) => typeof x !== "string")) {
|
|
883
|
+
problems.push(`${where}.feedback must be an array of strings`);
|
|
884
|
+
} else {
|
|
885
|
+
for (const ref of n.feedback) {
|
|
886
|
+
const dot = ref.lastIndexOf(".");
|
|
887
|
+
const bodyId = dot > 0 ? ref.slice(0, dot) : "";
|
|
888
|
+
const kind = dot > 0 ? ref.slice(dot + 1) : "";
|
|
889
|
+
if (!bodyIds.has(bodyId) || !["result", "files", "manifest"].includes(kind)) {
|
|
890
|
+
problems.push(`${where}.feedback "${ref}" must be <bodyId>.result | <bodyId>.files | <bodyId>.manifest`);
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
if (kind === "result" && !bodyNodes.find((b) => b.id === bodyId)?.resultSchema) {
|
|
894
|
+
problems.push(`${where}.feedback "${ref}" requires body node "${bodyId}" to declare a resultSchema`);
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
if (feedback.includes(ref)) {
|
|
898
|
+
problems.push(`${where}.feedback has duplicate "${ref}"`);
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
feedback.push(ref);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
let output;
|
|
906
|
+
if (n.output !== void 0) {
|
|
907
|
+
if (!isObject(n.output) || typeof n.output.from !== "string" || !bodyIds.has(n.output.from)) {
|
|
908
|
+
problems.push(`${where}.output must be { from: <bodyId> }`);
|
|
909
|
+
} else {
|
|
910
|
+
output = { from: n.output.from };
|
|
911
|
+
}
|
|
912
|
+
} else if (exit) {
|
|
913
|
+
output = { from: exit.node };
|
|
914
|
+
}
|
|
915
|
+
if (problems.length > before || maxIterations === void 0 || !exit || !output) return void 0;
|
|
916
|
+
return {
|
|
917
|
+
maxIterations,
|
|
918
|
+
body: { nodes: bodyNodes },
|
|
919
|
+
exit,
|
|
920
|
+
feedback,
|
|
921
|
+
output,
|
|
922
|
+
onExhausted: "blocked",
|
|
923
|
+
sessionPolicy: "fresh"
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
function normLoopExitWhen(v, schema, where, problems) {
|
|
927
|
+
const unknown = Object.keys(v).filter((k) => k !== "path" && !LOOP_WHEN_OPERATORS.includes(k));
|
|
928
|
+
if (unknown.length > 0) {
|
|
929
|
+
problems.push(`${where} has unsupported keyword(s): ${unknown.join(", ")} (allowed: path + one of ${LOOP_WHEN_OPERATORS.join("/")})`);
|
|
930
|
+
return void 0;
|
|
931
|
+
}
|
|
932
|
+
const m = typeof v.path === "string" ? /^result\.([A-Za-z0-9_-]+)$/.exec(v.path) : null;
|
|
933
|
+
if (!m) {
|
|
934
|
+
problems.push(`${where}.path must be "result.<key>" (the resultSchema subset is flat \u2014 no deeper paths)`);
|
|
935
|
+
return void 0;
|
|
936
|
+
}
|
|
937
|
+
const key = m[1];
|
|
938
|
+
const prop = schema.properties[key];
|
|
939
|
+
if (!prop) {
|
|
940
|
+
problems.push(`${where}.path references "${key}", which is not declared in the exit node's resultSchema`);
|
|
941
|
+
return void 0;
|
|
942
|
+
}
|
|
943
|
+
if (!(schema.required ?? []).includes(key)) {
|
|
944
|
+
problems.push(`${where}.path references "${key}", which must be in the exit node's resultSchema.required (otherwise the field may be absent at runtime)`);
|
|
945
|
+
return void 0;
|
|
946
|
+
}
|
|
947
|
+
const ops = LOOP_WHEN_OPERATORS.filter((op2) => v[op2] !== void 0);
|
|
948
|
+
if (ops.length !== 1) {
|
|
949
|
+
problems.push(`${where} must set exactly ONE operator (${LOOP_WHEN_OPERATORS.join("/")})`);
|
|
950
|
+
return void 0;
|
|
951
|
+
}
|
|
952
|
+
const op = ops[0];
|
|
953
|
+
const operand = v[op];
|
|
954
|
+
if (prop.type === "array" || prop.type === "object") {
|
|
955
|
+
problems.push(`${where}: cannot compare "${key}" \u2014 exit predicates only support string/number/boolean fields`);
|
|
956
|
+
return void 0;
|
|
957
|
+
}
|
|
958
|
+
if (op === "gt" || op === "gte" || op === "lt" || op === "lte") {
|
|
959
|
+
if (prop.type !== "number") {
|
|
960
|
+
problems.push(`${where}.${op} requires "${key}" to be a number field (it is ${prop.type})`);
|
|
961
|
+
return void 0;
|
|
962
|
+
}
|
|
963
|
+
if (typeof operand !== "number" || !Number.isFinite(operand)) {
|
|
964
|
+
problems.push(`${where}.${op} must be a finite number`);
|
|
965
|
+
return void 0;
|
|
966
|
+
}
|
|
967
|
+
} else {
|
|
968
|
+
if (typeof operand !== prop.type) {
|
|
969
|
+
problems.push(`${where}.${op} must be a ${prop.type} to match "${key}"`);
|
|
970
|
+
return void 0;
|
|
971
|
+
}
|
|
972
|
+
if (prop.type === "string" && prop.enum && !prop.enum.includes(operand)) {
|
|
973
|
+
problems.push(
|
|
974
|
+
`${where}.${op} value ${JSON.stringify(operand)} is not in "${key}"'s enum [${prop.enum.join(", ")}]`
|
|
975
|
+
);
|
|
976
|
+
return void 0;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return { path: v.path, [op]: operand };
|
|
980
|
+
}
|
|
981
|
+
function normResultSchema(v, id, problems) {
|
|
982
|
+
if (v === void 0 || v === null) return void 0;
|
|
983
|
+
const where = `node "${id}".resultSchema`;
|
|
984
|
+
if (!isObject(v)) {
|
|
985
|
+
problems.push(`${where} must be an object`);
|
|
986
|
+
return void 0;
|
|
987
|
+
}
|
|
988
|
+
const knownTop = /* @__PURE__ */ new Set(["type", "properties", "required"]);
|
|
989
|
+
for (const key of Object.keys(v)) {
|
|
990
|
+
if (!knownTop.has(key)) {
|
|
991
|
+
problems.push(`${where} has unsupported keyword "${key}" (subset allows: type/properties/required)`);
|
|
992
|
+
return void 0;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
if (v.type !== "object") {
|
|
996
|
+
problems.push(`${where}.type must be "object"`);
|
|
997
|
+
return void 0;
|
|
998
|
+
}
|
|
999
|
+
if (!isObject(v.properties) || Object.keys(v.properties).length === 0) {
|
|
1000
|
+
problems.push(`${where}.properties must be a non-empty object`);
|
|
1001
|
+
return void 0;
|
|
1002
|
+
}
|
|
1003
|
+
const props = Object.entries(v.properties);
|
|
1004
|
+
if (props.length > RESULT_SCHEMA_MAX_PROPERTIES) {
|
|
1005
|
+
problems.push(`${where} has ${props.length} properties (max ${RESULT_SCHEMA_MAX_PROPERTIES})`);
|
|
1006
|
+
return void 0;
|
|
1007
|
+
}
|
|
1008
|
+
const properties = {};
|
|
1009
|
+
for (const [name, spec] of props) {
|
|
1010
|
+
if (!isObject(spec)) {
|
|
1011
|
+
problems.push(`${where}.properties.${name} must be an object`);
|
|
1012
|
+
return void 0;
|
|
1013
|
+
}
|
|
1014
|
+
for (const key of Object.keys(spec)) {
|
|
1015
|
+
if (key !== "type" && key !== "enum") {
|
|
1016
|
+
problems.push(`${where}.properties.${name} has unsupported keyword "${key}" (subset allows: type, enum)`);
|
|
1017
|
+
return void 0;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if (!RESULT_FIELD_TYPES.includes(spec.type)) {
|
|
1021
|
+
problems.push(`${where}.properties.${name}.type must be one of ${RESULT_FIELD_TYPES.join(" | ")}`);
|
|
1022
|
+
return void 0;
|
|
1023
|
+
}
|
|
1024
|
+
let enumValues;
|
|
1025
|
+
if (spec.enum !== void 0) {
|
|
1026
|
+
if (spec.type !== "string") {
|
|
1027
|
+
problems.push(`${where}.properties.${name}.enum is only supported on string fields (it is ${String(spec.type)})`);
|
|
1028
|
+
return void 0;
|
|
1029
|
+
}
|
|
1030
|
+
if (!Array.isArray(spec.enum) || spec.enum.length === 0 || spec.enum.some((x) => typeof x !== "string" || x.length === 0)) {
|
|
1031
|
+
problems.push(`${where}.properties.${name}.enum must be a non-empty array of non-empty strings`);
|
|
1032
|
+
return void 0;
|
|
1033
|
+
}
|
|
1034
|
+
if (spec.enum.length > RESULT_ENUM_MAX_VALUES) {
|
|
1035
|
+
problems.push(`${where}.properties.${name}.enum has ${spec.enum.length} values (max ${RESULT_ENUM_MAX_VALUES})`);
|
|
1036
|
+
return void 0;
|
|
1037
|
+
}
|
|
1038
|
+
if (new Set(spec.enum).size !== spec.enum.length) {
|
|
1039
|
+
problems.push(`${where}.properties.${name}.enum has duplicates`);
|
|
1040
|
+
return void 0;
|
|
1041
|
+
}
|
|
1042
|
+
const tooLong = spec.enum.filter((x) => x.length > RESULT_ENUM_MAX_VALUE_LENGTH);
|
|
1043
|
+
if (tooLong.length > 0) {
|
|
1044
|
+
problems.push(`${where}.properties.${name}.enum value(s) exceed ${RESULT_ENUM_MAX_VALUE_LENGTH} chars: ${tooLong.join(", ")}`);
|
|
1045
|
+
return void 0;
|
|
1046
|
+
}
|
|
1047
|
+
enumValues = spec.enum;
|
|
1048
|
+
}
|
|
1049
|
+
properties[name] = enumValues ? { type: spec.type, enum: enumValues } : { type: spec.type };
|
|
1050
|
+
}
|
|
1051
|
+
let required;
|
|
1052
|
+
if (v.required !== void 0) {
|
|
1053
|
+
if (!Array.isArray(v.required) || v.required.some((x) => typeof x !== "string")) {
|
|
1054
|
+
problems.push(`${where}.required must be an array of strings`);
|
|
1055
|
+
return void 0;
|
|
1056
|
+
}
|
|
1057
|
+
const unknown = v.required.filter((r) => !(r in properties));
|
|
1058
|
+
if (unknown.length > 0) {
|
|
1059
|
+
problems.push(`${where}.required references undeclared properties: ${unknown.join(", ")}`);
|
|
1060
|
+
return void 0;
|
|
1061
|
+
}
|
|
1062
|
+
if (new Set(v.required).size !== v.required.length) {
|
|
1063
|
+
problems.push(`${where}.required has duplicates`);
|
|
1064
|
+
return void 0;
|
|
1065
|
+
}
|
|
1066
|
+
required = v.required;
|
|
1067
|
+
}
|
|
1068
|
+
const schema = required ? { type: "object", properties, required } : { type: "object", properties };
|
|
1069
|
+
const bytes = Buffer.byteLength(JSON.stringify(schema), "utf-8");
|
|
1070
|
+
if (bytes > RESULT_SCHEMA_MAX_BYTES) {
|
|
1071
|
+
problems.push(`${where} serializes to ${bytes} bytes (max ${RESULT_SCHEMA_MAX_BYTES})`);
|
|
1072
|
+
return void 0;
|
|
1073
|
+
}
|
|
1074
|
+
return schema;
|
|
1075
|
+
}
|
|
1076
|
+
function normInputs(v, id, problems) {
|
|
1077
|
+
if (v === void 0) return [];
|
|
1078
|
+
if (!Array.isArray(v)) {
|
|
1079
|
+
problems.push(`node "${id}".inputs must be an array`);
|
|
1080
|
+
return [];
|
|
1081
|
+
}
|
|
1082
|
+
const out = [];
|
|
1083
|
+
for (let j = 0; j < v.length; j++) {
|
|
1084
|
+
const inp = v[j];
|
|
1085
|
+
if (!isObject(inp) || typeof inp.from !== "string") {
|
|
1086
|
+
problems.push(`node "${id}".inputs[${j}] must be { from: <nodeId>, select? }`);
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
const extra = Object.keys(inp).filter((k) => k !== "from" && k !== "select");
|
|
1090
|
+
if (extra.length > 0) {
|
|
1091
|
+
problems.push(`node "${id}".inputs[${j}] has unsupported key(s): ${extra.join(", ")} (allowed: from, select)`);
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
if (inp.select === void 0) {
|
|
1095
|
+
out.push({ from: inp.from });
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
if (!isObject(inp.select)) {
|
|
1099
|
+
problems.push(`node "${id}".inputs[${j}].select must be { name: <string> } or { path: <string> }`);
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
const selKeys = Object.keys(inp.select);
|
|
1103
|
+
const badKeys = selKeys.filter((k) => k !== "name" && k !== "path");
|
|
1104
|
+
if (badKeys.length > 0 || selKeys.length !== 1) {
|
|
1105
|
+
problems.push(`node "${id}".inputs[${j}].select must set exactly ONE of name / path`);
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
const selVal = inp.select.name ?? inp.select.path;
|
|
1109
|
+
if (typeof selVal !== "string" || selVal.trim() === "") {
|
|
1110
|
+
problems.push(`node "${id}".inputs[${j}].select.${selKeys[0]} must be a non-empty string`);
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
out.push({
|
|
1114
|
+
from: inp.from,
|
|
1115
|
+
select: inp.select.name !== void 0 ? { name: selVal } : { path: selVal }
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
return out;
|
|
1119
|
+
}
|
|
1120
|
+
function topologicalOrder(dag) {
|
|
1121
|
+
const indeg = /* @__PURE__ */ new Map();
|
|
1122
|
+
const adj = /* @__PURE__ */ new Map();
|
|
1123
|
+
for (const node of dag.nodes) {
|
|
1124
|
+
indeg.set(node.id, indeg.get(node.id) ?? 0);
|
|
1125
|
+
if (!adj.has(node.id)) adj.set(node.id, []);
|
|
1126
|
+
}
|
|
1127
|
+
for (const node of dag.nodes) {
|
|
1128
|
+
for (const dep of node.depends) {
|
|
1129
|
+
indeg.set(node.id, (indeg.get(node.id) ?? 0) + 1);
|
|
1130
|
+
adj.get(dep.from).push(node.id);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
const ready = [...indeg.entries()].filter(([, d]) => d === 0).map(([id]) => id).sort();
|
|
1134
|
+
const order = [];
|
|
1135
|
+
while (ready.length > 0) {
|
|
1136
|
+
const id = ready.shift();
|
|
1137
|
+
order.push(id);
|
|
1138
|
+
for (const next of adj.get(id) ?? []) {
|
|
1139
|
+
const d = indeg.get(next) - 1;
|
|
1140
|
+
indeg.set(next, d);
|
|
1141
|
+
if (d === 0) {
|
|
1142
|
+
const pos = lowerBound(ready, next);
|
|
1143
|
+
ready.splice(pos, 0, next);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (order.length !== dag.nodes.length) {
|
|
1148
|
+
const stuck = dag.nodes.map((n) => n.id).filter((id) => !order.includes(id));
|
|
1149
|
+
throw new DagValidationError([`dag has a cycle among nodes: ${stuck.join(", ")}`]);
|
|
1150
|
+
}
|
|
1151
|
+
return order;
|
|
1152
|
+
}
|
|
1153
|
+
function lowerBound(arr, x) {
|
|
1154
|
+
let lo = 0;
|
|
1155
|
+
let hi = arr.length;
|
|
1156
|
+
while (lo < hi) {
|
|
1157
|
+
const mid = lo + hi >> 1;
|
|
1158
|
+
if (arr[mid] < x) lo = mid + 1;
|
|
1159
|
+
else hi = mid;
|
|
1160
|
+
}
|
|
1161
|
+
return lo;
|
|
1162
|
+
}
|
|
1163
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1164
|
+
0 && (module.exports = {
|
|
1165
|
+
DEFAULT_HUMAN_GATE_OPTIONS,
|
|
1166
|
+
DEFAULT_NODE_TIMEOUT_SEC,
|
|
1167
|
+
DEFAULT_REVISIT_BUDGET_PER_PAIR,
|
|
1168
|
+
DEFAULT_REVISIT_BUDGET_PER_RUN,
|
|
1169
|
+
DagValidationError,
|
|
1170
|
+
MAX_HUMAN_GATE_OPTIONS,
|
|
1171
|
+
MAX_HUMAN_GATE_OPTION_LENGTH,
|
|
1172
|
+
MAX_LOOP_ITERATIONS,
|
|
1173
|
+
MAX_NODE_TIMEOUT_SEC,
|
|
1174
|
+
MAX_OVERRIDE_MODEL_LENGTH,
|
|
1175
|
+
MAX_OVERRIDE_SYSTEM_PROMPT_APPEND,
|
|
1176
|
+
NODE_KINDS,
|
|
1177
|
+
RESULT_ENUM_MAX_VALUES,
|
|
1178
|
+
RESULT_ENUM_MAX_VALUE_LENGTH,
|
|
1179
|
+
RESULT_SCHEMA_MAX_BYTES,
|
|
1180
|
+
RESULT_SCHEMA_MAX_PROPERTIES,
|
|
1181
|
+
V3_DAG_SEGMENT_RE,
|
|
1182
|
+
V3_HOST_EXECUTORS,
|
|
1183
|
+
isGoalNode,
|
|
1184
|
+
isHostNode,
|
|
1185
|
+
isLoopNode,
|
|
1186
|
+
loopInstanceId,
|
|
1187
|
+
topologicalOrder,
|
|
1188
|
+
validateDag
|
|
1189
|
+
});
|
|
1190
|
+
//# sourceMappingURL=schema.cjs.map
|