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/cjs/runtime.cjs
CHANGED
|
@@ -267,6 +267,104 @@ function isRecord(value) {
|
|
|
267
267
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
// src/workflows/v3/artifact-contract.ts
|
|
271
|
+
var MANIFEST_FILE_KINDS = [
|
|
272
|
+
"markdown",
|
|
273
|
+
"json",
|
|
274
|
+
"text",
|
|
275
|
+
"code",
|
|
276
|
+
"log",
|
|
277
|
+
"binary",
|
|
278
|
+
"directory"
|
|
279
|
+
];
|
|
280
|
+
var MANIFEST_SUMMARY_MAX_BYTES = 4 * 1024;
|
|
281
|
+
var MANIFEST_PREVIEW_MAX_BYTES = 4 * 1024;
|
|
282
|
+
var MANIFEST_STATUSES = ["ok", "fail"];
|
|
283
|
+
var MANIFEST_SCHEMA_VERSION = 1;
|
|
284
|
+
|
|
285
|
+
// src/workflows/v3/artifact-contract-declarations.ts
|
|
286
|
+
var V3_ARTIFACT_OUTPUT_KEY_RE = /^[A-Za-z0-9._-]+$/;
|
|
287
|
+
var V3_ARTIFACT_OUTPUT_MAX_COUNT = 32;
|
|
288
|
+
var V3_ARTIFACT_OUTPUT_MAX_BYTES = 4 * 1024;
|
|
289
|
+
function normalizeArtifactOutputs(value, where, problems) {
|
|
290
|
+
if (value === void 0) return void 0;
|
|
291
|
+
if (!isRecord2(value) || Object.keys(value).length === 0) {
|
|
292
|
+
problems.push(`${where}.outputs must be a non-empty object when present`);
|
|
293
|
+
return void 0;
|
|
294
|
+
}
|
|
295
|
+
const entries = Object.entries(value);
|
|
296
|
+
if (entries.length > V3_ARTIFACT_OUTPUT_MAX_COUNT) {
|
|
297
|
+
problems.push(`${where}.outputs has ${entries.length} entries (max ${V3_ARTIFACT_OUTPUT_MAX_COUNT})`);
|
|
298
|
+
}
|
|
299
|
+
if (Buffer.byteLength(JSON.stringify(value), "utf-8") > V3_ARTIFACT_OUTPUT_MAX_BYTES) {
|
|
300
|
+
problems.push(`${where}.outputs exceeds ${V3_ARTIFACT_OUTPUT_MAX_BYTES} serialized bytes`);
|
|
301
|
+
}
|
|
302
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
303
|
+
const paths = /* @__PURE__ */ new Set();
|
|
304
|
+
for (const [key, raw] of entries) {
|
|
305
|
+
const itemWhere = `${where}.outputs.${JSON.stringify(key)}`;
|
|
306
|
+
if (!V3_ARTIFACT_OUTPUT_KEY_RE.test(key)) {
|
|
307
|
+
problems.push(`${itemWhere} key must match ${V3_ARTIFACT_OUTPUT_KEY_RE}`);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (!isRecord2(raw)) {
|
|
311
|
+
problems.push(`${itemWhere} must be { path, kind }`);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const extra = Object.keys(raw).filter((field) => field !== "path" && field !== "kind");
|
|
315
|
+
if (extra.length > 0) {
|
|
316
|
+
problems.push(`${itemWhere} has unsupported key(s): ${extra.join(", ")} (allowed: path, kind)`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (!isPortableRelativeArtifactPath(raw.path)) {
|
|
320
|
+
problems.push(`${itemWhere}.path must be a portable relative path without '.', '..', or empty segments`);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (typeof raw.kind !== "string" || !MANIFEST_FILE_KINDS.includes(raw.kind)) {
|
|
324
|
+
problems.push(`${itemWhere}.kind must be one of ${MANIFEST_FILE_KINDS.join(" | ")}`);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (paths.has(raw.path)) {
|
|
328
|
+
problems.push(`${where}.outputs contains duplicate path ${JSON.stringify(raw.path)}`);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
paths.add(raw.path);
|
|
332
|
+
out[key] = { path: raw.path, kind: raw.kind };
|
|
333
|
+
}
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
function validateManifestArtifactContract(outputs, manifest) {
|
|
337
|
+
if (!outputs) return { ok: true, problems: [] };
|
|
338
|
+
const problems = [];
|
|
339
|
+
for (const [key, declaration] of Object.entries(outputs)) {
|
|
340
|
+
const matches = manifest.files.filter((file) => file.path === declaration.path);
|
|
341
|
+
if (matches.length === 0) {
|
|
342
|
+
problems.push(
|
|
343
|
+
`output ${JSON.stringify(key)} requires path ${JSON.stringify(declaration.path)} (available: ${manifest.files.map((file) => `${file.path}:${file.kind}`).join(", ") || "none"})`
|
|
344
|
+
);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (matches.length > 1) {
|
|
348
|
+
problems.push(`output ${JSON.stringify(key)} path ${JSON.stringify(declaration.path)} appears more than once`);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (matches[0].kind !== declaration.kind) {
|
|
352
|
+
problems.push(
|
|
353
|
+
`output ${JSON.stringify(key)} path ${JSON.stringify(declaration.path)} has kind ${JSON.stringify(matches[0].kind)}, expected ${JSON.stringify(declaration.kind)}`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return { ok: problems.length === 0, problems };
|
|
358
|
+
}
|
|
359
|
+
function isPortableRelativeArtifactPath(value) {
|
|
360
|
+
if (typeof value !== "string" || value.length === 0 || value.startsWith("/") || value.includes("\\") || value.includes("\0") || /^[A-Za-z]:/.test(value)) return false;
|
|
361
|
+
const segments = value.split("/");
|
|
362
|
+
return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
|
|
363
|
+
}
|
|
364
|
+
function isRecord2(value) {
|
|
365
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
366
|
+
}
|
|
367
|
+
|
|
270
368
|
// src/workflows/v3/dag.ts
|
|
271
369
|
var NODE_KINDS = ["goal", "host", "loop"];
|
|
272
370
|
var V3_HOST_EXECUTORS = ["feishu-send", "feishu-reply", "botmux-schedule"];
|
|
@@ -317,6 +415,10 @@ function validateDag(raw) {
|
|
|
317
415
|
if (!isObject(raw)) {
|
|
318
416
|
throw new DagValidationError(["root must be a JSON object"]);
|
|
319
417
|
}
|
|
418
|
+
const schemaVersion = raw.schemaVersion === void 0 ? 1 : raw.schemaVersion;
|
|
419
|
+
if (schemaVersion !== 1 && schemaVersion !== 2) {
|
|
420
|
+
problems.push(`schemaVersion must be 1 or 2 (got ${JSON.stringify(raw.schemaVersion)})`);
|
|
421
|
+
}
|
|
320
422
|
if (typeof raw.runId !== "string" || !SEGMENT_RE.test(raw.runId)) {
|
|
321
423
|
problems.push(`runId must be a path-safe string matching ${SEGMENT_RE} (got ${JSON.stringify(raw.runId)})`);
|
|
322
424
|
}
|
|
@@ -353,9 +455,9 @@ function validateDag(raw) {
|
|
|
353
455
|
if (fromList.includes(id)) problems.push(`node "${id}" depends on itself`);
|
|
354
456
|
if (new Set(fromList).size !== fromList.length) problems.push(`node "${id}".depends has duplicates`);
|
|
355
457
|
const triggerRule = normTriggerRule(n.triggerRule, depends.length, `node "${id}"`, problems);
|
|
356
|
-
const inputs = normInputs(n.inputs, id, problems);
|
|
458
|
+
const inputs = normInputs(n.inputs, id, problems, schemaVersion === 2 ? 2 : 1);
|
|
357
459
|
if (type === "loop") {
|
|
358
|
-
const loopFields = normLoopFields(n, id, problems);
|
|
460
|
+
const loopFields = normLoopFields(n, id, problems, schemaVersion === 2 ? 2 : 1);
|
|
359
461
|
if (loopFields) {
|
|
360
462
|
nodes.push({
|
|
361
463
|
id,
|
|
@@ -372,6 +474,7 @@ function validateDag(raw) {
|
|
|
372
474
|
continue;
|
|
373
475
|
}
|
|
374
476
|
if (type === "host") {
|
|
477
|
+
if (n.outputs !== void 0) problems.push(`host node "${id}".outputs is not supported`);
|
|
375
478
|
if (n.goal !== void 0) problems.push(`host node "${id}".goal is not supported`);
|
|
376
479
|
if (n.bot !== void 0) problems.push(`host node "${id}".bot is not supported \u2014 host nodes do not spawn a CLI`);
|
|
377
480
|
if (n.override !== void 0) problems.push(`host node "${id}".override is not supported`);
|
|
@@ -447,6 +550,10 @@ function validateDag(raw) {
|
|
|
447
550
|
const humanGate = normHumanGate(n.humanGate, `node "${id}"`, problems);
|
|
448
551
|
const override = normOverride(n.override, `node "${id}"`, problems);
|
|
449
552
|
const revisitTo = normRevisitTo(n.revisitTo, id, problems);
|
|
553
|
+
if (schemaVersion !== 2 && n.outputs !== void 0) {
|
|
554
|
+
problems.push(`goal node "${id}".outputs requires schemaVersion 2`);
|
|
555
|
+
}
|
|
556
|
+
const outputs = schemaVersion === 2 ? normalizeArtifactOutputs(n.outputs, `goal node "${id}"`, problems) : void 0;
|
|
450
557
|
nodes.push({
|
|
451
558
|
id,
|
|
452
559
|
type,
|
|
@@ -456,6 +563,7 @@ function validateDag(raw) {
|
|
|
456
563
|
triggerRule,
|
|
457
564
|
override,
|
|
458
565
|
inputs,
|
|
566
|
+
outputs,
|
|
459
567
|
timeoutSec,
|
|
460
568
|
humanGate,
|
|
461
569
|
resultSchema,
|
|
@@ -471,6 +579,14 @@ function validateDag(raw) {
|
|
|
471
579
|
problems.push(`node "${node.id}".inputs references unknown node "${inp.from}"`);
|
|
472
580
|
} else if (!node.depends.some((d) => d.from === inp.from)) {
|
|
473
581
|
problems.push(`node "${node.id}".inputs.from "${inp.from}" must also be in depends`);
|
|
582
|
+
} else if (inp.output !== void 0) {
|
|
583
|
+
const source = nodes.find((candidate) => candidate.id === inp.from);
|
|
584
|
+
const outputs = source?.type === "loop" ? source.body?.nodes.find((bodyNode) => bodyNode.id === source.output?.from)?.outputs : source?.outputs;
|
|
585
|
+
if (!outputs || !Object.prototype.hasOwnProperty.call(outputs, inp.output)) {
|
|
586
|
+
problems.push(
|
|
587
|
+
`node "${node.id}".inputs output ${JSON.stringify(inp.output)} is not declared by source "${inp.from}"`
|
|
588
|
+
);
|
|
589
|
+
}
|
|
474
590
|
}
|
|
475
591
|
}
|
|
476
592
|
if (node.type === "host") {
|
|
@@ -549,7 +665,11 @@ function validateDag(raw) {
|
|
|
549
665
|
}
|
|
550
666
|
}
|
|
551
667
|
if (problems.length > 0) throw new DagValidationError(problems);
|
|
552
|
-
const dag = {
|
|
668
|
+
const dag = {
|
|
669
|
+
...raw.schemaVersion !== void 0 ? { schemaVersion } : {},
|
|
670
|
+
runId: raw.runId,
|
|
671
|
+
nodes
|
|
672
|
+
};
|
|
553
673
|
topologicalOrder(dag);
|
|
554
674
|
return dag;
|
|
555
675
|
}
|
|
@@ -863,7 +983,7 @@ function downstreamCone(nodeId, nodes) {
|
|
|
863
983
|
function nodeByIdUnsafe(nodes, nodeId) {
|
|
864
984
|
return nodes.find((node) => node.id === nodeId);
|
|
865
985
|
}
|
|
866
|
-
function normLoopFields(n, id, problems) {
|
|
986
|
+
function normLoopFields(n, id, problems, schemaVersion) {
|
|
867
987
|
const where = `loop node "${id}"`;
|
|
868
988
|
const before = problems.length;
|
|
869
989
|
if (n.timeoutSec !== void 0) {
|
|
@@ -934,10 +1054,14 @@ function normLoopFields(n, id, problems) {
|
|
|
934
1054
|
const bFromList = bdepends.map((d) => d.from);
|
|
935
1055
|
if (bFromList.includes(bid)) problems.push(`${where}.body node "${bid}" depends on itself`);
|
|
936
1056
|
if (new Set(bFromList).size !== bFromList.length) problems.push(`${where}.body node "${bid}".depends has duplicates`);
|
|
937
|
-
const binputs = normInputs(b.inputs, `${id}.body.${bid}`, problems);
|
|
1057
|
+
const binputs = normInputs(b.inputs, `${id}.body.${bid}`, problems, schemaVersion);
|
|
938
1058
|
const btimeout = normTimeoutSec(b.timeoutSec, `${where}.body node "${bid}"`, problems);
|
|
939
1059
|
const bschema = normResultSchema(b.resultSchema, `${id}.body.${bid}`, problems);
|
|
940
1060
|
const boverride = normOverride(b.override, `${where}.body node "${bid}"`, problems);
|
|
1061
|
+
if (schemaVersion !== 2 && b.outputs !== void 0) {
|
|
1062
|
+
problems.push(`${where}.body node "${bid}".outputs requires schemaVersion 2`);
|
|
1063
|
+
}
|
|
1064
|
+
const boutputs = schemaVersion === 2 ? normalizeArtifactOutputs(b.outputs, `${where}.body node "${bid}"`, problems) : void 0;
|
|
941
1065
|
bodyNodes.push({
|
|
942
1066
|
id: bid,
|
|
943
1067
|
type: "goal",
|
|
@@ -946,6 +1070,7 @@ function normLoopFields(n, id, problems) {
|
|
|
946
1070
|
depends: bdepends,
|
|
947
1071
|
override: boverride,
|
|
948
1072
|
inputs: binputs,
|
|
1073
|
+
outputs: boutputs,
|
|
949
1074
|
timeoutSec: btimeout,
|
|
950
1075
|
humanGate: null,
|
|
951
1076
|
resultSchema: bschema
|
|
@@ -960,6 +1085,13 @@ function normLoopFields(n, id, problems) {
|
|
|
960
1085
|
problems.push(`${where}.body node "${bn.id}".inputs references unknown body node "${inp.from}"`);
|
|
961
1086
|
} else if (!bn.depends.some((d) => d.from === inp.from)) {
|
|
962
1087
|
problems.push(`${where}.body node "${bn.id}".inputs.from "${inp.from}" must also be in depends`);
|
|
1088
|
+
} else if (inp.output !== void 0) {
|
|
1089
|
+
const source = bodyNodes.find((candidate) => candidate.id === inp.from);
|
|
1090
|
+
if (!source?.outputs || !Object.prototype.hasOwnProperty.call(source.outputs, inp.output)) {
|
|
1091
|
+
problems.push(
|
|
1092
|
+
`${where}.body node "${bn.id}".inputs output ${JSON.stringify(inp.output)} is not declared by source "${inp.from}"`
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
963
1095
|
}
|
|
964
1096
|
}
|
|
965
1097
|
}
|
|
@@ -1185,7 +1317,7 @@ function normResultSchema(v, id, problems) {
|
|
|
1185
1317
|
}
|
|
1186
1318
|
return schema;
|
|
1187
1319
|
}
|
|
1188
|
-
function normInputs(v, id, problems) {
|
|
1320
|
+
function normInputs(v, id, problems, schemaVersion) {
|
|
1189
1321
|
if (v === void 0) return [];
|
|
1190
1322
|
if (!Array.isArray(v)) {
|
|
1191
1323
|
problems.push(`node "${id}".inputs must be an array`);
|
|
@@ -1198,9 +1330,29 @@ function normInputs(v, id, problems) {
|
|
|
1198
1330
|
problems.push(`node "${id}".inputs[${j}] must be { from: <nodeId>, select? }`);
|
|
1199
1331
|
continue;
|
|
1200
1332
|
}
|
|
1201
|
-
const extra = Object.keys(inp).filter((k) => k !== "from" && k !== "select");
|
|
1333
|
+
const extra = Object.keys(inp).filter((k) => k !== "from" && k !== "select" && k !== "output");
|
|
1202
1334
|
if (extra.length > 0) {
|
|
1203
|
-
problems.push(`node "${id}".inputs[${j}] has unsupported key(s): ${extra.join(", ")} (allowed: from, select)`);
|
|
1335
|
+
problems.push(`node "${id}".inputs[${j}] has unsupported key(s): ${extra.join(", ")} (allowed: from, select/output)`);
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
if (schemaVersion === 2) {
|
|
1339
|
+
if (inp.select !== void 0) {
|
|
1340
|
+
problems.push(`node "${id}".inputs[${j}].select is legacy-only; schemaVersion 2 must use output`);
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
if (inp.output === void 0) {
|
|
1344
|
+
out.push({ from: inp.from });
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
if (typeof inp.output !== "string" || !SEGMENT_RE.test(inp.output)) {
|
|
1348
|
+
problems.push(`node "${id}".inputs[${j}].output must be a stable key matching ${SEGMENT_RE}`);
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
out.push({ from: inp.from, output: inp.output });
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
if (inp.output !== void 0) {
|
|
1355
|
+
problems.push(`node "${id}".inputs[${j}].output requires schemaVersion 2`);
|
|
1204
1356
|
continue;
|
|
1205
1357
|
}
|
|
1206
1358
|
if (inp.select === void 0) {
|
|
@@ -3213,21 +3365,6 @@ function writePendingWait(runDir, input) {
|
|
|
3213
3365
|
return wait;
|
|
3214
3366
|
}
|
|
3215
3367
|
|
|
3216
|
-
// src/workflows/v3/artifact-contract.ts
|
|
3217
|
-
var MANIFEST_FILE_KINDS = [
|
|
3218
|
-
"markdown",
|
|
3219
|
-
"json",
|
|
3220
|
-
"text",
|
|
3221
|
-
"code",
|
|
3222
|
-
"log",
|
|
3223
|
-
"binary",
|
|
3224
|
-
"directory"
|
|
3225
|
-
];
|
|
3226
|
-
var MANIFEST_SUMMARY_MAX_BYTES = 4 * 1024;
|
|
3227
|
-
var MANIFEST_PREVIEW_MAX_BYTES = 4 * 1024;
|
|
3228
|
-
var MANIFEST_STATUSES = ["ok", "fail"];
|
|
3229
|
-
var MANIFEST_SCHEMA_VERSION = 1;
|
|
3230
|
-
|
|
3231
3368
|
// src/workflows/v3/contract.ts
|
|
3232
3369
|
var GOAL_ENV = {
|
|
3233
3370
|
/** Path to the single-sentence goal text file. */
|
|
@@ -3696,7 +3833,7 @@ function readAndVerifyV3HostSuccessResult(input) {
|
|
|
3696
3833
|
`v3 host success result is invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
3697
3834
|
);
|
|
3698
3835
|
}
|
|
3699
|
-
if (!
|
|
3836
|
+
if (!isRecord3(raw)) throw new Error("v3 host success result must be an object");
|
|
3700
3837
|
const allowed = /* @__PURE__ */ new Set([
|
|
3701
3838
|
"schemaVersion",
|
|
3702
3839
|
"runId",
|
|
@@ -3729,13 +3866,13 @@ function readAndVerifyV3HostSuccessResult(input) {
|
|
|
3729
3866
|
]) {
|
|
3730
3867
|
if (raw[key] !== input[key]) throw new Error(`v3 host success result ${key} mismatch`);
|
|
3731
3868
|
}
|
|
3732
|
-
if (!
|
|
3869
|
+
if (!isRecord3(raw.externalRefs)) throw new Error("v3 host success result externalRefs must be an object");
|
|
3733
3870
|
canonicalJson(raw.output);
|
|
3734
3871
|
canonicalJson(raw.externalRefs);
|
|
3735
3872
|
return raw;
|
|
3736
3873
|
}
|
|
3737
3874
|
function parsePreparedHostInput(raw) {
|
|
3738
|
-
if (!
|
|
3875
|
+
if (!isRecord3(raw)) throw new Error("v3 host input sidecar must be an object");
|
|
3739
3876
|
const allowed = /* @__PURE__ */ new Set([
|
|
3740
3877
|
"schemaVersion",
|
|
3741
3878
|
"runId",
|
|
@@ -3856,12 +3993,12 @@ function assertDirectory(path, label) {
|
|
|
3856
3993
|
function sha256(value) {
|
|
3857
3994
|
return (0, import_node_crypto6.createHash)("sha256").update(value).digest("hex");
|
|
3858
3995
|
}
|
|
3859
|
-
function
|
|
3996
|
+
function isRecord3(value) {
|
|
3860
3997
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3861
3998
|
}
|
|
3862
3999
|
|
|
3863
4000
|
// src/workflows/v3/shared-node-runtime.ts
|
|
3864
|
-
function renderGoalFile(goal, resultSchema, loopCtx, nodeInstructions, hasWorkflowParams = false) {
|
|
4001
|
+
function renderGoalFile(goal, resultSchema, loopCtx, nodeInstructions, hasWorkflowParams = false, outputs) {
|
|
3865
4002
|
const E = GOAL_ENV;
|
|
3866
4003
|
const kinds = MANIFEST_FILE_KINDS.join(" | ");
|
|
3867
4004
|
const [okStatus, failStatus] = MANIFEST_STATUSES;
|
|
@@ -3879,6 +4016,16 @@ function renderGoalFile(goal, resultSchema, loopCtx, nodeInstructions, hasWorkfl
|
|
|
3879
4016
|
`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.`,
|
|
3880
4017
|
""
|
|
3881
4018
|
] : [];
|
|
4019
|
+
const artifactSection = outputs ? [
|
|
4020
|
+
"## Public artifact outputs (REQUIRED for this node)",
|
|
4021
|
+
"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:",
|
|
4022
|
+
"",
|
|
4023
|
+
" " + JSON.stringify(outputs),
|
|
4024
|
+
"",
|
|
4025
|
+
"Manifest `name` is presentation-only and may be human-readable; downstream nodes resolve these products by output key \u2192 declared path.",
|
|
4026
|
+
"A missing path or mismatched kind blocks the run and requires a Workflow revision; ordinary retry is disabled.",
|
|
4027
|
+
""
|
|
4028
|
+
] : [];
|
|
3882
4029
|
const loopSection = loopCtx ? [
|
|
3883
4030
|
"## Loop context",
|
|
3884
4031
|
`This node runs inside loop "${loopCtx.loopId}", iteration ${loopCtx.iteration} of at most ${loopCtx.maxIterations}.`,
|
|
@@ -3903,6 +4050,7 @@ function renderGoalFile(goal, resultSchema, loopCtx, nodeInstructions, hasWorkfl
|
|
|
3903
4050
|
...instructionsSection,
|
|
3904
4051
|
...paramsSection,
|
|
3905
4052
|
...loopSection,
|
|
4053
|
+
...artifactSection,
|
|
3906
4054
|
"## How to complete this node",
|
|
3907
4055
|
"You are an autonomous agent completing exactly ONE botmux v3 workflow node.",
|
|
3908
4056
|
"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).",
|
|
@@ -3944,6 +4092,8 @@ function classifyTerminal(errorClass, opts) {
|
|
|
3944
4092
|
switch (errorClass) {
|
|
3945
4093
|
case "manifestInvalid":
|
|
3946
4094
|
// agent wrote a bad manifest — a retry may fix it
|
|
4095
|
+
case "artifactContractInvalid":
|
|
4096
|
+
// definition/manifest public product mismatch
|
|
3947
4097
|
case "resultInvalid":
|
|
3948
4098
|
return "blocked";
|
|
3949
4099
|
case "workerError":
|
|
@@ -5448,7 +5598,8 @@ async function runWorkflow(dag, deps, opts) {
|
|
|
5448
5598
|
node.resultSchema,
|
|
5449
5599
|
loopCtx,
|
|
5450
5600
|
node.override?.systemPromptAppend,
|
|
5451
|
-
!!workflowDataPath
|
|
5601
|
+
!!workflowDataPath,
|
|
5602
|
+
node.outputs
|
|
5452
5603
|
)
|
|
5453
5604
|
);
|
|
5454
5605
|
const effSnap = mergeNodeCapability(botSnap, node.override);
|
|
@@ -5686,6 +5837,20 @@ async function runWorkflow(dag, deps, opts) {
|
|
|
5686
5837
|
return;
|
|
5687
5838
|
}
|
|
5688
5839
|
}
|
|
5840
|
+
const artifactContract = validateManifestArtifactContract(node.outputs, verdict.manifest);
|
|
5841
|
+
if (!artifactContract.ok) {
|
|
5842
|
+
appendWorkerOutcome({
|
|
5843
|
+
type: "nodeBlocked",
|
|
5844
|
+
nodeId: node.id,
|
|
5845
|
+
...instanceId ? { instanceId } : {},
|
|
5846
|
+
attemptId,
|
|
5847
|
+
errorClass: "artifactContractInvalid",
|
|
5848
|
+
errorCode: "OUTPUT_CONTRACT_VIOLATION",
|
|
5849
|
+
recovery: "reviseWorkflow",
|
|
5850
|
+
message: artifactContract.problems.join("; ")
|
|
5851
|
+
});
|
|
5852
|
+
return;
|
|
5853
|
+
}
|
|
5689
5854
|
appendWorkerOutcome({
|
|
5690
5855
|
type: "nodeSucceeded",
|
|
5691
5856
|
nodeId: node.id,
|
|
@@ -5941,8 +6106,9 @@ ${request.reason}
|
|
|
5941
6106
|
depends: bodyDef.depends.map((d) => ({ from: loopInstanceId(ref.loopId, ref.iteration, d.from) })),
|
|
5942
6107
|
inputs: bodyDef.inputs.map((r) => ({
|
|
5943
6108
|
from: loopInstanceId(ref.loopId, ref.iteration, r.from),
|
|
5944
|
-
...r.select ? { select: r.select } : {}
|
|
6109
|
+
...r.select ? { select: r.select } : {},
|
|
5945
6110
|
// P3: 实例化时保留 selector
|
|
6111
|
+
...r.output !== void 0 ? { output: r.output } : {}
|
|
5946
6112
|
}))
|
|
5947
6113
|
};
|
|
5948
6114
|
}
|
|
@@ -6142,7 +6308,7 @@ ${request.reason}
|
|
|
6142
6308
|
});
|
|
6143
6309
|
}
|
|
6144
6310
|
const latestSuccess = (key) => [...events].reverse().find((e) => e.type === "nodeSucceeded" && (e.instanceId ?? e.nodeId) === key);
|
|
6145
|
-
const pushFrom = (label, nodeId, filter) => {
|
|
6311
|
+
const pushFrom = (label, nodeId, filter, output) => {
|
|
6146
6312
|
const key = snap.nodes.get(nodeId)?.effectiveInstanceId ?? nodeId;
|
|
6147
6313
|
const succ = latestSuccess(key);
|
|
6148
6314
|
if (!succ) return;
|
|
@@ -6152,6 +6318,7 @@ ${request.reason}
|
|
|
6152
6318
|
if (filter && !filter(f)) continue;
|
|
6153
6319
|
inputs.push({
|
|
6154
6320
|
from: label,
|
|
6321
|
+
...output ? { output } : {},
|
|
6155
6322
|
...succ.instanceId ? { instanceId: succ.instanceId } : {},
|
|
6156
6323
|
name: f.name,
|
|
6157
6324
|
path: (0, import_node_path9.join)(upstreamOutputDir, f.path),
|
|
@@ -6184,9 +6351,14 @@ ${request.reason}
|
|
|
6184
6351
|
};
|
|
6185
6352
|
const selectorMisses = [];
|
|
6186
6353
|
const pushRef = (ref) => {
|
|
6187
|
-
const
|
|
6354
|
+
const source = nodesById.get(ref.from) ?? (loopRef ? nodesById.get(loopRef.loopId).body.nodes.find(
|
|
6355
|
+
(bodyNode) => loopInstanceId(loopRef.loopId, loopRef.iteration, bodyNode.id) === ref.from
|
|
6356
|
+
) : void 0);
|
|
6357
|
+
const declaredOutputs = source?.type === "loop" ? source.body?.nodes.find((bodyNode) => bodyNode.id === source.output?.from)?.outputs : source?.outputs;
|
|
6358
|
+
const declared = ref.output ? declaredOutputs?.[ref.output] : void 0;
|
|
6359
|
+
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;
|
|
6188
6360
|
const before = inputs.length;
|
|
6189
|
-
pushFrom(ref.from, ref.from, filter);
|
|
6361
|
+
pushFrom(ref.from, ref.from, filter, ref.output);
|
|
6190
6362
|
if (ref.select && inputs.length === before) {
|
|
6191
6363
|
selectorMisses.push({ from: ref.from, reason: "selectorMiss" });
|
|
6192
6364
|
}
|