botmux-workflow-core 3.16.0 → 3.17.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/dist/cjs/control.cjs +7 -0
- package/dist/cjs/control.cjs.map +3 -3
- package/dist/cjs/engine.cjs +7 -0
- package/dist/cjs/engine.cjs.map +3 -3
- package/dist/cjs/gate-policy.cjs +7 -0
- package/dist/cjs/gate-policy.cjs.map +3 -3
- package/dist/cjs/index.cjs +135 -8
- package/dist/cjs/index.cjs.map +4 -4
- package/dist/cjs/runtime.cjs +205 -33
- package/dist/cjs/runtime.cjs.map +4 -4
- package/dist/cjs/schema.cjs +135 -8
- package/dist/cjs/schema.cjs.map +4 -4
- package/dist/esm/control.js +7 -0
- package/dist/esm/control.js.map +3 -3
- package/dist/esm/engine.js +7 -0
- package/dist/esm/engine.js.map +3 -3
- package/dist/esm/gate-policy.js +7 -0
- package/dist/esm/gate-policy.js.map +3 -3
- package/dist/esm/index.js +135 -8
- package/dist/esm/index.js.map +4 -4
- package/dist/esm/runtime.js +205 -33
- package/dist/esm/runtime.js.map +4 -4
- package/dist/esm/schema.js +135 -8
- package/dist/esm/schema.js.map +4 -4
- package/dist/types/src/workflows/v3/artifact-contract-declarations.d.ts +21 -0
- package/dist/types/src/workflows/v3/dag.d.ts +7 -0
- package/dist/types/src/workflows/v3/event-contract.d.ts +3 -1
- package/package.json +1 -1
package/dist/esm/runtime.js
CHANGED
|
@@ -233,6 +233,104 @@ function isRecord(value) {
|
|
|
233
233
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
// src/workflows/v3/artifact-contract.ts
|
|
237
|
+
var MANIFEST_FILE_KINDS = [
|
|
238
|
+
"markdown",
|
|
239
|
+
"json",
|
|
240
|
+
"text",
|
|
241
|
+
"code",
|
|
242
|
+
"log",
|
|
243
|
+
"binary",
|
|
244
|
+
"directory"
|
|
245
|
+
];
|
|
246
|
+
var MANIFEST_SUMMARY_MAX_BYTES = 4 * 1024;
|
|
247
|
+
var MANIFEST_PREVIEW_MAX_BYTES = 4 * 1024;
|
|
248
|
+
var MANIFEST_STATUSES = ["ok", "fail"];
|
|
249
|
+
var MANIFEST_SCHEMA_VERSION = 1;
|
|
250
|
+
|
|
251
|
+
// src/workflows/v3/artifact-contract-declarations.ts
|
|
252
|
+
var V3_ARTIFACT_OUTPUT_KEY_RE = /^[A-Za-z0-9._-]+$/;
|
|
253
|
+
var V3_ARTIFACT_OUTPUT_MAX_COUNT = 32;
|
|
254
|
+
var V3_ARTIFACT_OUTPUT_MAX_BYTES = 4 * 1024;
|
|
255
|
+
function normalizeArtifactOutputs(value, where, problems) {
|
|
256
|
+
if (value === void 0) return void 0;
|
|
257
|
+
if (!isRecord2(value) || Object.keys(value).length === 0) {
|
|
258
|
+
problems.push(`${where}.outputs must be a non-empty object when present`);
|
|
259
|
+
return void 0;
|
|
260
|
+
}
|
|
261
|
+
const entries = Object.entries(value);
|
|
262
|
+
if (entries.length > V3_ARTIFACT_OUTPUT_MAX_COUNT) {
|
|
263
|
+
problems.push(`${where}.outputs has ${entries.length} entries (max ${V3_ARTIFACT_OUTPUT_MAX_COUNT})`);
|
|
264
|
+
}
|
|
265
|
+
if (Buffer.byteLength(JSON.stringify(value), "utf-8") > V3_ARTIFACT_OUTPUT_MAX_BYTES) {
|
|
266
|
+
problems.push(`${where}.outputs exceeds ${V3_ARTIFACT_OUTPUT_MAX_BYTES} serialized bytes`);
|
|
267
|
+
}
|
|
268
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
269
|
+
const paths = /* @__PURE__ */ new Set();
|
|
270
|
+
for (const [key, raw] of entries) {
|
|
271
|
+
const itemWhere = `${where}.outputs.${JSON.stringify(key)}`;
|
|
272
|
+
if (!V3_ARTIFACT_OUTPUT_KEY_RE.test(key)) {
|
|
273
|
+
problems.push(`${itemWhere} key must match ${V3_ARTIFACT_OUTPUT_KEY_RE}`);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (!isRecord2(raw)) {
|
|
277
|
+
problems.push(`${itemWhere} must be { path, kind }`);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const extra = Object.keys(raw).filter((field) => field !== "path" && field !== "kind");
|
|
281
|
+
if (extra.length > 0) {
|
|
282
|
+
problems.push(`${itemWhere} has unsupported key(s): ${extra.join(", ")} (allowed: path, kind)`);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (!isPortableRelativeArtifactPath(raw.path)) {
|
|
286
|
+
problems.push(`${itemWhere}.path must be a portable relative path without '.', '..', or empty segments`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (typeof raw.kind !== "string" || !MANIFEST_FILE_KINDS.includes(raw.kind)) {
|
|
290
|
+
problems.push(`${itemWhere}.kind must be one of ${MANIFEST_FILE_KINDS.join(" | ")}`);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (paths.has(raw.path)) {
|
|
294
|
+
problems.push(`${where}.outputs contains duplicate path ${JSON.stringify(raw.path)}`);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
paths.add(raw.path);
|
|
298
|
+
out[key] = { path: raw.path, kind: raw.kind };
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
function validateManifestArtifactContract(outputs, manifest) {
|
|
303
|
+
if (!outputs) return { ok: true, problems: [] };
|
|
304
|
+
const problems = [];
|
|
305
|
+
for (const [key, declaration] of Object.entries(outputs)) {
|
|
306
|
+
const matches = manifest.files.filter((file) => file.path === declaration.path);
|
|
307
|
+
if (matches.length === 0) {
|
|
308
|
+
problems.push(
|
|
309
|
+
`output ${JSON.stringify(key)} requires path ${JSON.stringify(declaration.path)} (available: ${manifest.files.map((file) => `${file.path}:${file.kind}`).join(", ") || "none"})`
|
|
310
|
+
);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (matches.length > 1) {
|
|
314
|
+
problems.push(`output ${JSON.stringify(key)} path ${JSON.stringify(declaration.path)} appears more than once`);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (matches[0].kind !== declaration.kind) {
|
|
318
|
+
problems.push(
|
|
319
|
+
`output ${JSON.stringify(key)} path ${JSON.stringify(declaration.path)} has kind ${JSON.stringify(matches[0].kind)}, expected ${JSON.stringify(declaration.kind)}`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return { ok: problems.length === 0, problems };
|
|
324
|
+
}
|
|
325
|
+
function isPortableRelativeArtifactPath(value) {
|
|
326
|
+
if (typeof value !== "string" || value.length === 0 || value.startsWith("/") || value.includes("\\") || value.includes("\0") || /^[A-Za-z]:/.test(value)) return false;
|
|
327
|
+
const segments = value.split("/");
|
|
328
|
+
return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
|
|
329
|
+
}
|
|
330
|
+
function isRecord2(value) {
|
|
331
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
332
|
+
}
|
|
333
|
+
|
|
236
334
|
// src/workflows/v3/dag.ts
|
|
237
335
|
var NODE_KINDS = ["goal", "host", "loop"];
|
|
238
336
|
var V3_HOST_EXECUTORS = ["feishu-send", "feishu-reply", "botmux-schedule"];
|
|
@@ -283,6 +381,10 @@ function validateDag(raw) {
|
|
|
283
381
|
if (!isObject(raw)) {
|
|
284
382
|
throw new DagValidationError(["root must be a JSON object"]);
|
|
285
383
|
}
|
|
384
|
+
const schemaVersion = raw.schemaVersion === void 0 ? 1 : raw.schemaVersion;
|
|
385
|
+
if (schemaVersion !== 1 && schemaVersion !== 2) {
|
|
386
|
+
problems.push(`schemaVersion must be 1 or 2 (got ${JSON.stringify(raw.schemaVersion)})`);
|
|
387
|
+
}
|
|
286
388
|
if (typeof raw.runId !== "string" || !SEGMENT_RE.test(raw.runId)) {
|
|
287
389
|
problems.push(`runId must be a path-safe string matching ${SEGMENT_RE} (got ${JSON.stringify(raw.runId)})`);
|
|
288
390
|
}
|
|
@@ -319,9 +421,9 @@ function validateDag(raw) {
|
|
|
319
421
|
if (fromList.includes(id)) problems.push(`node "${id}" depends on itself`);
|
|
320
422
|
if (new Set(fromList).size !== fromList.length) problems.push(`node "${id}".depends has duplicates`);
|
|
321
423
|
const triggerRule = normTriggerRule(n.triggerRule, depends.length, `node "${id}"`, problems);
|
|
322
|
-
const inputs = normInputs(n.inputs, id, problems);
|
|
424
|
+
const inputs = normInputs(n.inputs, id, problems, schemaVersion === 2 ? 2 : 1);
|
|
323
425
|
if (type === "loop") {
|
|
324
|
-
const loopFields = normLoopFields(n, id, problems);
|
|
426
|
+
const loopFields = normLoopFields(n, id, problems, schemaVersion === 2 ? 2 : 1);
|
|
325
427
|
if (loopFields) {
|
|
326
428
|
nodes.push({
|
|
327
429
|
id,
|
|
@@ -338,6 +440,7 @@ function validateDag(raw) {
|
|
|
338
440
|
continue;
|
|
339
441
|
}
|
|
340
442
|
if (type === "host") {
|
|
443
|
+
if (n.outputs !== void 0) problems.push(`host node "${id}".outputs is not supported`);
|
|
341
444
|
if (n.goal !== void 0) problems.push(`host node "${id}".goal is not supported`);
|
|
342
445
|
if (n.bot !== void 0) problems.push(`host node "${id}".bot is not supported \u2014 host nodes do not spawn a CLI`);
|
|
343
446
|
if (n.override !== void 0) problems.push(`host node "${id}".override is not supported`);
|
|
@@ -413,6 +516,10 @@ function validateDag(raw) {
|
|
|
413
516
|
const humanGate = normHumanGate(n.humanGate, `node "${id}"`, problems);
|
|
414
517
|
const override = normOverride(n.override, `node "${id}"`, problems);
|
|
415
518
|
const revisitTo = normRevisitTo(n.revisitTo, id, problems);
|
|
519
|
+
if (schemaVersion !== 2 && n.outputs !== void 0) {
|
|
520
|
+
problems.push(`goal node "${id}".outputs requires schemaVersion 2`);
|
|
521
|
+
}
|
|
522
|
+
const outputs = schemaVersion === 2 ? normalizeArtifactOutputs(n.outputs, `goal node "${id}"`, problems) : void 0;
|
|
416
523
|
nodes.push({
|
|
417
524
|
id,
|
|
418
525
|
type,
|
|
@@ -422,6 +529,7 @@ function validateDag(raw) {
|
|
|
422
529
|
triggerRule,
|
|
423
530
|
override,
|
|
424
531
|
inputs,
|
|
532
|
+
outputs,
|
|
425
533
|
timeoutSec,
|
|
426
534
|
humanGate,
|
|
427
535
|
resultSchema,
|
|
@@ -437,6 +545,14 @@ function validateDag(raw) {
|
|
|
437
545
|
problems.push(`node "${node.id}".inputs references unknown node "${inp.from}"`);
|
|
438
546
|
} else if (!node.depends.some((d) => d.from === inp.from)) {
|
|
439
547
|
problems.push(`node "${node.id}".inputs.from "${inp.from}" must also be in depends`);
|
|
548
|
+
} else if (inp.output !== void 0) {
|
|
549
|
+
const source = nodes.find((candidate) => candidate.id === inp.from);
|
|
550
|
+
const outputs = source?.type === "loop" ? source.body?.nodes.find((bodyNode) => bodyNode.id === source.output?.from)?.outputs : source?.outputs;
|
|
551
|
+
if (!outputs || !Object.prototype.hasOwnProperty.call(outputs, inp.output)) {
|
|
552
|
+
problems.push(
|
|
553
|
+
`node "${node.id}".inputs output ${JSON.stringify(inp.output)} is not declared by source "${inp.from}"`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
440
556
|
}
|
|
441
557
|
}
|
|
442
558
|
if (node.type === "host") {
|
|
@@ -515,7 +631,11 @@ function validateDag(raw) {
|
|
|
515
631
|
}
|
|
516
632
|
}
|
|
517
633
|
if (problems.length > 0) throw new DagValidationError(problems);
|
|
518
|
-
const dag = {
|
|
634
|
+
const dag = {
|
|
635
|
+
...raw.schemaVersion !== void 0 ? { schemaVersion } : {},
|
|
636
|
+
runId: raw.runId,
|
|
637
|
+
nodes
|
|
638
|
+
};
|
|
519
639
|
topologicalOrder(dag);
|
|
520
640
|
return dag;
|
|
521
641
|
}
|
|
@@ -829,7 +949,7 @@ function downstreamCone(nodeId, nodes) {
|
|
|
829
949
|
function nodeByIdUnsafe(nodes, nodeId) {
|
|
830
950
|
return nodes.find((node) => node.id === nodeId);
|
|
831
951
|
}
|
|
832
|
-
function normLoopFields(n, id, problems) {
|
|
952
|
+
function normLoopFields(n, id, problems, schemaVersion) {
|
|
833
953
|
const where = `loop node "${id}"`;
|
|
834
954
|
const before = problems.length;
|
|
835
955
|
if (n.timeoutSec !== void 0) {
|
|
@@ -900,10 +1020,14 @@ function normLoopFields(n, id, problems) {
|
|
|
900
1020
|
const bFromList = bdepends.map((d) => d.from);
|
|
901
1021
|
if (bFromList.includes(bid)) problems.push(`${where}.body node "${bid}" depends on itself`);
|
|
902
1022
|
if (new Set(bFromList).size !== bFromList.length) problems.push(`${where}.body node "${bid}".depends has duplicates`);
|
|
903
|
-
const binputs = normInputs(b.inputs, `${id}.body.${bid}`, problems);
|
|
1023
|
+
const binputs = normInputs(b.inputs, `${id}.body.${bid}`, problems, schemaVersion);
|
|
904
1024
|
const btimeout = normTimeoutSec(b.timeoutSec, `${where}.body node "${bid}"`, problems);
|
|
905
1025
|
const bschema = normResultSchema(b.resultSchema, `${id}.body.${bid}`, problems);
|
|
906
1026
|
const boverride = normOverride(b.override, `${where}.body node "${bid}"`, problems);
|
|
1027
|
+
if (schemaVersion !== 2 && b.outputs !== void 0) {
|
|
1028
|
+
problems.push(`${where}.body node "${bid}".outputs requires schemaVersion 2`);
|
|
1029
|
+
}
|
|
1030
|
+
const boutputs = schemaVersion === 2 ? normalizeArtifactOutputs(b.outputs, `${where}.body node "${bid}"`, problems) : void 0;
|
|
907
1031
|
bodyNodes.push({
|
|
908
1032
|
id: bid,
|
|
909
1033
|
type: "goal",
|
|
@@ -912,6 +1036,7 @@ function normLoopFields(n, id, problems) {
|
|
|
912
1036
|
depends: bdepends,
|
|
913
1037
|
override: boverride,
|
|
914
1038
|
inputs: binputs,
|
|
1039
|
+
outputs: boutputs,
|
|
915
1040
|
timeoutSec: btimeout,
|
|
916
1041
|
humanGate: null,
|
|
917
1042
|
resultSchema: bschema
|
|
@@ -926,6 +1051,13 @@ function normLoopFields(n, id, problems) {
|
|
|
926
1051
|
problems.push(`${where}.body node "${bn.id}".inputs references unknown body node "${inp.from}"`);
|
|
927
1052
|
} else if (!bn.depends.some((d) => d.from === inp.from)) {
|
|
928
1053
|
problems.push(`${where}.body node "${bn.id}".inputs.from "${inp.from}" must also be in depends`);
|
|
1054
|
+
} else if (inp.output !== void 0) {
|
|
1055
|
+
const source = bodyNodes.find((candidate) => candidate.id === inp.from);
|
|
1056
|
+
if (!source?.outputs || !Object.prototype.hasOwnProperty.call(source.outputs, inp.output)) {
|
|
1057
|
+
problems.push(
|
|
1058
|
+
`${where}.body node "${bn.id}".inputs output ${JSON.stringify(inp.output)} is not declared by source "${inp.from}"`
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
929
1061
|
}
|
|
930
1062
|
}
|
|
931
1063
|
}
|
|
@@ -1151,7 +1283,7 @@ function normResultSchema(v, id, problems) {
|
|
|
1151
1283
|
}
|
|
1152
1284
|
return schema;
|
|
1153
1285
|
}
|
|
1154
|
-
function normInputs(v, id, problems) {
|
|
1286
|
+
function normInputs(v, id, problems, schemaVersion) {
|
|
1155
1287
|
if (v === void 0) return [];
|
|
1156
1288
|
if (!Array.isArray(v)) {
|
|
1157
1289
|
problems.push(`node "${id}".inputs must be an array`);
|
|
@@ -1164,9 +1296,29 @@ function normInputs(v, id, problems) {
|
|
|
1164
1296
|
problems.push(`node "${id}".inputs[${j}] must be { from: <nodeId>, select? }`);
|
|
1165
1297
|
continue;
|
|
1166
1298
|
}
|
|
1167
|
-
const extra = Object.keys(inp).filter((k) => k !== "from" && k !== "select");
|
|
1299
|
+
const extra = Object.keys(inp).filter((k) => k !== "from" && k !== "select" && k !== "output");
|
|
1168
1300
|
if (extra.length > 0) {
|
|
1169
|
-
problems.push(`node "${id}".inputs[${j}] has unsupported key(s): ${extra.join(", ")} (allowed: from, select)`);
|
|
1301
|
+
problems.push(`node "${id}".inputs[${j}] has unsupported key(s): ${extra.join(", ")} (allowed: from, select/output)`);
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
if (schemaVersion === 2) {
|
|
1305
|
+
if (inp.select !== void 0) {
|
|
1306
|
+
problems.push(`node "${id}".inputs[${j}].select is legacy-only; schemaVersion 2 must use output`);
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
if (inp.output === void 0) {
|
|
1310
|
+
out.push({ from: inp.from });
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
if (typeof inp.output !== "string" || !SEGMENT_RE.test(inp.output)) {
|
|
1314
|
+
problems.push(`node "${id}".inputs[${j}].output must be a stable key matching ${SEGMENT_RE}`);
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
out.push({ from: inp.from, output: inp.output });
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
if (inp.output !== void 0) {
|
|
1321
|
+
problems.push(`node "${id}".inputs[${j}].output requires schemaVersion 2`);
|
|
1170
1322
|
continue;
|
|
1171
1323
|
}
|
|
1172
1324
|
if (inp.select === void 0) {
|
|
@@ -3235,21 +3387,6 @@ function writePendingWait(runDir, input) {
|
|
|
3235
3387
|
return wait;
|
|
3236
3388
|
}
|
|
3237
3389
|
|
|
3238
|
-
// src/workflows/v3/artifact-contract.ts
|
|
3239
|
-
var MANIFEST_FILE_KINDS = [
|
|
3240
|
-
"markdown",
|
|
3241
|
-
"json",
|
|
3242
|
-
"text",
|
|
3243
|
-
"code",
|
|
3244
|
-
"log",
|
|
3245
|
-
"binary",
|
|
3246
|
-
"directory"
|
|
3247
|
-
];
|
|
3248
|
-
var MANIFEST_SUMMARY_MAX_BYTES = 4 * 1024;
|
|
3249
|
-
var MANIFEST_PREVIEW_MAX_BYTES = 4 * 1024;
|
|
3250
|
-
var MANIFEST_STATUSES = ["ok", "fail"];
|
|
3251
|
-
var MANIFEST_SCHEMA_VERSION = 1;
|
|
3252
|
-
|
|
3253
3390
|
// src/workflows/v3/contract.ts
|
|
3254
3391
|
var GOAL_ENV = {
|
|
3255
3392
|
/** Path to the single-sentence goal text file. */
|
|
@@ -3730,7 +3867,7 @@ function readAndVerifyV3HostSuccessResult(input) {
|
|
|
3730
3867
|
`v3 host success result is invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
3731
3868
|
);
|
|
3732
3869
|
}
|
|
3733
|
-
if (!
|
|
3870
|
+
if (!isRecord3(raw)) throw new Error("v3 host success result must be an object");
|
|
3734
3871
|
const allowed = /* @__PURE__ */ new Set([
|
|
3735
3872
|
"schemaVersion",
|
|
3736
3873
|
"runId",
|
|
@@ -3763,13 +3900,13 @@ function readAndVerifyV3HostSuccessResult(input) {
|
|
|
3763
3900
|
]) {
|
|
3764
3901
|
if (raw[key] !== input[key]) throw new Error(`v3 host success result ${key} mismatch`);
|
|
3765
3902
|
}
|
|
3766
|
-
if (!
|
|
3903
|
+
if (!isRecord3(raw.externalRefs)) throw new Error("v3 host success result externalRefs must be an object");
|
|
3767
3904
|
canonicalJson(raw.output);
|
|
3768
3905
|
canonicalJson(raw.externalRefs);
|
|
3769
3906
|
return raw;
|
|
3770
3907
|
}
|
|
3771
3908
|
function parsePreparedHostInput(raw) {
|
|
3772
|
-
if (!
|
|
3909
|
+
if (!isRecord3(raw)) throw new Error("v3 host input sidecar must be an object");
|
|
3773
3910
|
const allowed = /* @__PURE__ */ new Set([
|
|
3774
3911
|
"schemaVersion",
|
|
3775
3912
|
"runId",
|
|
@@ -3890,12 +4027,12 @@ function assertDirectory(path, label) {
|
|
|
3890
4027
|
function sha256(value) {
|
|
3891
4028
|
return createHash5("sha256").update(value).digest("hex");
|
|
3892
4029
|
}
|
|
3893
|
-
function
|
|
4030
|
+
function isRecord3(value) {
|
|
3894
4031
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3895
4032
|
}
|
|
3896
4033
|
|
|
3897
4034
|
// src/workflows/v3/shared-node-runtime.ts
|
|
3898
|
-
function renderGoalFile(goal, resultSchema, loopCtx, nodeInstructions, hasWorkflowParams = false) {
|
|
4035
|
+
function renderGoalFile(goal, resultSchema, loopCtx, nodeInstructions, hasWorkflowParams = false, outputs) {
|
|
3899
4036
|
const E = GOAL_ENV;
|
|
3900
4037
|
const kinds = MANIFEST_FILE_KINDS.join(" | ");
|
|
3901
4038
|
const [okStatus, failStatus] = MANIFEST_STATUSES;
|
|
@@ -3913,6 +4050,16 @@ function renderGoalFile(goal, resultSchema, loopCtx, nodeInstructions, hasWorkfl
|
|
|
3913
4050
|
`List \`result.json\` in the manifest \`files\` array like any other product (its \`path\` is exactly "result.json"). A missing or schema-violating result.json blocks this node.`,
|
|
3914
4051
|
""
|
|
3915
4052
|
] : [];
|
|
4053
|
+
const artifactSection = outputs ? [
|
|
4054
|
+
"## Public artifact outputs (REQUIRED for this node)",
|
|
4055
|
+
"These stable output keys are part of the Workflow interface. Write every declared path under the output directory and list it in the success Manifest with the exact kind:",
|
|
4056
|
+
"",
|
|
4057
|
+
" " + JSON.stringify(outputs),
|
|
4058
|
+
"",
|
|
4059
|
+
"Manifest `name` is presentation-only and may be human-readable; downstream nodes resolve these products by output key \u2192 declared path.",
|
|
4060
|
+
"A missing path or mismatched kind blocks the run and requires a Workflow revision; ordinary retry is disabled.",
|
|
4061
|
+
""
|
|
4062
|
+
] : [];
|
|
3916
4063
|
const loopSection = loopCtx ? [
|
|
3917
4064
|
"## Loop context",
|
|
3918
4065
|
`This node runs inside loop "${loopCtx.loopId}", iteration ${loopCtx.iteration} of at most ${loopCtx.maxIterations}.`,
|
|
@@ -3937,6 +4084,7 @@ function renderGoalFile(goal, resultSchema, loopCtx, nodeInstructions, hasWorkfl
|
|
|
3937
4084
|
...instructionsSection,
|
|
3938
4085
|
...paramsSection,
|
|
3939
4086
|
...loopSection,
|
|
4087
|
+
...artifactSection,
|
|
3940
4088
|
"## How to complete this node",
|
|
3941
4089
|
"You are an autonomous agent completing exactly ONE botmux v3 workflow node.",
|
|
3942
4090
|
"Work toward the goal above until it is done, then stop. Do NOT ask the user with interactive tools (they are disabled in this mode). If you genuinely need a human DECISION to proceed, use the human-ask escape hatch described below (also available as the `botmux-goal-ask` skill).",
|
|
@@ -3978,6 +4126,8 @@ function classifyTerminal(errorClass, opts) {
|
|
|
3978
4126
|
switch (errorClass) {
|
|
3979
4127
|
case "manifestInvalid":
|
|
3980
4128
|
// agent wrote a bad manifest — a retry may fix it
|
|
4129
|
+
case "artifactContractInvalid":
|
|
4130
|
+
// definition/manifest public product mismatch
|
|
3981
4131
|
case "resultInvalid":
|
|
3982
4132
|
return "blocked";
|
|
3983
4133
|
case "workerError":
|
|
@@ -5482,7 +5632,8 @@ async function runWorkflow(dag, deps, opts) {
|
|
|
5482
5632
|
node.resultSchema,
|
|
5483
5633
|
loopCtx,
|
|
5484
5634
|
node.override?.systemPromptAppend,
|
|
5485
|
-
!!workflowDataPath
|
|
5635
|
+
!!workflowDataPath,
|
|
5636
|
+
node.outputs
|
|
5486
5637
|
)
|
|
5487
5638
|
);
|
|
5488
5639
|
const effSnap = mergeNodeCapability(botSnap, node.override);
|
|
@@ -5720,6 +5871,20 @@ async function runWorkflow(dag, deps, opts) {
|
|
|
5720
5871
|
return;
|
|
5721
5872
|
}
|
|
5722
5873
|
}
|
|
5874
|
+
const artifactContract = validateManifestArtifactContract(node.outputs, verdict.manifest);
|
|
5875
|
+
if (!artifactContract.ok) {
|
|
5876
|
+
appendWorkerOutcome({
|
|
5877
|
+
type: "nodeBlocked",
|
|
5878
|
+
nodeId: node.id,
|
|
5879
|
+
...instanceId ? { instanceId } : {},
|
|
5880
|
+
attemptId,
|
|
5881
|
+
errorClass: "artifactContractInvalid",
|
|
5882
|
+
errorCode: "OUTPUT_CONTRACT_VIOLATION",
|
|
5883
|
+
recovery: "reviseWorkflow",
|
|
5884
|
+
message: artifactContract.problems.join("; ")
|
|
5885
|
+
});
|
|
5886
|
+
return;
|
|
5887
|
+
}
|
|
5723
5888
|
appendWorkerOutcome({
|
|
5724
5889
|
type: "nodeSucceeded",
|
|
5725
5890
|
nodeId: node.id,
|
|
@@ -5975,8 +6140,9 @@ ${request.reason}
|
|
|
5975
6140
|
depends: bodyDef.depends.map((d) => ({ from: loopInstanceId(ref.loopId, ref.iteration, d.from) })),
|
|
5976
6141
|
inputs: bodyDef.inputs.map((r) => ({
|
|
5977
6142
|
from: loopInstanceId(ref.loopId, ref.iteration, r.from),
|
|
5978
|
-
...r.select ? { select: r.select } : {}
|
|
6143
|
+
...r.select ? { select: r.select } : {},
|
|
5979
6144
|
// P3: 实例化时保留 selector
|
|
6145
|
+
...r.output !== void 0 ? { output: r.output } : {}
|
|
5980
6146
|
}))
|
|
5981
6147
|
};
|
|
5982
6148
|
}
|
|
@@ -6176,7 +6342,7 @@ ${request.reason}
|
|
|
6176
6342
|
});
|
|
6177
6343
|
}
|
|
6178
6344
|
const latestSuccess = (key) => [...events].reverse().find((e) => e.type === "nodeSucceeded" && (e.instanceId ?? e.nodeId) === key);
|
|
6179
|
-
const pushFrom = (label, nodeId, filter) => {
|
|
6345
|
+
const pushFrom = (label, nodeId, filter, output) => {
|
|
6180
6346
|
const key = snap.nodes.get(nodeId)?.effectiveInstanceId ?? nodeId;
|
|
6181
6347
|
const succ = latestSuccess(key);
|
|
6182
6348
|
if (!succ) return;
|
|
@@ -6186,6 +6352,7 @@ ${request.reason}
|
|
|
6186
6352
|
if (filter && !filter(f)) continue;
|
|
6187
6353
|
inputs.push({
|
|
6188
6354
|
from: label,
|
|
6355
|
+
...output ? { output } : {},
|
|
6189
6356
|
...succ.instanceId ? { instanceId: succ.instanceId } : {},
|
|
6190
6357
|
name: f.name,
|
|
6191
6358
|
path: join6(upstreamOutputDir, f.path),
|
|
@@ -6218,9 +6385,14 @@ ${request.reason}
|
|
|
6218
6385
|
};
|
|
6219
6386
|
const selectorMisses = [];
|
|
6220
6387
|
const pushRef = (ref) => {
|
|
6221
|
-
const
|
|
6388
|
+
const source = nodesById.get(ref.from) ?? (loopRef ? nodesById.get(loopRef.loopId).body.nodes.find(
|
|
6389
|
+
(bodyNode) => loopInstanceId(loopRef.loopId, loopRef.iteration, bodyNode.id) === ref.from
|
|
6390
|
+
) : void 0);
|
|
6391
|
+
const declaredOutputs = source?.type === "loop" ? source.body?.nodes.find((bodyNode) => bodyNode.id === source.output?.from)?.outputs : source?.outputs;
|
|
6392
|
+
const declared = ref.output ? declaredOutputs?.[ref.output] : void 0;
|
|
6393
|
+
const filter = declared ? (f) => f.path === declared.path : ref.select ? (f) => ref.select.name !== void 0 ? f.name === ref.select.name : f.path === ref.select.path : void 0;
|
|
6222
6394
|
const before = inputs.length;
|
|
6223
|
-
pushFrom(ref.from, ref.from, filter);
|
|
6395
|
+
pushFrom(ref.from, ref.from, filter, ref.output);
|
|
6224
6396
|
if (ref.select && inputs.length === before) {
|
|
6225
6397
|
selectorMisses.push({ from: ref.from, reason: "selectorMiss" });
|
|
6226
6398
|
}
|