@automatalabs/workflows 0.55.0 → 0.57.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/README.md +17 -18
- package/dist/agent-event-source.d.ts.map +1 -1
- package/dist/agent-event-source.js +8 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp-server.js +1026 -232
- package/package.json +5 -5
package/dist/mcp-server.js
CHANGED
|
@@ -14,7 +14,7 @@ import { realpathSync as realpathSync4 } from "node:fs";
|
|
|
14
14
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
15
15
|
|
|
16
16
|
// ../mcp-server/src/daemon/commands.ts
|
|
17
|
-
import { readFileSync as
|
|
17
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
18
18
|
|
|
19
19
|
// ../../node_modules/.pnpm/@modelcontextprotocol+server@2.0.0/node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
|
|
20
20
|
var __create = Object.create;
|
|
@@ -1419,8 +1419,8 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
|
1419
1419
|
let i = 0;
|
|
1420
1420
|
while (i < fullpath.length) {
|
|
1421
1421
|
const el = fullpath[i];
|
|
1422
|
-
const
|
|
1423
|
-
if (!
|
|
1422
|
+
const terminal2 = i === fullpath.length - 1;
|
|
1423
|
+
if (!terminal2) {
|
|
1424
1424
|
curr[el] = curr[el] || { _errors: [] };
|
|
1425
1425
|
} else {
|
|
1426
1426
|
curr[el] = curr[el] || { _errors: [] };
|
|
@@ -1457,7 +1457,7 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
|
1457
1457
|
let i = 0;
|
|
1458
1458
|
while (i < fullpath.length) {
|
|
1459
1459
|
const el = fullpath[i];
|
|
1460
|
-
const
|
|
1460
|
+
const terminal2 = i === fullpath.length - 1;
|
|
1461
1461
|
if (typeof el === "string") {
|
|
1462
1462
|
curr.properties ?? (curr.properties = {});
|
|
1463
1463
|
(_a3 = curr.properties)[el] ?? (_a3[el] = { errors: [] });
|
|
@@ -1467,7 +1467,7 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
|
1467
1467
|
(_b = curr.items)[el] ?? (_b[el] = { errors: [] });
|
|
1468
1468
|
curr = curr.items[el];
|
|
1469
1469
|
}
|
|
1470
|
-
if (
|
|
1470
|
+
if (terminal2) {
|
|
1471
1471
|
curr.errors.push(mapper(issue2));
|
|
1472
1472
|
}
|
|
1473
1473
|
i++;
|
|
@@ -11559,8 +11559,8 @@ function process2(schema, ctx, _params = { path: [], schemaPath: [] }) {
|
|
|
11559
11559
|
return _result.schema;
|
|
11560
11560
|
}
|
|
11561
11561
|
function extractDefs(ctx, schema) {
|
|
11562
|
-
const
|
|
11563
|
-
if (!
|
|
11562
|
+
const root2 = ctx.seen.get(schema);
|
|
11563
|
+
if (!root2)
|
|
11564
11564
|
throw new Error("Unprocessed schema. This is a bug in Zod.");
|
|
11565
11565
|
const idToSchema = /* @__PURE__ */ new Map();
|
|
11566
11566
|
for (const entry of ctx.seen.entries()) {
|
|
@@ -11585,7 +11585,7 @@ function extractDefs(ctx, schema) {
|
|
|
11585
11585
|
entry[1].defId = id;
|
|
11586
11586
|
return { defId: id, ref: `${uriGenerator("__shared")}#/${defsSegment}/${id}` };
|
|
11587
11587
|
}
|
|
11588
|
-
if (entry[1] ===
|
|
11588
|
+
if (entry[1] === root2) {
|
|
11589
11589
|
return { ref: "#" };
|
|
11590
11590
|
}
|
|
11591
11591
|
const uriPrefix = `#`;
|
|
@@ -11649,8 +11649,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11649
11649
|
}
|
|
11650
11650
|
}
|
|
11651
11651
|
function finalize(ctx, schema) {
|
|
11652
|
-
const
|
|
11653
|
-
if (!
|
|
11652
|
+
const root2 = ctx.seen.get(schema);
|
|
11653
|
+
if (!root2)
|
|
11654
11654
|
throw new Error("Unprocessed schema. This is a bug in Zod.");
|
|
11655
11655
|
const flattenRef = (zodSchema) => {
|
|
11656
11656
|
const seen = ctx.seen.get(zodSchema);
|
|
@@ -11733,7 +11733,7 @@ function finalize(ctx, schema) {
|
|
|
11733
11733
|
throw new Error("Schema is missing an `id` property");
|
|
11734
11734
|
result.$id = ctx.external.uri(id);
|
|
11735
11735
|
}
|
|
11736
|
-
Object.assign(result,
|
|
11736
|
+
Object.assign(result, root2.def ?? root2.schema);
|
|
11737
11737
|
const rootMetaId = ctx.metadataRegistry.get(schema)?.id;
|
|
11738
11738
|
if (rootMetaId !== void 0 && result.id === rootMetaId)
|
|
11739
11739
|
delete result.id;
|
|
@@ -18579,8 +18579,8 @@ function decodeMcpParamValue(value) {
|
|
|
18579
18579
|
return;
|
|
18580
18580
|
}
|
|
18581
18581
|
}
|
|
18582
|
-
function valueAtPath(
|
|
18583
|
-
let node =
|
|
18582
|
+
function valueAtPath(root2, path) {
|
|
18583
|
+
let node = root2;
|
|
18584
18584
|
for (const key of path) {
|
|
18585
18585
|
if (node === null || typeof node !== "object") return void 0;
|
|
18586
18586
|
node = node[key];
|
|
@@ -23206,24 +23206,24 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
23206
23206
|
}
|
|
23207
23207
|
}
|
|
23208
23208
|
exports.compileSchema = compileSchema;
|
|
23209
|
-
function resolveRef2(
|
|
23209
|
+
function resolveRef2(root2, baseId, ref) {
|
|
23210
23210
|
var _a3;
|
|
23211
23211
|
ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
|
|
23212
|
-
const schOrFunc =
|
|
23212
|
+
const schOrFunc = root2.refs[ref];
|
|
23213
23213
|
if (schOrFunc) return schOrFunc;
|
|
23214
|
-
let _sch = resolve.call(this,
|
|
23214
|
+
let _sch = resolve.call(this, root2, ref);
|
|
23215
23215
|
if (_sch === void 0) {
|
|
23216
|
-
const schema = (_a3 =
|
|
23216
|
+
const schema = (_a3 = root2.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
|
|
23217
23217
|
const { schemaId } = this.opts;
|
|
23218
23218
|
if (schema) _sch = new SchemaEnv({
|
|
23219
23219
|
schema,
|
|
23220
23220
|
schemaId,
|
|
23221
|
-
root,
|
|
23221
|
+
root: root2,
|
|
23222
23222
|
baseId
|
|
23223
23223
|
});
|
|
23224
23224
|
}
|
|
23225
23225
|
if (_sch === void 0) return;
|
|
23226
|
-
return
|
|
23226
|
+
return root2.refs[ref] = inlineOrCompile.call(this, _sch);
|
|
23227
23227
|
}
|
|
23228
23228
|
exports.resolveRef = resolveRef2;
|
|
23229
23229
|
function inlineOrCompile(sch) {
|
|
@@ -23237,20 +23237,20 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
23237
23237
|
function sameSchemaEnv(s1, s2) {
|
|
23238
23238
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
23239
23239
|
}
|
|
23240
|
-
function resolve(
|
|
23240
|
+
function resolve(root2, ref) {
|
|
23241
23241
|
let sch;
|
|
23242
23242
|
while (typeof (sch = this.refs[ref]) == "string") ref = sch;
|
|
23243
|
-
return sch || this.schemas[ref] || resolveSchema.call(this,
|
|
23243
|
+
return sch || this.schemas[ref] || resolveSchema.call(this, root2, ref);
|
|
23244
23244
|
}
|
|
23245
|
-
function resolveSchema(
|
|
23245
|
+
function resolveSchema(root2, ref) {
|
|
23246
23246
|
const p = this.opts.uriResolver.parse(ref);
|
|
23247
23247
|
const refPath = (0, resolve_1._getFullPath)(this.opts.uriResolver, p);
|
|
23248
|
-
let baseId = (0, resolve_1.getFullPath)(this.opts.uriResolver,
|
|
23249
|
-
if (Object.keys(
|
|
23248
|
+
let baseId = (0, resolve_1.getFullPath)(this.opts.uriResolver, root2.baseId, void 0);
|
|
23249
|
+
if (Object.keys(root2.schema).length > 0 && refPath === baseId) return getJsonPointer.call(this, p, root2);
|
|
23250
23250
|
const id = (0, resolve_1.normalizeId)(refPath);
|
|
23251
23251
|
const schOrRef = this.refs[id] || this.schemas[id];
|
|
23252
23252
|
if (typeof schOrRef == "string") {
|
|
23253
|
-
const sch = resolveSchema.call(this,
|
|
23253
|
+
const sch = resolveSchema.call(this, root2, schOrRef);
|
|
23254
23254
|
if (typeof (sch === null || sch === void 0 ? void 0 : sch.schema) !== "object") return;
|
|
23255
23255
|
return getJsonPointer.call(this, p, sch);
|
|
23256
23256
|
}
|
|
@@ -23264,7 +23264,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
23264
23264
|
return new SchemaEnv({
|
|
23265
23265
|
schema,
|
|
23266
23266
|
schemaId,
|
|
23267
|
-
root,
|
|
23267
|
+
root: root2,
|
|
23268
23268
|
baseId
|
|
23269
23269
|
});
|
|
23270
23270
|
}
|
|
@@ -23278,7 +23278,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
23278
23278
|
"dependencies",
|
|
23279
23279
|
"definitions"
|
|
23280
23280
|
]);
|
|
23281
|
-
function getJsonPointer(parsedRef, { baseId, schema, root }) {
|
|
23281
|
+
function getJsonPointer(parsedRef, { baseId, schema, root: root2 }) {
|
|
23282
23282
|
var _a3;
|
|
23283
23283
|
if (((_a3 = parsedRef.fragment) === null || _a3 === void 0 ? void 0 : _a3[0]) !== "/") return;
|
|
23284
23284
|
for (const part of parsedRef.fragment.slice(1).split("/")) {
|
|
@@ -23292,13 +23292,13 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
23292
23292
|
let env;
|
|
23293
23293
|
if (typeof schema != "boolean" && schema.$ref && !(0, util_1.schemaHasRulesButRef)(schema, this.RULES)) {
|
|
23294
23294
|
const $ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, schema.$ref);
|
|
23295
|
-
env = resolveSchema.call(this,
|
|
23295
|
+
env = resolveSchema.call(this, root2, $ref);
|
|
23296
23296
|
}
|
|
23297
23297
|
const { schemaId } = this.opts;
|
|
23298
23298
|
env = env || new SchemaEnv({
|
|
23299
23299
|
schema,
|
|
23300
23300
|
schemaId,
|
|
23301
|
-
root,
|
|
23301
|
+
root: root2,
|
|
23302
23302
|
baseId
|
|
23303
23303
|
});
|
|
23304
23304
|
if (env.schema !== env.root.schema) return env;
|
|
@@ -24163,11 +24163,11 @@ var require_core$3 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
24163
24163
|
while (typeof (sch = getSchEnv.call(this, keyRef)) == "string") keyRef = sch;
|
|
24164
24164
|
if (sch === void 0) {
|
|
24165
24165
|
const { schemaId } = this.opts;
|
|
24166
|
-
const
|
|
24166
|
+
const root2 = new compile_1.SchemaEnv({
|
|
24167
24167
|
schema: {},
|
|
24168
24168
|
schemaId
|
|
24169
24169
|
});
|
|
24170
|
-
sch = compile_1.resolveSchema.call(this,
|
|
24170
|
+
sch = compile_1.resolveSchema.call(this, root2, keyRef);
|
|
24171
24171
|
if (!sch) return;
|
|
24172
24172
|
this.refs[keyRef] = sch;
|
|
24173
24173
|
}
|
|
@@ -24472,16 +24472,16 @@ var require_ref = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
24472
24472
|
code(cxt) {
|
|
24473
24473
|
const { gen, schema: $ref, it } = cxt;
|
|
24474
24474
|
const { baseId, schemaEnv: env, validateName, opts, self } = it;
|
|
24475
|
-
const { root } = env;
|
|
24476
|
-
if (($ref === "#" || $ref === "#/") && baseId ===
|
|
24477
|
-
const schOrEnv = compile_1.resolveRef.call(self,
|
|
24475
|
+
const { root: root2 } = env;
|
|
24476
|
+
if (($ref === "#" || $ref === "#/") && baseId === root2.baseId) return callRootRef();
|
|
24477
|
+
const schOrEnv = compile_1.resolveRef.call(self, root2, baseId, $ref);
|
|
24478
24478
|
if (schOrEnv === void 0) throw new ref_error_1.default(it.opts.uriResolver, baseId, $ref);
|
|
24479
24479
|
if (schOrEnv instanceof compile_1.SchemaEnv) return callValidate(schOrEnv);
|
|
24480
24480
|
return inlineRefSchema(schOrEnv);
|
|
24481
24481
|
function callRootRef() {
|
|
24482
|
-
if (env ===
|
|
24483
|
-
const rootName = gen.scopeValue("root", { ref:
|
|
24484
|
-
return callRef(cxt, (0, codegen_1._)`${rootName}.validate`,
|
|
24482
|
+
if (env === root2) return callRef(cxt, validateName, env, env.$async);
|
|
24483
|
+
const rootName = gen.scopeValue("root", { ref: root2 });
|
|
24484
|
+
return callRef(cxt, (0, codegen_1._)`${rootName}.validate`, root2, root2.$async);
|
|
24485
24485
|
}
|
|
24486
24486
|
function callValidate(sch) {
|
|
24487
24487
|
callRef(cxt, getValidate(cxt, sch), sch, sch.$async);
|
|
@@ -26196,12 +26196,12 @@ var require_dynamicAnchor = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
26196
26196
|
exports.dynamicAnchor = dynamicAnchor;
|
|
26197
26197
|
function _getValidate(cxt) {
|
|
26198
26198
|
const { schemaEnv, schema, self } = cxt.it;
|
|
26199
|
-
const { root, baseId, localRefs, meta: meta3 } = schemaEnv.root;
|
|
26199
|
+
const { root: root2, baseId, localRefs, meta: meta3 } = schemaEnv.root;
|
|
26200
26200
|
const { schemaId } = self.opts;
|
|
26201
26201
|
const sch = new compile_1.SchemaEnv({
|
|
26202
26202
|
schema,
|
|
26203
26203
|
schemaId,
|
|
26204
|
-
root,
|
|
26204
|
+
root: root2,
|
|
26205
26205
|
baseId,
|
|
26206
26206
|
localRefs,
|
|
26207
26207
|
meta: meta3
|
|
@@ -30640,7 +30640,7 @@ function createRequestStateCodec(options) {
|
|
|
30640
30640
|
|
|
30641
30641
|
// ../mcp-server/src/server.ts
|
|
30642
30642
|
import { createHash, randomBytes } from "node:crypto";
|
|
30643
|
-
import { readFileSync as
|
|
30643
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
30644
30644
|
import { createRequire } from "node:module";
|
|
30645
30645
|
import {
|
|
30646
30646
|
buildModelFilter,
|
|
@@ -30696,7 +30696,8 @@ var workflowToolInputShape = {
|
|
|
30696
30696
|
maxAgents: external_exports.number().int().positive().optional().describe("Max agents allowed in this run. Default 1000 (engine cap MAX_AGENTS_PER_RUN)."),
|
|
30697
30697
|
concurrency: external_exports.number().int().positive().optional().describe("Max concurrent agents. CLAMPED to the runtime max (16) by the engine \u2014 not rejected."),
|
|
30698
30698
|
agentRetries: external_exports.number().int().min(0).optional().describe("Retry attempts for recoverable agent failures. CLAMPED to the runtime max (3) by the engine."),
|
|
30699
|
-
agentTimeoutMs: external_exports.number().int().positive().nullable().optional().describe("Per-agent timeout in ms. Omit/null for no hard timeout (the engine owns the timeout)."),
|
|
30699
|
+
agentTimeoutMs: external_exports.number().int().positive().nullable().optional().describe("Per-agent total-wall timeout in ms. Omit/null for no hard timeout (the engine owns the timeout)."),
|
|
30700
|
+
agentIdleTimeoutMs: external_exports.number().int().positive().nullable().optional().describe("Per-agent no-backend-activity timeout in ms. Omit/null to disable the idle watchdog."),
|
|
30700
30701
|
resumeFromRunId: external_exports.string().min(1).optional().describe(
|
|
30701
30702
|
"Start a new run from this persisted source run. Re-send the script via script or scriptPath and the desired args; the manager validates replay eligibility and runs live wherever reuse is uncertain. The source ID must exist in this project namespace."
|
|
30702
30703
|
),
|
|
@@ -30707,6 +30708,9 @@ var workflowToolInputShape = {
|
|
|
30707
30708
|
callIndex: external_exports.number().int().nonnegative().safe().optional().describe(
|
|
30708
30709
|
"With action=stop, cancel exactly this in-flight agent call without aborting the run. Forbidden for every other action."
|
|
30709
30710
|
),
|
|
30711
|
+
forceOwner: external_exports.boolean().optional().describe(
|
|
30712
|
+
"With whole-run action=stop, explicitly authorize terminating a superseded owner daemon when graceful cross-generation control cannot settle. Forbidden with callIndex and every other action."
|
|
30713
|
+
),
|
|
30710
30714
|
lastN: external_exports.number().int().min(1).max(50).optional().describe("Latest matching calls. Default 20; range 1..50."),
|
|
30711
30715
|
labelGlob: external_exports.string().refine((value) => [...value].length >= 1 && [...value].length <= 128, {
|
|
30712
30716
|
message: "labelGlob must contain from 1 through 128 Unicode code points"
|
|
@@ -30718,14 +30722,14 @@ function hasConfigFields(raw) {
|
|
|
30718
30722
|
return raw.harnesses !== void 0 || raw.modelSpecs !== void 0 || raw.modelFilter !== void 0 || raw.probeTimeoutMs !== void 0;
|
|
30719
30723
|
}
|
|
30720
30724
|
function hasExecutionFields(raw) {
|
|
30721
|
-
return raw.script !== void 0 || raw.scriptPath !== void 0 || raw.projectDir !== void 0 || raw.args !== void 0 || raw.maxAgents !== void 0 || raw.concurrency !== void 0 || raw.agentRetries !== void 0 || raw.agentTimeoutMs !== void 0 || raw.resumeFromRunId !== void 0 || raw.resumePolicy !== void 0 || raw.checkpointReplies !== void 0 || raw.background !== void 0;
|
|
30725
|
+
return raw.script !== void 0 || raw.scriptPath !== void 0 || raw.projectDir !== void 0 || raw.args !== void 0 || raw.maxAgents !== void 0 || raw.concurrency !== void 0 || raw.agentRetries !== void 0 || raw.agentTimeoutMs !== void 0 || raw.agentIdleTimeoutMs !== void 0 || raw.resumeFromRunId !== void 0 || raw.resumePolicy !== void 0 || raw.checkpointReplies !== void 0 || raw.background !== void 0;
|
|
30722
30726
|
}
|
|
30723
30727
|
function invalid(message) {
|
|
30724
30728
|
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid workflow tool input: ${message}`);
|
|
30725
30729
|
}
|
|
30726
30730
|
function parseWorkflowToolInput(raw, options = {}) {
|
|
30727
30731
|
if (raw.action === "config") {
|
|
30728
|
-
if (raw.script !== void 0 || raw.scriptPath !== void 0 || raw.args !== void 0 || raw.maxAgents !== void 0 || raw.concurrency !== void 0 || raw.agentRetries !== void 0 || raw.agentTimeoutMs !== void 0 || raw.resumeFromRunId !== void 0 || raw.resumePolicy !== void 0 || raw.checkpointReplies !== void 0 || raw.background !== void 0 || raw.runId !== void 0 || raw.callIndex !== void 0 || raw.waitMs !== void 0 || raw.lastN !== void 0 || raw.labelGlob !== void 0 || raw.logLines !== void 0) {
|
|
30732
|
+
if (raw.script !== void 0 || raw.scriptPath !== void 0 || raw.args !== void 0 || raw.maxAgents !== void 0 || raw.concurrency !== void 0 || raw.agentRetries !== void 0 || raw.agentTimeoutMs !== void 0 || raw.agentIdleTimeoutMs !== void 0 || raw.resumeFromRunId !== void 0 || raw.resumePolicy !== void 0 || raw.checkpointReplies !== void 0 || raw.background !== void 0 || raw.runId !== void 0 || raw.callIndex !== void 0 || raw.forceOwner !== void 0 || raw.waitMs !== void 0 || raw.lastN !== void 0 || raw.labelGlob !== void 0 || raw.logLines !== void 0) {
|
|
30729
30733
|
invalid('action="config" accepts only projectDir, harnesses, modelSpecs, modelFilter, and probeTimeoutMs');
|
|
30730
30734
|
}
|
|
30731
30735
|
if (options.requireProjectDir === true && raw.projectDir === void 0) {
|
|
@@ -30744,7 +30748,7 @@ function parseWorkflowToolInput(raw, options = {}) {
|
|
|
30744
30748
|
}
|
|
30745
30749
|
if (raw.action === "inspect") {
|
|
30746
30750
|
if (!raw.runId) invalid('action="inspect" requires runId');
|
|
30747
|
-
if (hasExecutionFields(raw) || hasConfigFields(raw) || raw.waitMs !== void 0 || raw.callIndex !== void 0) {
|
|
30751
|
+
if (hasExecutionFields(raw) || hasConfigFields(raw) || raw.waitMs !== void 0 || raw.callIndex !== void 0 || raw.forceOwner !== void 0) {
|
|
30748
30752
|
invalid('action="inspect" cannot include execution fields');
|
|
30749
30753
|
}
|
|
30750
30754
|
return {
|
|
@@ -30757,7 +30761,7 @@ function parseWorkflowToolInput(raw, options = {}) {
|
|
|
30757
30761
|
}
|
|
30758
30762
|
if (raw.action === "await") {
|
|
30759
30763
|
if (!raw.runId) invalid('action="await" requires runId');
|
|
30760
|
-
if (hasExecutionFields(raw) || hasConfigFields(raw) || raw.callIndex !== void 0) {
|
|
30764
|
+
if (hasExecutionFields(raw) || hasConfigFields(raw) || raw.callIndex !== void 0 || raw.forceOwner !== void 0) {
|
|
30761
30765
|
invalid('action="await" cannot include execution fields');
|
|
30762
30766
|
}
|
|
30763
30767
|
return {
|
|
@@ -30774,16 +30778,20 @@ function parseWorkflowToolInput(raw, options = {}) {
|
|
|
30774
30778
|
if (hasExecutionFields(raw) || hasConfigFields(raw) || raw.waitMs !== void 0) {
|
|
30775
30779
|
invalid('action="stop" cannot include execution fields or waitMs');
|
|
30776
30780
|
}
|
|
30781
|
+
if (raw.callIndex !== void 0 && raw.forceOwner !== void 0) {
|
|
30782
|
+
invalid('action="stop" forceOwner is forbidden with callIndex');
|
|
30783
|
+
}
|
|
30777
30784
|
return {
|
|
30778
30785
|
action: "stop",
|
|
30779
30786
|
runId: raw.runId,
|
|
30780
30787
|
callIndex: raw.callIndex,
|
|
30788
|
+
...raw.forceOwner === void 0 ? {} : { forceOwner: raw.forceOwner },
|
|
30781
30789
|
lastN: raw.lastN,
|
|
30782
30790
|
labelGlob: raw.labelGlob,
|
|
30783
30791
|
logLines: raw.logLines
|
|
30784
30792
|
};
|
|
30785
30793
|
}
|
|
30786
|
-
if (raw.runId !== void 0 || raw.callIndex !== void 0 || raw.waitMs !== void 0 || raw.lastN !== void 0 || raw.labelGlob !== void 0 || raw.logLines !== void 0 || hasConfigFields(raw)) {
|
|
30794
|
+
if (raw.runId !== void 0 || raw.callIndex !== void 0 || raw.forceOwner !== void 0 || raw.waitMs !== void 0 || raw.lastN !== void 0 || raw.labelGlob !== void 0 || raw.logLines !== void 0 || hasConfigFields(raw)) {
|
|
30787
30795
|
invalid("run inputs cannot include inspection fields");
|
|
30788
30796
|
}
|
|
30789
30797
|
const hasScript = raw.script !== void 0;
|
|
@@ -30808,6 +30816,7 @@ function parseWorkflowToolInput(raw, options = {}) {
|
|
|
30808
30816
|
concurrency: raw.concurrency,
|
|
30809
30817
|
agentRetries: raw.agentRetries,
|
|
30810
30818
|
agentTimeoutMs: raw.agentTimeoutMs,
|
|
30819
|
+
agentIdleTimeoutMs: raw.agentIdleTimeoutMs,
|
|
30811
30820
|
resumeFromRunId: raw.resumeFromRunId,
|
|
30812
30821
|
resumePolicy: raw.resumePolicy,
|
|
30813
30822
|
checkpointReplies: raw.checkpointReplies === void 0 ? void 0 : Object.fromEntries(
|
|
@@ -30950,8 +30959,8 @@ async function discoverProjectDefaultBackend(context, probeRunner, timeoutMs = 6
|
|
|
30950
30959
|
}
|
|
30951
30960
|
|
|
30952
30961
|
// ../mcp-server/src/project-registry.ts
|
|
30953
|
-
import { existsSync as existsSync2, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
30954
|
-
import { isAbsolute as isAbsolute2, join } from "node:path";
|
|
30962
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync, statSync } from "node:fs";
|
|
30963
|
+
import { isAbsolute as isAbsolute2, join as join2 } from "node:path";
|
|
30955
30964
|
import {
|
|
30956
30965
|
WORKFLOW_PROJECTS_SUBDIR,
|
|
30957
30966
|
WorkflowManager,
|
|
@@ -31240,6 +31249,152 @@ async function resetReplProjectState(state, boundMs = SHUTDOWN_DEADLINE_MS) {
|
|
|
31240
31249
|
state.drainError = null;
|
|
31241
31250
|
}
|
|
31242
31251
|
|
|
31252
|
+
// ../mcp-server/src/daemon/run-control-store.ts
|
|
31253
|
+
import { randomUUID } from "node:crypto";
|
|
31254
|
+
import {
|
|
31255
|
+
chmodSync,
|
|
31256
|
+
linkSync,
|
|
31257
|
+
mkdirSync,
|
|
31258
|
+
readFileSync,
|
|
31259
|
+
readdirSync,
|
|
31260
|
+
rmSync,
|
|
31261
|
+
writeFileSync
|
|
31262
|
+
} from "node:fs";
|
|
31263
|
+
import { join } from "node:path";
|
|
31264
|
+
var CONTROL_DIR = ".control";
|
|
31265
|
+
var RUN_ID = /^[a-z0-9]+-[a-z0-9]+$/;
|
|
31266
|
+
var OPERATION_ID = /^[0-9a-f-]{36}$/i;
|
|
31267
|
+
function root(manager) {
|
|
31268
|
+
return join(manager.getPersistence().getRunsDir(), CONTROL_DIR);
|
|
31269
|
+
}
|
|
31270
|
+
function runDir(manager, runId) {
|
|
31271
|
+
if (!RUN_ID.test(runId)) throw new Error(`Invalid run-control runId: ${runId}`);
|
|
31272
|
+
return join(root(manager), runId);
|
|
31273
|
+
}
|
|
31274
|
+
function requestPath(manager, runId, operationId) {
|
|
31275
|
+
if (!OPERATION_ID.test(operationId)) throw new Error(`Invalid run-control operationId: ${operationId}`);
|
|
31276
|
+
return join(runDir(manager, runId), `${operationId}.request.json`);
|
|
31277
|
+
}
|
|
31278
|
+
function acknowledgementPath(manager, runId, operationId) {
|
|
31279
|
+
if (!OPERATION_ID.test(operationId)) throw new Error(`Invalid run-control operationId: ${operationId}`);
|
|
31280
|
+
return join(runDir(manager, runId), `${operationId}.ack.json`);
|
|
31281
|
+
}
|
|
31282
|
+
function ensureRunDir(manager, runId) {
|
|
31283
|
+
const dir = runDir(manager, runId);
|
|
31284
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
31285
|
+
chmodSync(root(manager), 448);
|
|
31286
|
+
chmodSync(dir, 448);
|
|
31287
|
+
}
|
|
31288
|
+
function writeImmutableJson(path, value) {
|
|
31289
|
+
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
31290
|
+
try {
|
|
31291
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
|
|
31292
|
+
`, { flag: "wx", mode: 384 });
|
|
31293
|
+
linkSync(tmp, path);
|
|
31294
|
+
} finally {
|
|
31295
|
+
rmSync(tmp, { force: true });
|
|
31296
|
+
}
|
|
31297
|
+
}
|
|
31298
|
+
function readJson(path) {
|
|
31299
|
+
try {
|
|
31300
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
31301
|
+
} catch {
|
|
31302
|
+
return void 0;
|
|
31303
|
+
}
|
|
31304
|
+
}
|
|
31305
|
+
function validIntent(value, runId) {
|
|
31306
|
+
return value?.version === 1 && value.action === "stop" && OPERATION_ID.test(value.operationId) && RUN_ID.test(value.runId) && (runId === void 0 || value.runId === runId) && typeof value.requestedAt === "string" && typeof value.requestedByInstanceId === "string";
|
|
31307
|
+
}
|
|
31308
|
+
function validAcknowledgement(value, runId, operationId) {
|
|
31309
|
+
return value?.version === 1 && value.runId === runId && value.operationId === operationId && typeof value.acknowledgedAt === "string" && typeof value.acknowledgedByInstanceId === "string" && (value.outcome === "stopped" || value.outcome === "already-terminal");
|
|
31310
|
+
}
|
|
31311
|
+
function findPendingWholeStopIntent(manager, runId) {
|
|
31312
|
+
let files;
|
|
31313
|
+
try {
|
|
31314
|
+
files = readdirSync(runDir(manager, runId)).filter((file2) => file2.endsWith(".request.json")).sort();
|
|
31315
|
+
} catch {
|
|
31316
|
+
return void 0;
|
|
31317
|
+
}
|
|
31318
|
+
for (const file2 of files) {
|
|
31319
|
+
const operationId = file2.slice(0, -".request.json".length);
|
|
31320
|
+
if (!OPERATION_ID.test(operationId)) continue;
|
|
31321
|
+
const intent = readJson(requestPath(manager, runId, operationId));
|
|
31322
|
+
if (!validIntent(intent, runId)) continue;
|
|
31323
|
+
const acknowledgement = readWholeStopAcknowledgement(manager, runId, operationId);
|
|
31324
|
+
if (acknowledgement === void 0) return intent;
|
|
31325
|
+
}
|
|
31326
|
+
return void 0;
|
|
31327
|
+
}
|
|
31328
|
+
function createOrReuseWholeStopIntent(manager, runId, requesterInstanceId) {
|
|
31329
|
+
const pending = findPendingWholeStopIntent(manager, runId);
|
|
31330
|
+
if (pending) return pending;
|
|
31331
|
+
ensureRunDir(manager, runId);
|
|
31332
|
+
for (; ; ) {
|
|
31333
|
+
const intent = {
|
|
31334
|
+
version: 1,
|
|
31335
|
+
operationId: randomUUID(),
|
|
31336
|
+
runId,
|
|
31337
|
+
action: "stop",
|
|
31338
|
+
requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31339
|
+
requestedByInstanceId: requesterInstanceId
|
|
31340
|
+
};
|
|
31341
|
+
try {
|
|
31342
|
+
writeImmutableJson(requestPath(manager, runId, intent.operationId), intent);
|
|
31343
|
+
return intent;
|
|
31344
|
+
} catch (error51) {
|
|
31345
|
+
if (error51.code !== "EEXIST") throw error51;
|
|
31346
|
+
}
|
|
31347
|
+
}
|
|
31348
|
+
}
|
|
31349
|
+
function readWholeStopIntent(manager, runId, operationId) {
|
|
31350
|
+
const value = readJson(requestPath(manager, runId, operationId));
|
|
31351
|
+
return validIntent(value, runId) && value.operationId === operationId ? value : void 0;
|
|
31352
|
+
}
|
|
31353
|
+
function readWholeStopAcknowledgement(manager, runId, operationId) {
|
|
31354
|
+
const value = readJson(acknowledgementPath(manager, runId, operationId));
|
|
31355
|
+
return validAcknowledgement(value, runId, operationId) ? value : void 0;
|
|
31356
|
+
}
|
|
31357
|
+
function acknowledgeWholeStopIntent(manager, intent, ownerInstanceId, outcome) {
|
|
31358
|
+
ensureRunDir(manager, intent.runId);
|
|
31359
|
+
const acknowledgement = {
|
|
31360
|
+
version: 1,
|
|
31361
|
+
operationId: intent.operationId,
|
|
31362
|
+
runId: intent.runId,
|
|
31363
|
+
acknowledgedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31364
|
+
acknowledgedByInstanceId: ownerInstanceId,
|
|
31365
|
+
outcome
|
|
31366
|
+
};
|
|
31367
|
+
const path = acknowledgementPath(manager, intent.runId, intent.operationId);
|
|
31368
|
+
try {
|
|
31369
|
+
writeImmutableJson(path, acknowledgement);
|
|
31370
|
+
return acknowledgement;
|
|
31371
|
+
} catch (error51) {
|
|
31372
|
+
const existing = readWholeStopAcknowledgement(manager, intent.runId, intent.operationId);
|
|
31373
|
+
if (existing) return existing;
|
|
31374
|
+
throw error51;
|
|
31375
|
+
}
|
|
31376
|
+
}
|
|
31377
|
+
function listPendingWholeStopIntents(manager) {
|
|
31378
|
+
let runIds;
|
|
31379
|
+
try {
|
|
31380
|
+
runIds = readdirSync(root(manager)).filter((entry) => RUN_ID.test(entry));
|
|
31381
|
+
} catch {
|
|
31382
|
+
return [];
|
|
31383
|
+
}
|
|
31384
|
+
const intents = [];
|
|
31385
|
+
for (const runId of runIds) {
|
|
31386
|
+
const pending = findPendingWholeStopIntent(manager, runId);
|
|
31387
|
+
if (pending) intents.push(pending);
|
|
31388
|
+
}
|
|
31389
|
+
return intents;
|
|
31390
|
+
}
|
|
31391
|
+
function deleteRunControlSidecars(manager, runId) {
|
|
31392
|
+
try {
|
|
31393
|
+
rmSync(runDir(manager, runId), { recursive: true, force: true });
|
|
31394
|
+
} catch {
|
|
31395
|
+
}
|
|
31396
|
+
}
|
|
31397
|
+
|
|
31243
31398
|
// ../mcp-server/src/project-registry.ts
|
|
31244
31399
|
var MAX_BACKGROUND_RUNS = 4;
|
|
31245
31400
|
var BackgroundRunRegistry = class {
|
|
@@ -31289,13 +31444,16 @@ function resolveProjectDir(raw) {
|
|
|
31289
31444
|
}
|
|
31290
31445
|
}
|
|
31291
31446
|
var WorkflowProjectRegistry = class {
|
|
31292
|
-
constructor(runner) {
|
|
31447
|
+
constructor(runner, options = {}) {
|
|
31293
31448
|
this.runner = runner;
|
|
31449
|
+
this.options = options;
|
|
31294
31450
|
}
|
|
31295
31451
|
runner;
|
|
31452
|
+
options;
|
|
31296
31453
|
contexts = /* @__PURE__ */ new Map();
|
|
31297
31454
|
deletionListeners = /* @__PURE__ */ new Set();
|
|
31298
31455
|
persistedEventListeners = /* @__PURE__ */ new Set();
|
|
31456
|
+
stoppedListeners = /* @__PURE__ */ new Set();
|
|
31299
31457
|
/** Adopt an externally built manager as its project's context (composition back-compat). */
|
|
31300
31458
|
adopt(manager, backgroundRuns) {
|
|
31301
31459
|
const existing = this.contexts.get(manager.cwd);
|
|
@@ -31312,18 +31470,26 @@ var WorkflowProjectRegistry = class {
|
|
|
31312
31470
|
if (existing !== void 0) return existing;
|
|
31313
31471
|
return this.register({
|
|
31314
31472
|
projectDir,
|
|
31315
|
-
manager: new WorkflowManager({
|
|
31473
|
+
manager: new WorkflowManager({
|
|
31474
|
+
agent: this.runner,
|
|
31475
|
+
cwd: projectDir,
|
|
31476
|
+
leaseOwnerId: this.options.leaseOwnerId
|
|
31477
|
+
}),
|
|
31316
31478
|
backgroundRuns: new BackgroundRunRegistry()
|
|
31317
31479
|
});
|
|
31318
31480
|
}
|
|
31319
31481
|
register(context) {
|
|
31320
31482
|
this.contexts.set(context.projectDir, context);
|
|
31321
31483
|
context.manager.on("runDeleted", (event) => {
|
|
31484
|
+
deleteRunControlSidecars(context.manager, event.runId);
|
|
31322
31485
|
for (const listener of this.deletionListeners) listener(event);
|
|
31323
31486
|
});
|
|
31324
31487
|
context.manager.on("runEventPersisted", (record2) => {
|
|
31325
31488
|
for (const listener of this.persistedEventListeners) listener(record2);
|
|
31326
31489
|
});
|
|
31490
|
+
context.manager.on("stopped", (event) => {
|
|
31491
|
+
for (const listener of this.stoppedListeners) listener(event);
|
|
31492
|
+
});
|
|
31327
31493
|
return context;
|
|
31328
31494
|
}
|
|
31329
31495
|
storeFor(runId) {
|
|
@@ -31339,18 +31505,18 @@ var WorkflowProjectRegistry = class {
|
|
|
31339
31505
|
* project's next engine construction).
|
|
31340
31506
|
*/
|
|
31341
31507
|
locateOnDisk(runId) {
|
|
31342
|
-
const projectsDir =
|
|
31508
|
+
const projectsDir = join2(workflowHomeDir(), WORKFLOW_PROJECTS_SUBDIR);
|
|
31343
31509
|
let keys;
|
|
31344
31510
|
try {
|
|
31345
|
-
keys =
|
|
31511
|
+
keys = readdirSync2(projectsDir);
|
|
31346
31512
|
} catch {
|
|
31347
31513
|
return void 0;
|
|
31348
31514
|
}
|
|
31349
31515
|
for (const key of keys) {
|
|
31350
|
-
const rootDir =
|
|
31516
|
+
const rootDir = join2(projectsDir, key);
|
|
31351
31517
|
try {
|
|
31352
|
-
if (!existsSync2(
|
|
31353
|
-
const manifest = JSON.parse(
|
|
31518
|
+
if (!existsSync2(join2(rootDir, "runs", `${runId}.json`))) continue;
|
|
31519
|
+
const manifest = JSON.parse(readFileSync2(join2(rootDir, "project.json"), "utf-8"));
|
|
31354
31520
|
if (typeof manifest.projectDir !== "string" || !isAbsolute2(manifest.projectDir)) continue;
|
|
31355
31521
|
return this.getOrCreate(manifest.projectDir);
|
|
31356
31522
|
} catch {
|
|
@@ -31370,9 +31536,13 @@ var WorkflowProjectRegistry = class {
|
|
|
31370
31536
|
this.persistedEventListeners.add(listener);
|
|
31371
31537
|
return () => this.persistedEventListeners.delete(listener);
|
|
31372
31538
|
}
|
|
31539
|
+
onRunStopped(listener) {
|
|
31540
|
+
this.stoppedListeners.add(listener);
|
|
31541
|
+
return () => this.stoppedListeners.delete(listener);
|
|
31542
|
+
}
|
|
31373
31543
|
activeRunCount() {
|
|
31374
31544
|
let total = 0;
|
|
31375
|
-
for (const context of this.contexts.values()) total += context.
|
|
31545
|
+
for (const context of this.contexts.values()) total += context.manager.activeExecutionCount();
|
|
31376
31546
|
return total;
|
|
31377
31547
|
}
|
|
31378
31548
|
/** Dispose every context's REPL workspace: each one DRAINS with the
|
|
@@ -31407,7 +31577,7 @@ var WorkflowProjectRegistry = class {
|
|
|
31407
31577
|
snapshot() {
|
|
31408
31578
|
return [...this.contexts.values()].map((context) => ({
|
|
31409
31579
|
projectDir: context.projectDir,
|
|
31410
|
-
activeRuns: context.
|
|
31580
|
+
activeRuns: context.manager.activeExecutionCount()
|
|
31411
31581
|
}));
|
|
31412
31582
|
}
|
|
31413
31583
|
};
|
|
@@ -31424,6 +31594,10 @@ function singleStoreRouter(manager) {
|
|
|
31424
31594
|
onRunEventPersisted: (listener) => {
|
|
31425
31595
|
manager.on("runEventPersisted", listener);
|
|
31426
31596
|
return () => manager.off("runEventPersisted", listener);
|
|
31597
|
+
},
|
|
31598
|
+
onRunStopped: (listener) => {
|
|
31599
|
+
manager.on("stopped", listener);
|
|
31600
|
+
return () => manager.off("stopped", listener);
|
|
31427
31601
|
}
|
|
31428
31602
|
};
|
|
31429
31603
|
}
|
|
@@ -31484,7 +31658,8 @@ var workflowRunLimitsSchema = external_exports.object({
|
|
|
31484
31658
|
tokenBudget: external_exports.number().nonnegative().nullable(),
|
|
31485
31659
|
concurrency: external_exports.number().int().positive(),
|
|
31486
31660
|
agentRetries: external_exports.number().int().nonnegative(),
|
|
31487
|
-
agentTimeoutMs: external_exports.number().nonnegative().nullable()
|
|
31661
|
+
agentTimeoutMs: external_exports.number().nonnegative().nullable(),
|
|
31662
|
+
agentIdleTimeoutMs: external_exports.number().nonnegative().nullable()
|
|
31488
31663
|
});
|
|
31489
31664
|
var authContextSchema = external_exports.object({
|
|
31490
31665
|
backendId: external_exports.string().optional(),
|
|
@@ -31607,7 +31782,7 @@ var resumeReportSchema = external_exports.discriminatedUnion("strategy", [
|
|
|
31607
31782
|
})
|
|
31608
31783
|
]);
|
|
31609
31784
|
var replayOperationalChangeSchema = external_exports.object({
|
|
31610
|
-
option: external_exports.enum(["agentTimeoutMs", "agentRetries", "concurrency"]),
|
|
31785
|
+
option: external_exports.enum(["agentTimeoutMs", "agentIdleTimeoutMs", "agentRetries", "concurrency"]),
|
|
31611
31786
|
source: external_exports.number().nullable(),
|
|
31612
31787
|
current: external_exports.number().nullable(),
|
|
31613
31788
|
detail: external_exports.string()
|
|
@@ -31699,6 +31874,7 @@ var runStatusShape = {
|
|
|
31699
31874
|
model: external_exports.string().optional(),
|
|
31700
31875
|
backendId: external_exports.string().optional(),
|
|
31701
31876
|
timeoutMs: external_exports.number().nonnegative().nullable().optional(),
|
|
31877
|
+
idleTimeoutMs: external_exports.number().nonnegative().nullable().optional(),
|
|
31702
31878
|
errorCode: external_exports.string().optional(),
|
|
31703
31879
|
status: external_exports.enum(["queued", "running"]).optional(),
|
|
31704
31880
|
resultPreview: external_exports.string(),
|
|
@@ -31851,6 +32027,19 @@ var discoveryOutputFields = [
|
|
|
31851
32027
|
"omittedHarnesses",
|
|
31852
32028
|
"models"
|
|
31853
32029
|
];
|
|
32030
|
+
var stopControlSchema = external_exports.object({
|
|
32031
|
+
state: external_exports.literal("pending"),
|
|
32032
|
+
operationId: external_exports.string(),
|
|
32033
|
+
requestedAt: external_exports.string(),
|
|
32034
|
+
owner: external_exports.object({
|
|
32035
|
+
pid: external_exports.number().int().positive(),
|
|
32036
|
+
instanceId: external_exports.string().optional(),
|
|
32037
|
+
version: external_exports.string().optional(),
|
|
32038
|
+
lameDuck: external_exports.boolean().optional(),
|
|
32039
|
+
activeRuns: external_exports.number().int().nonnegative().optional(),
|
|
32040
|
+
controlProtocol: external_exports.literal(1).optional()
|
|
32041
|
+
}).optional()
|
|
32042
|
+
});
|
|
31854
32043
|
var variantOutputFields = [
|
|
31855
32044
|
...executionDetailFields,
|
|
31856
32045
|
"scriptSource",
|
|
@@ -31859,6 +32048,7 @@ var variantOutputFields = [
|
|
|
31859
32048
|
"outcome",
|
|
31860
32049
|
"stopped",
|
|
31861
32050
|
"alreadyTerminal",
|
|
32051
|
+
"control",
|
|
31862
32052
|
...discoveryOutputFields
|
|
31863
32053
|
];
|
|
31864
32054
|
var forbidsRequired = (...fields) => ({
|
|
@@ -31900,23 +32090,26 @@ var workflowToolOutputShape = external_exports.object({
|
|
|
31900
32090
|
wait: waitSchema.optional(),
|
|
31901
32091
|
outcome: executionResultSchema.optional(),
|
|
31902
32092
|
stopped: external_exports.boolean().optional(),
|
|
31903
|
-
alreadyTerminal: external_exports.boolean().optional()
|
|
32093
|
+
alreadyTerminal: external_exports.boolean().optional(),
|
|
32094
|
+
control: stopControlSchema.optional()
|
|
31904
32095
|
}).superRefine((value, context) => {
|
|
31905
32096
|
const has = (field) => value[field] !== void 0;
|
|
31906
32097
|
const inspectionComplete = inspectionRequired.every((field) => has(field));
|
|
31907
32098
|
const runCommonComplete = has("runId") && has("status") && has("scriptUri");
|
|
31908
|
-
const
|
|
32099
|
+
const terminal2 = terminalStatuses.includes(value.status);
|
|
31909
32100
|
let valid;
|
|
31910
32101
|
if (value.action === "config") {
|
|
31911
32102
|
valid = has("ok") && has("harnessOptions") && has("omittedHarnesses") && has("models") && hasOnlyExactFields(value, ["action", "ok", "harnessOptions", "omittedHarnesses", "models"]);
|
|
31912
32103
|
} else if (value.action === "run") {
|
|
31913
32104
|
valid = value.status === "rejected" && has("validation") && hasOnlyExactFields(value, ["action", "status", "validation"]);
|
|
31914
32105
|
} else if (has("scriptSource")) {
|
|
31915
|
-
valid = runCommonComplete && has("limits") && (value.status === "running" ? hasOnlyFields(value, ["scriptSource"]) :
|
|
32106
|
+
valid = runCommonComplete && has("limits") && (value.status === "running" ? hasOnlyFields(value, ["scriptSource"]) : terminal2 && hasOnlyFields(value, ["scriptSource", ...executionDetailFields]));
|
|
32107
|
+
} else if (has("control")) {
|
|
32108
|
+
valid = runCommonComplete && inspectionComplete && value.stopped === false && value.alreadyTerminal === false && (value.status === "pending" || value.status === "running") && hasOnlyFields(value, [...inspectionFields, "stopped", "alreadyTerminal", "control"]);
|
|
31916
32109
|
} else if (has("stopped") || has("alreadyTerminal")) {
|
|
31917
32110
|
valid = runCommonComplete && inspectionComplete && has("stopped") && has("alreadyTerminal") && (value.status === "completed" || value.status === "failed" || value.status === "aborted") && hasOnlyFields(value, [...inspectionFields, "stopped", "alreadyTerminal"]);
|
|
31918
32111
|
} else if (has("wait")) {
|
|
31919
|
-
valid = runCommonComplete && inspectionComplete && hasOnlyFields(value, [...inspectionFields, "wait", "tokenUsage", "outcome"]) && (
|
|
32112
|
+
valid = runCommonComplete && inspectionComplete && hasOnlyFields(value, [...inspectionFields, "wait", "tokenUsage", "outcome"]) && (terminal2 ? has("outcome") : !has("outcome"));
|
|
31920
32113
|
} else {
|
|
31921
32114
|
valid = runCommonComplete && inspectionComplete && hasOnlyFields(value, inspectionFields);
|
|
31922
32115
|
}
|
|
@@ -31940,7 +32133,8 @@ var workflowToolOutputShape = external_exports.object({
|
|
|
31940
32133
|
"wait",
|
|
31941
32134
|
"outcome",
|
|
31942
32135
|
"stopped",
|
|
31943
|
-
"alreadyTerminal"
|
|
32136
|
+
"alreadyTerminal",
|
|
32137
|
+
"control"
|
|
31944
32138
|
)
|
|
31945
32139
|
},
|
|
31946
32140
|
{
|
|
@@ -31960,7 +32154,8 @@ var workflowToolOutputShape = external_exports.object({
|
|
|
31960
32154
|
"wait",
|
|
31961
32155
|
"outcome",
|
|
31962
32156
|
"stopped",
|
|
31963
|
-
"alreadyTerminal"
|
|
32157
|
+
"alreadyTerminal",
|
|
32158
|
+
"control"
|
|
31964
32159
|
)
|
|
31965
32160
|
},
|
|
31966
32161
|
{
|
|
@@ -32000,6 +32195,16 @@ var workflowToolOutputShape = external_exports.object({
|
|
|
32000
32195
|
required: [...runOutputRequired, ...inspectionRequired, "stopped", "alreadyTerminal"],
|
|
32001
32196
|
properties: { status: { enum: ["completed", "failed", "aborted"] } },
|
|
32002
32197
|
...forbidsOutside([...inspectionFields, "stopped", "alreadyTerminal"])
|
|
32198
|
+
},
|
|
32199
|
+
{
|
|
32200
|
+
title: "Workflow stop pending",
|
|
32201
|
+
required: [...runOutputRequired, ...inspectionRequired, "stopped", "alreadyTerminal", "control"],
|
|
32202
|
+
properties: {
|
|
32203
|
+
status: { enum: nonterminalStatuses },
|
|
32204
|
+
stopped: { const: false },
|
|
32205
|
+
alreadyTerminal: { const: false }
|
|
32206
|
+
},
|
|
32207
|
+
...forbidsOutside([...inspectionFields, "stopped", "alreadyTerminal", "control"])
|
|
32003
32208
|
}
|
|
32004
32209
|
]
|
|
32005
32210
|
});
|
|
@@ -32280,9 +32485,9 @@ var AUTHORING_DOC_TOPICS = [
|
|
|
32280
32485
|
"workflow/determinism-and-resume",
|
|
32281
32486
|
"workflow/models-and-config"
|
|
32282
32487
|
],
|
|
32283
|
-
"bytes":
|
|
32284
|
-
"sha256": "
|
|
32285
|
-
"text": '## Running workflows \u2014 the MCP `workflow` tool\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\nUse the connected `workflow` tool for deterministic batch orchestration. The shared server daemon owns execution, so admitted runs survive MCP client session churn and tool-request timeouts;
|
|
32488
|
+
"bytes": 7983,
|
|
32489
|
+
"sha256": "55bae1f2f722adc7e9fd856f2d673f21ef90805e72473162750ee48dfab83744",
|
|
32490
|
+
"text": '## Running workflows \u2014 the MCP `workflow` tool\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\nUse the connected `workflow` tool for deterministic batch orchestration. The shared server daemon owns execution, so admitted runs survive MCP client session churn and tool-request timeouts. During a version upgrade, the successor becomes the front door while a predecessor may remain the execution owner; signed run-control forwarding keeps later-session stop/cancel operations location-independent. Owner-process exit can still interrupt in-flight work. Any later session can await, inspect, or stop a run. Runs, journals, logs, and outstanding whole-stop intents persist per project namespace.\n\nEvery `config` and `run` call on the shared daemon names its project with the required `projectDir` argument \u2014 an absolute path, normally the workspace root. `inspect`/`await`/`stop` take only a `runId`; the run ID locates its project store automatically. In a single-project server, `projectDir` defaults to that server\'s project.\n\n### The `workflow` tool, by action\n\n- **Config** (`{ action: "config", projectDir, harnesses?, modelSpecs?, modelFilter? }`): discover live model, mode, effort, and `configOptions` values from no-prompt backend sessions. Use `harnesses` plus `modelFilter` to find ids, then `modelSpecs` to select exact models and read their model-specific option domains. Each successful entry reports `modes` explicitly: use only exact ids in `modes.availableModes`; `modes:null` means omit `mode`, never guess a default. It starts no workflow and spends zero tokens. Use it only when pinning those values; an omitted model or backend-only model uses configured defaults without discovery.\n- **Run** (default, no `action`): supply exactly one of `script` (the raw source string, no Markdown fences) or `scriptPath` (an absolute path on the server\'s filesystem), plus `projectDir`. The tool automatically performs static validation, a mocked dry run, and routed config checks before admission. Invalid scripts return bounded `status:"rejected"` diagnostics with no run ID, background slot, or token spend. A path is read once at admission and its content snapshotted; later edits affect only a new run. `args` arrives in the script as the `args` global; the run\'s base directory is the `cwd` global. Some hosts hand `args` through as a JSON **string** \u2014 tolerate both shapes (`typeof args === "string" ? JSON.parse(args) : args`). Foreground streams progress but is bound to the request and its timeout. Pass `background: true` for anything that may outlive one request; it acknowledges after durable admission with a `runId`.\n- **Await** (`{ action: "await", runId, waitMs }`): bounded collection for background runs. A timeout is progress, not failure \u2014 call again (`waitMs: 20000` is typical). At terminal status the response adds `outcome`: the authored result or pause context, plus `replayEligibility`, `resumeReport`, `fallbacks`, and `checkpointsTaken`.\n- **Inspect** (`{ action: "inspect", runId, lastN, labelGlob, logLines }`): a bounded snapshot \u2014 the latest matching calls with compact result previews plus the newest log lines. Use a narrow `labelGlob` to diagnose before deciding whether to resume, edit, or stop. Inspection never executes or resumes a script.\n- **Stop**: `{ action: "stop", runId }` durably aborts the whole run and normally returns its final snapshot; stopping a terminal run is a successful no-op. Across a daemon upgrade, the successor persists an idempotent stop intent and forwards to the predecessor that owns execution. If that owner does not settle within the bounded control wait, the successful response remains nonterminal with `control:{ state:"pending", operationId, requestedAt, owner? }`; retry stop, inspect, or await to observe settlement. `{ action: "stop", runId, callIndex }` synchronously routes to the live owner and cancels exactly that in-flight agent: its slot settles to `null` with `AGENT_CANCELLED` and the run stays live; call cancellation is never reconstructed after owner loss. Whole-run `{ action:"stop", runId, forceOwner:true }` explicitly authorizes terminating a superseded owner daemon when graceful control cannot settle and may interrupt sibling runs in that process; it is forbidden with `callIndex`. `labelGlob` only filters the returned snapshot; it never selects what to cancel.\n- **Resume**: a NEW run with `resumeFromRunId` plus the script content re-sent (the same `script` or `scriptPath`) and the desired `args` (+ `checkpointReplies` when answering a durable checkpoint). Read the returned `replayEligibility` for the predicted and observed replay prefix; never assume a prefix hit. Full semantics: **Determinism and resume**.\n\n### Operating rules\n\n- **Always retain the returned `runId`.** A paused, failed, or aborted response carries a redacted final-20 `logTail`. Read it before you change anything. Every admitted script is also an immutable resource at `workflow://runs/{runId}/script`, so a later session can recover a lost inline script.\n- **Two fingerprints control replay.** The identity hash covers the prompt, the resolved model, `mode` when set, non-empty sorted `configOptions`, `tier`, `phase`, `agentType`, the resolved agent definition, and the schema. The input fingerprint covers the resolved label, per-call `cwd` and isolation, `keepSession`, images, MCP servers, session/prompt metadata, and the approved script-backend digest.\n- **Operational bounds are not replay inputs.** Host `concurrency`, `agentRetries`, `agentTimeoutMs`, and `agentIdleTimeoutMs`, plus per-call `timeoutMs`, `idleTimeoutMs`, and `retries`, enter neither fingerprint. A resume does not inherit them from its source run; pass the values you want on every run. `agentTimeoutMs` caps total wall time and is not an idle timer. The separate opt-in `agentIdleTimeoutMs` fires after that long without real backend activity; ACP `session/update` re-arms it and synthetic progress heartbeats do not. Per-call values can tighten but cannot escape finite host ceilings. Every retry gets fresh clocks.\n- **Old journals stay usable.** Input formats below 2 replay positionally with `fallbackReason: "inputs-format-legacy"`. A current-format crash snapshot uses identity matching even without terminal-environment capture. Ancestor-scoped rows carried from \u22640.23 resume chains replay only while that ancestor run is still persisted. Journals resume across filesystem, environment, engine, Node, and V8 changes; `replayEligibility` reports those differences as diagnostics, never as gates.\n- **A background start returns immediately.** It sends no progress after it returns; collect progress with later bounded awaits. Background runs have no live checkpoint channel, so authored `headless` checkpoint modes apply. When a run\'s owner process dies, a pending whole-stop intent is applied under the reclaimed lease; otherwise cold preflights reconcile stale `pending`/`running` state to `paused` with `pauseReason:"interrupted"`. A live owner lease is never stolen because of a timeout.\n- A run paused with `reason: "auth_required"` resumes as a new run after that backend\'s credentials are configured.\n\n### Execution logs \u2014 the events resource\n\nEvery journaling run publishes an MCP resource at `workflow://runs/{runId}/events`. Subscribe to the canonical URI for advisory `resources/updated` hints, then read and paginate with `after`, `limit`, and `streamId`. Progress is coarse and redacted: `agentTranscript` rows are assistant/tool upserts partitioned by `(scope, callIndex, executionStartSeq)` and reduced by greatest revision per entry index. The durable cursor is authoritative when hints coalesce or a subscriber falls behind.\n\nEmbedding hosts can drive the same contract with `runDynamicWorkflow` / `WorkflowManager` from `@automatalabs/workflows`; the script contract is identical either way.\n'
|
|
32286
32491
|
},
|
|
32287
32492
|
{
|
|
32288
32493
|
"id": "workflow/models-and-config",
|
|
@@ -32310,9 +32515,9 @@ var AUTHORING_DOC_TOPICS = [
|
|
|
32310
32515
|
"workflow/api-control-flow",
|
|
32311
32516
|
"workflow/checkpoints-and-quality"
|
|
32312
32517
|
],
|
|
32313
|
-
"bytes":
|
|
32314
|
-
"sha256": "
|
|
32315
|
-
"text": '## The `meta` header\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\nEvery script must **begin** with `export const meta = {...}` as a plain object literal (no computed values \u2014 it is parsed from the source text before anything runs):\n\n```js\nexport const meta = {\n name: "fix-flaky-tests", // required\n description: "Find flaky tests and fix them", // required\n phases: [ // optional; one { title, detail?, model? } entry\n { title: "Find", model: "opencode/zai/glm-5.2" }, // per phase() call, matched by exact title;\n { title: "Fix" }, // a phase model is that phase\'s default\n ],\n model: "claude/sonnet", // optional run-wide default model\n backends: { /* optional custom ACP agents \u2014 see "Custom ACP backends" */ },\n};\n```\n\nPer-agent model resolution order: explicit `agent({ model })` > `agent({ tier })` > the current phase\'s `model` > `meta.model` > the host session\'s default. So `meta.phases[].model` gives a whole phase a backend without repeating it on every call.\n\n## Fan-out: `parallel` and `pipeline`\n\n```js\n// parallel: an array of THUNKS (not promises!) run concurrently \u2014 a barrier that\n// resolves in input order. A failed slot resolves to null; filter before use.\nconst sweeps = (await parallel([\n () => agent("Audit error handling in src/server", { label: "sweep:errors", schema: FINDINGS }),\n () => agent("Audit input validation in src/api", { label: "sweep:input", schema: FINDINGS }),\n])).filter(Boolean);\n\n// pipeline: each item flows through the stages independently \u2014 NO barrier between\n// stages, so item A can be in stage 2 while item B is still in stage 1.\n// Stages receive (previousResult, originalItem, index).\nconst verified = (await pipeline(\n sweeps.flatMap((s) => s.findings),\n (f) => agent(`Adversarially verify this finding \u2014 try to refute it:\\n${JSON.stringify(f)}`,\n { label: `verify:${f.file}`, schema: VERDICT }),\n (verdict, f) => ({ ...f, real: verdict.real }),\n)).filter(Boolean).filter((f) => f.real);\n```\n\n**Default to `pipeline`** for multi-stage work. Add a `parallel` barrier only when the next stage needs *all* prior results at once: dedup across the full set, early-exit on a zero count, or prompts that compare "the other findings". The test is the **information dependency** \u2014 a barrier\'s cost is real, because the fastest worker idles for the slowest. All coordination lives in script code: agents cannot see each other, so never ask an agent to "check with the other reviewers" or "spawn helpers". Passing a promise instead of a thunk to `parallel` is a `TypeError` \u2014 wrap every call: `() => agent(...)`.\n\nFan-out also contends for the **working tree**, not just the concurrency limiter. Two agents running builds or test suites in the same checkout collide on build outputs, caches, and lockfiles, and concurrent `git fetch`es contend on the same `.git`. Give run-things agents `isolation: "worktree"` when the commits they must inspect are reachable from the run cwd\'s repository, or serialize them; fan out freely only the agents that just read.\n\nThe host caps concurrent agents per run (default 8); hand `parallel`/`pipeline` as many items as the task needs and let the limiter schedule them. The cap counts active agent attempts, not authored branches: queued branches begin as other attempts finish, and a branch that exhausts its timeout settles to `null` and frees its slot. `workflow(nameOrScript, args)` nests another workflow inline (one level deep, sharing this run\'s limiter) \u2014 inline script strings always work; saved names resolve when the host serves a workflows folder.\n\n## Failure semantics \u2014 design for `null`\n\n- A **recoverable** failure (timeout, empty output, transient execution error) is retried per the call\'s `retries` (default 0), then the call **resolves to `null`** \u2014 inside `parallel`/`pipeline` *and* as a bare `await agent(...)`. Null-check anything load-bearing, and set `retries: 1\u20132` on steps you can\'t afford to lose.\n- A host can settle one runaway in-flight call with MCP `{ action: "stop", runId, callIndex }` or SDK `manager.cancelAgentCall(runId, callIndex)`. The call resolves to `null` with `AGENT_CANCELLED`, skips every configured retry, and does not abort the run or its siblings. Its failed call record is not cached as a journal result, so a later resume runs that occurrence live.\n- A **non-recoverable** failure (schema never validated, script bug) throws and fails the run. You *may* `try/catch` around an `agent()` call to degrade gracefully \u2014 rethrow anything you can\'t meaningfully handle. In particular, **always rethrow pause-class errors** (`err.code === "PROVIDER_USAGE_LIMIT"` or `"AUTH_REQUIRED"`): they must propagate out of the script so the engine can pause the run resumably \u2014 swallowing one converts that pause into a fake, lossy completion.\n- A **provider quota wall, missing backend authentication, or opted-in durable checkpoint pauses a managed run instead of failing it** \u2014 the journal checkpoints and the host can resume after the provider quota refills, authentication completes, or a checkpoint decision is supplied. Direct `runner.run()` calls still receive the `AUTH_REQUIRED` error because they have no manager lifecycle.\n- Per-call knobs: `timeoutMs` and `retries`.
|
|
32518
|
+
"bytes": 6494,
|
|
32519
|
+
"sha256": "08569d83a9662ca094c3324a3fc737e4670876948ba863f04b01cca566afc2c0",
|
|
32520
|
+
"text": '## The `meta` header\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\nEvery script must **begin** with `export const meta = {...}` as a plain object literal (no computed values \u2014 it is parsed from the source text before anything runs):\n\n```js\nexport const meta = {\n name: "fix-flaky-tests", // required\n description: "Find flaky tests and fix them", // required\n phases: [ // optional; one { title, detail?, model? } entry\n { title: "Find", model: "opencode/zai/glm-5.2" }, // per phase() call, matched by exact title;\n { title: "Fix" }, // a phase model is that phase\'s default\n ],\n model: "claude/sonnet", // optional run-wide default model\n backends: { /* optional custom ACP agents \u2014 see "Custom ACP backends" */ },\n};\n```\n\nPer-agent model resolution order: explicit `agent({ model })` > `agent({ tier })` > the current phase\'s `model` > `meta.model` > the host session\'s default. So `meta.phases[].model` gives a whole phase a backend without repeating it on every call.\n\n## Fan-out: `parallel` and `pipeline`\n\n```js\n// parallel: an array of THUNKS (not promises!) run concurrently \u2014 a barrier that\n// resolves in input order. A failed slot resolves to null; filter before use.\nconst sweeps = (await parallel([\n () => agent("Audit error handling in src/server", { label: "sweep:errors", schema: FINDINGS }),\n () => agent("Audit input validation in src/api", { label: "sweep:input", schema: FINDINGS }),\n])).filter(Boolean);\n\n// pipeline: each item flows through the stages independently \u2014 NO barrier between\n// stages, so item A can be in stage 2 while item B is still in stage 1.\n// Stages receive (previousResult, originalItem, index).\nconst verified = (await pipeline(\n sweeps.flatMap((s) => s.findings),\n (f) => agent(`Adversarially verify this finding \u2014 try to refute it:\\n${JSON.stringify(f)}`,\n { label: `verify:${f.file}`, schema: VERDICT }),\n (verdict, f) => ({ ...f, real: verdict.real }),\n)).filter(Boolean).filter((f) => f.real);\n```\n\n**Default to `pipeline`** for multi-stage work. Add a `parallel` barrier only when the next stage needs *all* prior results at once: dedup across the full set, early-exit on a zero count, or prompts that compare "the other findings". The test is the **information dependency** \u2014 a barrier\'s cost is real, because the fastest worker idles for the slowest. All coordination lives in script code: agents cannot see each other, so never ask an agent to "check with the other reviewers" or "spawn helpers". Passing a promise instead of a thunk to `parallel` is a `TypeError` \u2014 wrap every call: `() => agent(...)`.\n\nFan-out also contends for the **working tree**, not just the concurrency limiter. Two agents running builds or test suites in the same checkout collide on build outputs, caches, and lockfiles, and concurrent `git fetch`es contend on the same `.git`. Give run-things agents `isolation: "worktree"` when the commits they must inspect are reachable from the run cwd\'s repository, or serialize them; fan out freely only the agents that just read.\n\nThe host caps concurrent agents per run (default 8); hand `parallel`/`pipeline` as many items as the task needs and let the limiter schedule them. The cap counts active agent attempts, not authored branches: queued branches begin as other attempts finish, and a branch that exhausts its timeout settles to `null` and frees its slot. `workflow(nameOrScript, args)` nests another workflow inline (one level deep, sharing this run\'s limiter) \u2014 inline script strings always work; saved names resolve when the host serves a workflows folder.\n\n## Failure semantics \u2014 design for `null`\n\n- A **recoverable** failure (timeout, empty output, transient execution error) is retried per the call\'s `retries` (default 0), then the call **resolves to `null`** \u2014 inside `parallel`/`pipeline` *and* as a bare `await agent(...)`. Null-check anything load-bearing, and set `retries: 1\u20132` on steps you can\'t afford to lose.\n- A host can settle one runaway in-flight call with MCP `{ action: "stop", runId, callIndex }` or SDK `manager.cancelAgentCall(runId, callIndex)`. The call resolves to `null` with `AGENT_CANCELLED`, skips every configured retry, and does not abort the run or its siblings. Its failed call record is not cached as a journal result, so a later resume runs that occurrence live.\n- A **non-recoverable** failure (schema never validated, script bug) throws and fails the run. You *may* `try/catch` around an `agent()` call to degrade gracefully \u2014 rethrow anything you can\'t meaningfully handle. In particular, **always rethrow pause-class errors** (`err.code === "PROVIDER_USAGE_LIMIT"` or `"AUTH_REQUIRED"`): they must propagate out of the script so the engine can pause the run resumably \u2014 swallowing one converts that pause into a fake, lossy completion.\n- A **provider quota wall, missing backend authentication, or opted-in durable checkpoint pauses a managed run instead of failing it** \u2014 the journal checkpoints and the host can resume after the provider quota refills, authentication completes, or a checkpoint decision is supplied. Direct `runner.run()` calls still receive the `AUTH_REQUIRED` error because they have no manager lifecycle.\n- Per-call knobs: `timeoutMs`, `idleTimeoutMs`, and `retries`. `timeoutMs` is total wall-clock time; `idleTimeoutMs` is opt-in no-backend-activity detection. Each may shorten but cannot bypass its finite host `agentTimeoutMs` / `agentIdleTimeoutMs` ceiling. Real ACP `session/update` traffic resets idle; synthetic progress heartbeats do not. Every retry gets fresh clocks.\n\n## Phases\n\n```js\nphase("Explore"); // open a named phase: subsequent agents group under it\n\nconst found = [];\nwhile (found.length < 20) {\n const r = await agent("Find one more edge case not in: " + JSON.stringify(found.map((f) => f.name)),\n { label: `edge:${found.length}`, schema: EDGE });\n if (!r) break;\n found.push(r);\n}\n```\n\nTerminate every loop on a bound the script controls. The agent-count limit (`maxAgents`) is hard: once exhausted, further `agent()` calls throw `AGENT_LIMIT_EXCEEDED`. `phase()` groups agents in progress UIs and run logs; `log(msg)` (and `console.log`) append to the run log \u2014 narrate what matters, especially anything you drop.\n'
|
|
32316
32521
|
},
|
|
32317
32522
|
{
|
|
32318
32523
|
"id": "workflow/checkpoints-and-quality",
|
|
@@ -32355,9 +32560,9 @@ var AUTHORING_DOC_TOPICS = [
|
|
|
32355
32560
|
"workflow/api-resume-and-backends",
|
|
32356
32561
|
"workflow/examples"
|
|
32357
32562
|
],
|
|
32358
|
-
"bytes":
|
|
32359
|
-
"sha256": "
|
|
32360
|
-
"text": '## Determinism and resume\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\nRuns are journaled: every `agent()` and `checkpoint()` result is recorded under a deterministic call index. A new run may reuse eligible results from a terminal source run. Uncertainty always means live execution.\n\n> **Resume rule:** replay is content-addressed and fail-to-live on correspondence: a completed call replays when its identity and input fingerprint match uniquely. Filesystem or world state never gates replay.\n\n- Direct `Date.now()`, `Math.random()`, and no-arg `new Date()` / `Date()` calls fail static validation. The realm also blocks aliased or computed forms at runtime; `new Date(isoString)` is fine. Pass timestamps and random seeds through `args`.\n- The replay identity of an `agent()` call hashes: the prompt, the resolved `model`, `mode` when set, `configOptions` when non-empty (sorted keys), `tier`, `phase`, `agentType`, the resolved agent definition, and `schema`. The resolved agent definition includes its tool allowlist and denylist, model, isolation, and body prompt \u2014 editing a definition invalidates the calls that use it.\n- A separate input fingerprint hashes: the resolved label, per-call `cwd`, resolved isolation, `keepSession`, `images`, `mcpServers`, `meta`, `promptMeta`, and the approved script-backend digest.\n- Host `agentTimeoutMs`, `agentRetries`, and `concurrency`, plus per-call `timeoutMs` and `retries`, are operational bounds. They enter neither hash and may change freely on resume. A new run resolves them from its own request; it does not inherit the source values.\n- `args` is not hashed directly. New args that only raise a loop cap leave earlier identities unchanged, so those calls can replay. New args that change a prompt, model selection, phase, schema, call order, or runner-visible input make the affected calls run live. Unchanged independent calls may still replay.\n- Matching tries a unique exact `(kind, call path, identity hash)` row first (`"path-hash"`), then a unique `(kind, identity hash, input fingerprint)` row, so an unchanged call can replay as `"unique-hash"` after insertions or deletions. Source and current input fingerprints must be equal. Duplicate identities, duplicate content, consumed candidates, missing facts, and empty schema-less results run live. The engine never guesses by source order or occurrence.\n- Source admission requires: exact `cwd`, compatible call-path/input/checkpoint fingerprint formats, complete call/journal/allocation metadata, and a valid manifest and seed. Git HEAD and dirty digest, `environmentKey`, captured environment values, Node/V8, and producing engine version are diagnostics only. Environment differences may appear in `replayEligibility.provenanceChanges`; they never gate admission or matching.\n- A completed writer replays exactly like a reader. A live call, nested workflow, host checkpoint callback, or degraded worktree does not clear unrelated candidates. Nested child calls run live \u2014 they are outside the parent\'s journal \u2014 while matching root calls around them still replay. The engine does not reproduce file writes; a later live agent navigates the world it finds.\n- Replay costs zero current provider usage: a cached call returns its recorded result without spawning a session. Replayed session records keep their backend and session identity, rebound to the current call index, label, and phase.\n- A root call interrupted by `PROVIDER_USAGE_LIMIT` or `AUTH_REQUIRED` can continue its recorded session on either resume API. Continuation requires: the exact call index, identity hash, complete input fingerprint, non-worktree isolation, identical existing cwd, a coherent recorded session, and the runner\'s current backend/`poolKey`/reopen gates. A successful continuation finishes the unfinished turn and charges only its usage delta. Every failed gate runs fresh, and `fallbacks` records the reopen method or the exact skip reason. No script option controls this.\n- Completed checkpoint results replay when the identity and the `default`/`headless`/`timeoutMs` fingerprint match \u2014 headless results included. `checkpointReplies` keys always name the checkpoint index in the source run. A moved reply can follow intact prior correspondence; after a live divergence it must reach the exact recorded call site, so a different same-text branch cannot consume it.\n- `resumePolicy: "positional"` is a migration escape hatch for index/prefix matching. It cannot bypass format, metadata, manifest, cwd, or input checks. Marker-less, manual, and same-ID legacy journals keep historical hash-only positional behavior. Input formats below 2 use the `inputs-format-legacy` positional bridge and are rewritten under the current format on the next hop. A current-format crash snapshot with a valid identity manifest uses identity matching even without terminal-environment capture.\n- `label`, `cwd`, `mcpServers`, `images`, `meta`, `promptMeta`, and `keepSession` are not identity-hashed: changing one does not invalidate an ordinary replay. They are in the input fingerprint: changing one rejects continuation of an interrupted turn, and that occurrence runs fresh. To force a completed call to run again, change a hashed field \u2014 normally the prompt.\n- Keep call order deterministic. Derive iteration from `args` and prior agent results, never from ambient state.\n\nEvery `resumeFromRunId` result has a bounded `replayEligibility` summary. Background admission, foreground completion, both await shapes, and inspect expose the same fields: strategy, predicted replayable-prefix length, observed replayed prefix and counts, and the first non-replay when known. Active correspondence reasons include `strategy-live`, `positional-miss`, `positional-suffix`, `not-recorded`, `path-missing`, `inputs-missing`, `inputs-changed`, `ambiguous-identity`, `ambiguous-content`, `candidate-consumed`, `empty-output`, `worktree-degraded`, `seed-persistence-error`, and `resume-fatal-latch`. Older reason literals stay exported only so historical journals parse. Engine and input-format versions and environment provenance ride along as diagnostics.\n\nAn all-live outcome means correspondence could not be established \u2014 not that the world changed. Missing resume metadata, incompatible format literals, or an invalid manifest or seed disable new-format replay. If any source row lacks a captured path or input fact (possible past the raw-frame cap, or with a non-strict-JSON `meta` value), the whole source is `"manifest-invalid"`: dropping the row could make an ambiguous sibling look unique.\n\n### Worked resume \u2014 raise a loop cap\n\nThe following workflow requires eight reviews but lets the caller cap how many are attempted in one run:\n\n```js\nexport const meta = {\n name: "resume-loop-cap",\n description: "Run expensive review rounds up to an args-controlled cap",\n phases: [{ title: "Review" }],\n};\n\nconst input = args && typeof args === "object" && !Array.isArray(args) ? args : {};\nconst numericCap = Number(input.maxRounds);\nconst maxRounds = Number.isInteger(numericCap) && numericCap > 0 ? numericCap : 8;\n\nphase("Review");\nconst rounds = [];\nfor (let i = 0; i < maxRounds; i += 1) {\n rounds.push(\n await agent(\n `Review round ${i + 1}: inspect the repository and report unresolved release blockers.`,\n { label: `review:${i + 1}`, phase: "Review" },\n ),\n );\n}\n\nif (maxRounds < 8) throw new Error(`review cap ${maxRounds} reached before 8 rounds`);\nreturn { rounds };\n```\n\nRun it with `args: { "maxRounds": 6 }`. Then send the same content (via `script`, or the absolute `scriptPath` you edit) with `args: { "maxRounds": 8 }` and the first result\'s `runId` as `resumeFromRunId`. Rounds 1\u20136 replay for zero current provider tokens; only rounds 7\u20138 run live, because the cap controls call count but is not interpolated into the round prompt. If every round prompt included `maxRounds`, all eight identities would change and all would run live. Resume always states its content; a bare `resumeFromRunId` never silently reuses the old script.\n\nGive repeated calls stable, descriptive labels and narrate decisions with `log()` \u2014 inspection by `labelGlob` then turns a pause or failure into a diagnosis instead of a guess.\n\n### Kill, patch, resume\n\nStop the live run with `{ action: "stop", runId }`. The returned `aborted` snapshot is the durable acknowledgement: resume is safe immediately, and a further await adds nothing. Edit the file. Start a new run with its absolute `scriptPath` and `resumeFromRunId`. Every completed call whose recorded identity and input fingerprint correspond replays, regardless of filesystem or environment drift. Read `replayEligibility` and the full `resumeReport` for the per-call decisions. A repeated stop of a terminal run is a successful no-op.\n\nRegistration, the per-action contracts, background collection, and the events resource are covered in **Running workflows** ([mcp-server-setup.md](mcp-server-setup.md)). Resume a durable checkpoint pause by re-sending the script with `resumeFromRunId` and `checkpointReplies` keyed by the source run\'s `checkpointContext.callIndex`.\n'
|
|
32563
|
+
"bytes": 9274,
|
|
32564
|
+
"sha256": "e62a5afd106e73ce1e7edd675573fb920fe09711173d7b23f1a04fd883f59ba2",
|
|
32565
|
+
"text": '## Determinism and resume\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\nRuns are journaled: every `agent()` and `checkpoint()` result is recorded under a deterministic call index. A new run may reuse eligible results from a terminal source run. Uncertainty always means live execution.\n\n> **Resume rule:** replay is content-addressed and fail-to-live on correspondence: a completed call replays when its identity and input fingerprint match uniquely. Filesystem or world state never gates replay.\n\n- Direct `Date.now()`, `Math.random()`, and no-arg `new Date()` / `Date()` calls fail static validation. The realm also blocks aliased or computed forms at runtime; `new Date(isoString)` is fine. Pass timestamps and random seeds through `args`.\n- The replay identity of an `agent()` call hashes: the prompt, the resolved `model`, `mode` when set, `configOptions` when non-empty (sorted keys), `tier`, `phase`, `agentType`, the resolved agent definition, and `schema`. The resolved agent definition includes its tool allowlist and denylist, model, isolation, and body prompt \u2014 editing a definition invalidates the calls that use it.\n- A separate input fingerprint hashes: the resolved label, per-call `cwd`, resolved isolation, `keepSession`, `images`, `mcpServers`, `meta`, `promptMeta`, and the approved script-backend digest.\n- Host `agentTimeoutMs`, `agentIdleTimeoutMs`, `agentRetries`, and `concurrency`, plus per-call `timeoutMs`, `idleTimeoutMs`, and `retries`, are operational bounds. They enter neither hash and may change freely on resume. A new run resolves them from its own request; it does not inherit the source values.\n- `args` is not hashed directly. New args that only raise a loop cap leave earlier identities unchanged, so those calls can replay. New args that change a prompt, model selection, phase, schema, call order, or runner-visible input make the affected calls run live. Unchanged independent calls may still replay.\n- Matching tries a unique exact `(kind, call path, identity hash)` row first (`"path-hash"`), then a unique `(kind, identity hash, input fingerprint)` row, so an unchanged call can replay as `"unique-hash"` after insertions or deletions. Source and current input fingerprints must be equal. Duplicate identities, duplicate content, consumed candidates, missing facts, and empty schema-less results run live. The engine never guesses by source order or occurrence.\n- Source admission requires: exact `cwd`, compatible call-path/input/checkpoint fingerprint formats, complete call/journal/allocation metadata, and a valid manifest and seed. Git HEAD and dirty digest, `environmentKey`, captured environment values, Node/V8, and producing engine version are diagnostics only. Environment differences may appear in `replayEligibility.provenanceChanges`; they never gate admission or matching.\n- A completed writer replays exactly like a reader. A live call, nested workflow, host checkpoint callback, or degraded worktree does not clear unrelated candidates. Nested child calls run live \u2014 they are outside the parent\'s journal \u2014 while matching root calls around them still replay. The engine does not reproduce file writes; a later live agent navigates the world it finds.\n- Replay costs zero current provider usage: a cached call returns its recorded result without spawning a session. Replayed session records keep their backend and session identity, rebound to the current call index, label, and phase.\n- A root call interrupted by `PROVIDER_USAGE_LIMIT` or `AUTH_REQUIRED` can continue its recorded session on either resume API. Continuation requires: the exact call index, identity hash, complete input fingerprint, non-worktree isolation, identical existing cwd, a coherent recorded session, and the runner\'s current backend/`poolKey`/reopen gates. A successful continuation finishes the unfinished turn and charges only its usage delta. Every failed gate runs fresh, and `fallbacks` records the reopen method or the exact skip reason. No script option controls this.\n- Completed checkpoint results replay when the identity and the `default`/`headless`/`timeoutMs` fingerprint match \u2014 headless results included. `checkpointReplies` keys always name the checkpoint index in the source run. A moved reply can follow intact prior correspondence; after a live divergence it must reach the exact recorded call site, so a different same-text branch cannot consume it.\n- `resumePolicy: "positional"` is a migration escape hatch for index/prefix matching. It cannot bypass format, metadata, manifest, cwd, or input checks. Marker-less, manual, and same-ID legacy journals keep historical hash-only positional behavior. Input formats below 2 use the `inputs-format-legacy` positional bridge and are rewritten under the current format on the next hop. A current-format crash snapshot with a valid identity manifest uses identity matching even without terminal-environment capture.\n- `label`, `cwd`, `mcpServers`, `images`, `meta`, `promptMeta`, and `keepSession` are not identity-hashed: changing one does not invalidate an ordinary replay. They are in the input fingerprint: changing one rejects continuation of an interrupted turn, and that occurrence runs fresh. To force a completed call to run again, change a hashed field \u2014 normally the prompt.\n- Keep call order deterministic. Derive iteration from `args` and prior agent results, never from ambient state.\n\nEvery `resumeFromRunId` result has a bounded `replayEligibility` summary. Background admission, foreground completion, both await shapes, and inspect expose the same fields: strategy, predicted replayable-prefix length, observed replayed prefix and counts, and the first non-replay when known. Active correspondence reasons include `strategy-live`, `positional-miss`, `positional-suffix`, `not-recorded`, `path-missing`, `inputs-missing`, `inputs-changed`, `ambiguous-identity`, `ambiguous-content`, `candidate-consumed`, `empty-output`, `worktree-degraded`, `seed-persistence-error`, and `resume-fatal-latch`. Older reason literals stay exported only so historical journals parse. Engine and input-format versions and environment provenance ride along as diagnostics.\n\nAn all-live outcome means correspondence could not be established \u2014 not that the world changed. Missing resume metadata, incompatible format literals, or an invalid manifest or seed disable new-format replay. If any source row lacks a captured path or input fact (possible past the raw-frame cap, or with a non-strict-JSON `meta` value), the whole source is `"manifest-invalid"`: dropping the row could make an ambiguous sibling look unique.\n\n### Worked resume \u2014 raise a loop cap\n\nThe following workflow requires eight reviews but lets the caller cap how many are attempted in one run:\n\n```js\nexport const meta = {\n name: "resume-loop-cap",\n description: "Run expensive review rounds up to an args-controlled cap",\n phases: [{ title: "Review" }],\n};\n\nconst input = args && typeof args === "object" && !Array.isArray(args) ? args : {};\nconst numericCap = Number(input.maxRounds);\nconst maxRounds = Number.isInteger(numericCap) && numericCap > 0 ? numericCap : 8;\n\nphase("Review");\nconst rounds = [];\nfor (let i = 0; i < maxRounds; i += 1) {\n rounds.push(\n await agent(\n `Review round ${i + 1}: inspect the repository and report unresolved release blockers.`,\n { label: `review:${i + 1}`, phase: "Review" },\n ),\n );\n}\n\nif (maxRounds < 8) throw new Error(`review cap ${maxRounds} reached before 8 rounds`);\nreturn { rounds };\n```\n\nRun it with `args: { "maxRounds": 6 }`. Then send the same content (via `script`, or the absolute `scriptPath` you edit) with `args: { "maxRounds": 8 }` and the first result\'s `runId` as `resumeFromRunId`. Rounds 1\u20136 replay for zero current provider tokens; only rounds 7\u20138 run live, because the cap controls call count but is not interpolated into the round prompt. If every round prompt included `maxRounds`, all eight identities would change and all would run live. Resume always states its content; a bare `resumeFromRunId` never silently reuses the old script.\n\nGive repeated calls stable, descriptive labels and narrate decisions with `log()` \u2014 inspection by `labelGlob` then turns a pause or failure into a diagnosis instead of a guess.\n\n### Kill, patch, resume\n\nStop the live run with `{ action: "stop", runId }`. The returned `aborted` snapshot is the durable acknowledgement: resume is safe immediately, and a further await adds nothing. Edit the file. Start a new run with its absolute `scriptPath` and `resumeFromRunId`. Every completed call whose recorded identity and input fingerprint correspond replays, regardless of filesystem or environment drift. Read `replayEligibility` and the full `resumeReport` for the per-call decisions. A repeated stop of a terminal run is a successful no-op.\n\nRegistration, the per-action contracts, background collection, and the events resource are covered in **Running workflows** ([mcp-server-setup.md](mcp-server-setup.md)). Resume a durable checkpoint pause by re-sending the script with `resumeFromRunId` and `checkpointReplies` keyed by the source run\'s `checkpointContext.callIndex`.\n'
|
|
32361
32566
|
},
|
|
32362
32567
|
{
|
|
32363
32568
|
"id": "workflow/api-agents",
|
|
@@ -32370,9 +32575,9 @@ var AUTHORING_DOC_TOPICS = [
|
|
|
32370
32575
|
"workflow/environment-and-tools",
|
|
32371
32576
|
"workflow/api-control-flow"
|
|
32372
32577
|
],
|
|
32373
|
-
"bytes":
|
|
32374
|
-
"sha256": "
|
|
32375
|
-
"text": '# Workflow agent API reference\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\n## `agent(prompt, options?)` \u2014 full option table\n\nReturns the agent\'s final assistant text, or the schema-validated object when `schema` is set. Resolves to `null` when a *recoverable* failure survives all retries.\n\n| option | type | meaning |\n|---|---|---|\n| `label` | `string` | Display/telemetry name; also stamped on every live ACP event for this call. Always set it. Not part of the resume hash. |\n| `phase` | `string` | Assign this call to a phase explicitly (needed inside concurrent stages where the global `phase()` state would race). |\n| `schema` | JSON Schema object | Structured output. Plain object literal only \u2014 no schema builders exist in the realm. Part of the resume hash. |\n| `model` | `string` | Model spec: optional registered harness prefix plus a verbatim id, or a backend-only name. See [Model specs & routing](#model-specs--routing). Part of the resume hash. |\n| `tier` | `"small" \\| "medium" \\| "big"` | Coarse tier resolved from host config; beats phase/meta model, loses to explicit `model`. Part of the resume hash. |\n| `mode` | `string` | ACP session mode id advertised by the selected backend/model. **Strict**: unsupported/unadvertised ids fail before prompting (and automatic workflow preflight rejects them before admission). Read the selected `action:"config"` entry\'s `modes.availableModes` and copy only an exact id; `modes:null` means omit this field. Never infer a generic `"default"`. Part of the resume hash when set. |\n| `configOptions` | `Record<string, string \\| boolean>` | Exact ACP session option ids and authored values. Applied in ascending id order after model and before the prompt, with no aliases or coercion. `"model"` is reserved for the dedicated `model` field. Part of the resume hash only when non-empty, with sorted keys. With MCP, read the advertised-options table from `workflow` action `config` before choosing values. |\n| `agentType` | `string` | Bind a named subagent definition (tools allow/deny, model, isolation, role prompt). See [agentType definitions](#agenttype-definitions). Part of the resume hash. |\n| `isolation` | `"worktree"` | Run in a throwaway git worktree branched from the run cwd. **Always removed (worktree + branch) when the call ends** \u2014 edits are discarded; return work as data. Degrades to the shared tree outside a git repo (logged). |\n| `resume` | `{ filesystem: "read-only" }` | Deprecated compatibility annotation. It is recorded as legacy diagnostic provenance, is not sent to the runner or hashed, and has no effect on replay. New scripts should omit it. |\n| `cwd` | `string` | Per-session working directory; relative resolves against the run\'s base cwd. Overridden by worktree isolation. Not hashed. |\n| `timeoutMs` | `number \\| null` | Total wall-clock cap for each attempt. A finite value may tighten a finite host `agentTimeoutMs` ceiling but cannot raise or disable it. With no host ceiling, a finite value applies and `null`/omitted is uncapped. |\n| `retries` | `number` | Retries after *recoverable* failures (default 0, host-overridable). Exhausted retries \u21D2 the call resolves `null`. |\n| `mcpServers` | `McpServerConfig[]` | MCP servers attached to this session. Stdio shape: `{ name, command, args: [], env: [{ name, value }] }` (`args`/`env` required, `env` is name/value pairs, not a map); `{ type: "http" \\| "sse", name, url, headers: [] }` also accepted. Not hashed. |\n| `images` | `PromptImage[]` | Base64 image blocks appended to the prompt; backends without image support get a bracketed text note. Not hashed. |\n| `meta` | `object` | ACP `_meta` merged into `session/new` \u2014 session-scoped extension passthrough (pairs with custom backends). Not hashed. |\n| `promptMeta` | `object` | ACP `_meta` merged into `session/prompt` \u2014 turn-scoped passthrough. Backend-computed keys win on conflict. Not hashed. |\n| `keepSession` | `boolean` | Skip release-time best-effort `session/close`; the non-secret re-attach record lands in `WorkflowRunResult.agentSessions` for host-side `loadSession()` / `resumeSession()`. Usage/auth pause failures are kept open automatically for managed continuation. Not identity-hashed; included in the input fingerprint. |\n\nThe
|
|
32578
|
+
"bytes": 8866,
|
|
32579
|
+
"sha256": "cce59fffcb8a5e58e8a127220bf303a1d362d66b1216ecde0712b38d6e8eae47",
|
|
32580
|
+
"text": '# Workflow agent API reference\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\n## `agent(prompt, options?)` \u2014 full option table\n\nReturns the agent\'s final assistant text, or the schema-validated object when `schema` is set. Resolves to `null` when a *recoverable* failure survives all retries.\n\n| option | type | meaning |\n|---|---|---|\n| `label` | `string` | Display/telemetry name; also stamped on every live ACP event for this call. Always set it. Not part of the resume hash. |\n| `phase` | `string` | Assign this call to a phase explicitly (needed inside concurrent stages where the global `phase()` state would race). |\n| `schema` | JSON Schema object | Structured output. Plain object literal only \u2014 no schema builders exist in the realm. Part of the resume hash. |\n| `model` | `string` | Model spec: optional registered harness prefix plus a verbatim id, or a backend-only name. See [Model specs & routing](#model-specs--routing). Part of the resume hash. |\n| `tier` | `"small" \\| "medium" \\| "big"` | Coarse tier resolved from host config; beats phase/meta model, loses to explicit `model`. Part of the resume hash. |\n| `mode` | `string` | ACP session mode id advertised by the selected backend/model. **Strict**: unsupported/unadvertised ids fail before prompting (and automatic workflow preflight rejects them before admission). Read the selected `action:"config"` entry\'s `modes.availableModes` and copy only an exact id; `modes:null` means omit this field. Never infer a generic `"default"`. Part of the resume hash when set. |\n| `configOptions` | `Record<string, string \\| boolean>` | Exact ACP session option ids and authored values. Applied in ascending id order after model and before the prompt, with no aliases or coercion. `"model"` is reserved for the dedicated `model` field. Part of the resume hash only when non-empty, with sorted keys. With MCP, read the advertised-options table from `workflow` action `config` before choosing values. |\n| `agentType` | `string` | Bind a named subagent definition (tools allow/deny, model, isolation, role prompt). See [agentType definitions](#agenttype-definitions). Part of the resume hash. |\n| `isolation` | `"worktree"` | Run in a throwaway git worktree branched from the run cwd. **Always removed (worktree + branch) when the call ends** \u2014 edits are discarded; return work as data. Degrades to the shared tree outside a git repo (logged). |\n| `resume` | `{ filesystem: "read-only" }` | Deprecated compatibility annotation. It is recorded as legacy diagnostic provenance, is not sent to the runner or hashed, and has no effect on replay. New scripts should omit it. |\n| `cwd` | `string` | Per-session working directory; relative resolves against the run\'s base cwd. Overridden by worktree isolation. Not hashed. |\n| `timeoutMs` | `number \\| null` | Total wall-clock cap for each attempt. A finite value may tighten a finite host `agentTimeoutMs` ceiling but cannot raise or disable it. With no host ceiling, a finite value applies and `null`/omitted is uncapped. |\n| `idleTimeoutMs` | `number \\| null` | No-backend-activity cap for each attempt. It may tighten a finite host `agentIdleTimeoutMs` ceiling but cannot raise or disable it. |\n| `retries` | `number` | Retries after *recoverable* failures (default 0, host-overridable). Exhausted retries \u21D2 the call resolves `null`. |\n| `mcpServers` | `McpServerConfig[]` | MCP servers attached to this session. Stdio shape: `{ name, command, args: [], env: [{ name, value }] }` (`args`/`env` required, `env` is name/value pairs, not a map); `{ type: "http" \\| "sse", name, url, headers: [] }` also accepted. Not hashed. |\n| `images` | `PromptImage[]` | Base64 image blocks appended to the prompt; backends without image support get a bracketed text note. Not hashed. |\n| `meta` | `object` | ACP `_meta` merged into `session/new` \u2014 session-scoped extension passthrough (pairs with custom backends). Not hashed. |\n| `promptMeta` | `object` | ACP `_meta` merged into `session/prompt` \u2014 turn-scoped passthrough. Backend-computed keys win on conflict. Not hashed. |\n| `keepSession` | `boolean` | Skip release-time best-effort `session/close`; the non-secret re-attach record lands in `WorkflowRunResult.agentSessions` for host-side `loadSession()` / `resumeSession()`. Usage/auth pause failures are kept open automatically for managed continuation. Not identity-hashed; included in the input fingerprint. |\n\nThe total-wall clock measures the whole attempt, including backend startup, model/config setup,\ntool work, and streamed output; it is not an idle timer. The separate idle clock is opt-in and\nre-arms on real backend activity (every ACP `session/update`), never synthetic progress heartbeats.\nSize it above the longest expected backend-silent local tool call. Each retry starts fresh clocks.\nExhaustion is recoverable `AGENT_TIMEOUT` or `AGENT_IDLE_TIMEOUT`: the call resolves to `null`,\nreleases its concurrency slot, and asks the ACP session to cancel. A session that keeps running\nafter the cancellation grace is closed where supported and its pooled child is recycled.\n\nEvery new run, including one admitted with `resumeFromRunId`, resolves host limits from that run\'s\nrequest. It does not inherit `agentTimeoutMs`, `agentIdleTimeoutMs`, retries, concurrency, or\nagent-count values from its source, so pass every operational bound the resumed execution should use.\n\n## Model specs & routing\n\nA `model` string is resolved solely from its first segment, then delegated to the harness:\n\n| spec shape | routes to | notes |\n|---|---|---|\n| *(omitted)* | host-pinned/default backend | MCP: explicit `AGENTPRISM_DEFAULT_BACKEND` wins; when truly unset, zero-token readiness discovery pins one project default before validation/execution and preserves it across resume. SDK runner: configured default, historical fallback `claude`. The selected harness keeps its session default model. Most portable. |\n| `claude`, `codex`, `opencode`, `pi`, or `<custom-name>` | that registered harness | Backend-only: no model config call; the harness default remains active. |\n| `claude/<id>`, `codex/<id>`, `opencode/<id>`, `pi/<id>`, or `<custom-name>/<id>` | that registered harness | Match the first segment ASCII-case-insensitively and strip exactly one segment. Custom names take priority on collision. The remaining `<id>` is sent verbatim, including further `/` characters. For Pi, that remainder is its `<provider>/<model-id>` and Pi preserves any further slashes in the model id. |\n| any other string, including `anthropic/\u2026`, `openai/\u2026`, bare `opus`, or bare `gpt-\u2026` | host default backend | The **entire** authored string is sent verbatim; these are not routing aliases. |\n\nSelection is a single `session/set_config_option` with `configId: "model"` and the exact remaining string. There is no catalog matching, case folding, normalization, bracket parsing, nearest-neighbor selection, sibling effort/Fast option driving, retry, or echo verification. Brackets, dots, and provider-style prefixes are ordinary model-id characters.\n\nWhatever the harness returns is the outcome. A rejection follows the existing agent-error path with no resolution-specific code or model fallback event. `onModelFallback` and `WorkflowRunResult.fallbacks` remain public compatibility surfaces; model resolution does not emit entries, while pause recovery emits `kind: "continuation"` reattach/skip notices.\n\n## Structured output channels\n\nOne author API (`schema`), four fulfillment paths \u2014 chosen automatically per backend:\n\n| backend | channel |\n|---|---|\n| Claude | native `outputFormat`, schema normalized to Anthropic\'s structured-outputs subset (e.g. `oneOf` \u2192 `anyOf`; unsupported keywords/formats stripped on the wire) |\n| Codex | native strict `outputSchema` (OpenAI strict subset normalization) |\n| Pi | a client-hosted `StructuredOutput` MCP tool injected when the agent advertises HTTP MCP support; common prompt-embedded schema and validated final-text JSON fallback |\n| OpenCode / custom ACP | a client-hosted **`StructuredOutput` MCP tool** injected into the session when the agent advertises HTTP MCP support (an agent may show it as `structured_output_StructuredOutput`); otherwise prompt-embedded schema + JSON parse of the final message. Custom backends can opt out of tool injection with `structuredOutputTool: false`. |\n\nPi accepts stdio, Streamable HTTP, and SSE MCP servers; ACP-transport MCP hosting remains client-side.\n\nIn every channel the runner coerces + validates client-side and re-prompts a bounded number of times; the final miss fails the call with non-recoverable `SCHEMA_NONCOMPLIANCE`. Constraints stripped from the wire are still enforced client-side \u2014 an exotic schema keyword shows up as re-prompt churn, so keep schemas simple.\n'
|
|
32376
32581
|
},
|
|
32377
32582
|
{
|
|
32378
32583
|
"id": "workflow/api-control-flow",
|
|
@@ -32385,9 +32590,9 @@ var AUTHORING_DOC_TOPICS = [
|
|
|
32385
32590
|
"workflow/checkpoints-and-quality",
|
|
32386
32591
|
"workflow/api-agents"
|
|
32387
32592
|
],
|
|
32388
|
-
"bytes":
|
|
32389
|
-
"sha256": "
|
|
32390
|
-
"text": '# Workflow control-flow API reference\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\n## DSL globals \u2014 complete signatures\n\n```\nagent(prompt, options?) \u2192 Promise<string | object | null>\nparallel(thunks) \u2192 Promise<results[]> // barrier; input order; failed slot = null\npipeline(items, ...stages) \u2192 Promise<results[]> // no inter-stage barrier; stage(prev, original, index); failed item = null\nworkflow(nameOrScript, args?) \u2192 Promise<unknown> // one nesting level; names resolve from the host\'s workflows folder, inline scripts always work\ngate(thunk, validator, { attempts = 3 }) \u2192 { ok, value, verdict, attempts }\n // thunk(feedback, attempt); validator(result) \u2192 { ok, feedback?, ... } | boolean | null (may be async / an agent call)\nretry(thunk, { attempts = 3, until? }) \u2192 last result // thunk(attempt); stops early when until(result)\nverify(item, { reviewers = 2, threshold = 0.5, lens? })\n \u2192 { real, realCount, total, votes: [{ real?, reason? }] }\n // N adversarial reviewers prompted to REFUTE; lens (string | string[]) rotates focus per reviewer\njudgePanel(attempts, { judges = 3, rubric = "overall quality and correctness" })\n \u2192 { index, attempt, score, judgments } // mean 0\u20131 score per candidate; stable tie-break by index\nloopUntilDry({ round, key = JSON.stringify, consecutiveEmpty = 2, maxRounds = 50 })\n \u2192 unique items[] // round(i) returns items; stops after N dry rounds; agent-limit exhaustion returns the partial result\ncompletenessCheck(taskArgs, results) \u2192 { complete, missing?: string[] }\ncheckpoint(promptText, options?) \u2192 Promise<reply> // journaled human gate; zero tokens\nphase(title) \u2192 void // open a named phase\nlog(message) \u2192 void // console.log/info/warn/error route here too\nargs // the host-provided input value, verbatim\ncwd // the run\'s base working directory (string); process.cwd() returns it too\n```\n\nFor `gate()`, `value` is the final producer result and `verdict` is the exact last completed\nvalidator return, including any extra structured fields. `{ ok: true }` and bare `true` pass;\n`{ ok: false, feedback? }`, bare `false`, and `null` reject. Only object feedback is threaded into\nthe next producer attempt. A producer result of `null` is still passed to the validator. Producer\nor validator exceptions propagate immediately, so no partial gate result is returned and no later\nattempt runs. An explicit unsupported `undefined` validator return is a rejection represented as\n`verdict: null`. If the script returns the gate result, its complete verdict is persisted and may\nreach the host; keep evidence concise and never put credentials or other secrets in verdict data.\n\n`verify`, `judgePanel`, and `completenessCheck` spawn their subagents on the run\'s default model \u2014 hand-roll with `parallel` + `agent` to pin panel members to specific backends.\n\n## `checkpoint()` options\n\n| option | type | meaning |\n|---|---|---|\n| `kind` | `"confirm" \\| "input" \\| "select"` | Reply shape: boolean-ish / free text / one of `choices`. Affects the journal hash and the host UI widget. |\n| `choices` | `string[]` | For `kind: "select"`. |\n| `default` | `unknown` | Reply taken in the default headless mode \u2014 journaled like a real reply. Defaults to `true`. |\n| `headless` | `"default" \\| "abort" \\| "pause"` | No live channel: `"default"` takes `default ?? true`, `"abort"` aborts, and `"pause"` creates a persisted `checkpoint_required` pause. Default `"default"`. |\n| `timeoutMs` | `number` | Deadline for the interactive prompt. |\n\nThe host supplies the live human channel (elicitation in the MCP server; `ExecOptions.confirm` in the SDK), and that channel wins even when `headless: "pause"` is declared. A durable pause carries non-secret `checkpointContext`; resume with `ExecOptions.checkpointReplies: { [context.callIndex]: decision }` or attach a live channel. On a new `resumeFromRunId` execution, reply keys always name indexes in the **source** recording; identity matching may inject that decision at a shifted current index. Completed host and headless checkpoint results both replay when identity and the checkpoint-options fingerprint over `default`, `headless`, and `timeoutMs` match. A changed option or ambiguous match runs fresh. Detached runs never pause for a checkpoint unless the author opts into `"pause"`.\n\n## Error codes (`WorkflowError.code`)\n\n| code | recoverable | engine behavior |\n|---|---|---|\n| `AGENT_TIMEOUT` | yes | Total wall-clock attempt cap exhausted. Every retry gets a fresh clock; after the final attempt the call resolves `null`, and ACP cancel escalates to close/recycle when the turn does not stop. |\n| `AGENT_CANCELLED` | yes | The host selected this in-flight call for cancellation. It resolves `null` immediately through an engine race, skips retries, leaves the run live, and is recorded as a failed call rather than a replayable journal result. |\n| `AGENT_EMPTY_OUTPUT` | yes | No assistant text on a schema-less call; same retry-then-`null`. |\n| `AGENT_EXECUTION_ERROR` | yes* | Generic agent failure (*refusal/truncation variants are non-recoverable). |\n| `SCHEMA_NONCOMPLIANCE` | no | Structured output never validated after the re-prompt ladder. Halts the run (catchable in-script). |\n| `PROVIDER_USAGE_LIMIT` | no | Quota/rate wall \u2014 the run **pauses** (journaled, resumable), with the provider\'s reset hint. |\n| `AGENT_LIMIT_EXCEEDED` | no | `maxAgents` cap hit. |\n| `AUTH_REQUIRED` | no | Backend needs authentication. `WorkflowManager` returns a resumable pause with `reason: "auth_required"` and redacted `authContext`; a direct runner throws. The host completes auth before resuming/retrying. |\n| `CHECKPOINT_REQUIRED` | no | `headless: "pause"` reached without a live channel. `WorkflowManager` returns `reason: "checkpoint_required"` plus non-secret `checkpointContext`; resume with `checkpointReplies` or live confirm. |\n| `SCRIPT_VALIDATION_ERROR` | no | Script failed parse/validation (bad meta, nondeterministic API, bad `meta.backends` shape). |\n| `SCRIPT_ERROR` | no | The script itself crashed (uncaught throw, floated rejection). |\n| `WORKFLOW_ABORTED` | \u2014 | Real cancellation (pause/stop/host signal) \u2014 never used for crashes. |\n\n`loopUntilDry` absorbs `AGENT_LIMIT_EXCEEDED` from its rounds and returns the partial result; everywhere else it propagates.\n'
|
|
32593
|
+
"bytes": 6852,
|
|
32594
|
+
"sha256": "7c7ad1129ead65b362723d231edd20011a86588d3b7a12f9618ba1618a91d271",
|
|
32595
|
+
"text": '# Workflow control-flow API reference\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\n## DSL globals \u2014 complete signatures\n\n```\nagent(prompt, options?) \u2192 Promise<string | object | null>\nparallel(thunks) \u2192 Promise<results[]> // barrier; input order; failed slot = null\npipeline(items, ...stages) \u2192 Promise<results[]> // no inter-stage barrier; stage(prev, original, index); failed item = null\nworkflow(nameOrScript, args?) \u2192 Promise<unknown> // one nesting level; names resolve from the host\'s workflows folder, inline scripts always work\ngate(thunk, validator, { attempts = 3 }) \u2192 { ok, value, verdict, attempts }\n // thunk(feedback, attempt); validator(result) \u2192 { ok, feedback?, ... } | boolean | null (may be async / an agent call)\nretry(thunk, { attempts = 3, until? }) \u2192 last result // thunk(attempt); stops early when until(result)\nverify(item, { reviewers = 2, threshold = 0.5, lens? })\n \u2192 { real, realCount, total, votes: [{ real?, reason? }] }\n // N adversarial reviewers prompted to REFUTE; lens (string | string[]) rotates focus per reviewer\njudgePanel(attempts, { judges = 3, rubric = "overall quality and correctness" })\n \u2192 { index, attempt, score, judgments } // mean 0\u20131 score per candidate; stable tie-break by index\nloopUntilDry({ round, key = JSON.stringify, consecutiveEmpty = 2, maxRounds = 50 })\n \u2192 unique items[] // round(i) returns items; stops after N dry rounds; agent-limit exhaustion returns the partial result\ncompletenessCheck(taskArgs, results) \u2192 { complete, missing?: string[] }\ncheckpoint(promptText, options?) \u2192 Promise<reply> // journaled human gate; zero tokens\nphase(title) \u2192 void // open a named phase\nlog(message) \u2192 void // console.log/info/warn/error route here too\nargs // the host-provided input value, verbatim\ncwd // the run\'s base working directory (string); process.cwd() returns it too\n```\n\nFor `gate()`, `value` is the final producer result and `verdict` is the exact last completed\nvalidator return, including any extra structured fields. `{ ok: true }` and bare `true` pass;\n`{ ok: false, feedback? }`, bare `false`, and `null` reject. Only object feedback is threaded into\nthe next producer attempt. A producer result of `null` is still passed to the validator. Producer\nor validator exceptions propagate immediately, so no partial gate result is returned and no later\nattempt runs. An explicit unsupported `undefined` validator return is a rejection represented as\n`verdict: null`. If the script returns the gate result, its complete verdict is persisted and may\nreach the host; keep evidence concise and never put credentials or other secrets in verdict data.\n\n`verify`, `judgePanel`, and `completenessCheck` spawn their subagents on the run\'s default model \u2014 hand-roll with `parallel` + `agent` to pin panel members to specific backends.\n\n## `checkpoint()` options\n\n| option | type | meaning |\n|---|---|---|\n| `kind` | `"confirm" \\| "input" \\| "select"` | Reply shape: boolean-ish / free text / one of `choices`. Affects the journal hash and the host UI widget. |\n| `choices` | `string[]` | For `kind: "select"`. |\n| `default` | `unknown` | Reply taken in the default headless mode \u2014 journaled like a real reply. Defaults to `true`. |\n| `headless` | `"default" \\| "abort" \\| "pause"` | No live channel: `"default"` takes `default ?? true`, `"abort"` aborts, and `"pause"` creates a persisted `checkpoint_required` pause. Default `"default"`. |\n| `timeoutMs` | `number` | Deadline for the interactive prompt. |\n\nThe host supplies the live human channel (elicitation in the MCP server; `ExecOptions.confirm` in the SDK), and that channel wins even when `headless: "pause"` is declared. A durable pause carries non-secret `checkpointContext`; resume with `ExecOptions.checkpointReplies: { [context.callIndex]: decision }` or attach a live channel. On a new `resumeFromRunId` execution, reply keys always name indexes in the **source** recording; identity matching may inject that decision at a shifted current index. Completed host and headless checkpoint results both replay when identity and the checkpoint-options fingerprint over `default`, `headless`, and `timeoutMs` match. A changed option or ambiguous match runs fresh. Detached runs never pause for a checkpoint unless the author opts into `"pause"`.\n\n## Error codes (`WorkflowError.code`)\n\n| code | recoverable | engine behavior |\n|---|---|---|\n| `AGENT_TIMEOUT` | yes | Total wall-clock attempt cap exhausted. Every retry gets a fresh clock; after the final attempt the call resolves `null`, and ACP cancel escalates to close/recycle when the turn does not stop. |\n| `AGENT_IDLE_TIMEOUT` | yes | Opt-in no-backend-activity cap exhausted. Real backend events re-arm it; retries and cancellation match `AGENT_TIMEOUT`. |\n| `AGENT_CANCELLED` | yes | The host selected this in-flight call for cancellation. It resolves `null` immediately through an engine race, skips retries, leaves the run live, and is recorded as a failed call rather than a replayable journal result. |\n| `AGENT_EMPTY_OUTPUT` | yes | No assistant text on a schema-less call; same retry-then-`null`. |\n| `AGENT_EXECUTION_ERROR` | yes* | Generic agent failure (*refusal/truncation variants are non-recoverable). |\n| `SCHEMA_NONCOMPLIANCE` | no | Structured output never validated after the re-prompt ladder. Halts the run (catchable in-script). |\n| `PROVIDER_USAGE_LIMIT` | no | Quota/rate wall \u2014 the run **pauses** (journaled, resumable), with the provider\'s reset hint. |\n| `AGENT_LIMIT_EXCEEDED` | no | `maxAgents` cap hit. |\n| `AUTH_REQUIRED` | no | Backend needs authentication. `WorkflowManager` returns a resumable pause with `reason: "auth_required"` and redacted `authContext`; a direct runner throws. The host completes auth before resuming/retrying. |\n| `CHECKPOINT_REQUIRED` | no | `headless: "pause"` reached without a live channel. `WorkflowManager` returns `reason: "checkpoint_required"` plus non-secret `checkpointContext`; resume with `checkpointReplies` or live confirm. |\n| `SCRIPT_VALIDATION_ERROR` | no | Script failed parse/validation (bad meta, nondeterministic API, bad `meta.backends` shape). |\n| `SCRIPT_ERROR` | no | The script itself crashed (uncaught throw, floated rejection). |\n| `WORKFLOW_ABORTED` | \u2014 | Real cancellation (pause/stop/host signal) \u2014 never used for crashes. |\n\n`loopUntilDry` absorbs `AGENT_LIMIT_EXCEEDED` from its rounds and returns the partial result; everywhere else it propagates.\n'
|
|
32391
32596
|
},
|
|
32392
32597
|
{
|
|
32393
32598
|
"id": "workflow/api-resume-and-backends",
|
|
@@ -33553,11 +33758,13 @@ var WorkflowScriptResources = class {
|
|
|
33553
33758
|
this.modernNotifier = modernNotifier;
|
|
33554
33759
|
this.router = source instanceof WorkflowManager2 ? singleStoreRouter(source) : source.router;
|
|
33555
33760
|
this.detachRunDeleted = this.router.onRunDeleted(this.onRunDeleted);
|
|
33761
|
+
this.detachRunStopped = this.router.onRunStopped(({ runId }) => this.cancelPendingElicitation(runId));
|
|
33556
33762
|
const previousOnClose = this.mcp.server.onclose;
|
|
33557
33763
|
this.mcp.server.onclose = () => {
|
|
33558
33764
|
for (const controller of this.elicitationControllers.values()) controller.abort();
|
|
33559
33765
|
this.elicitationControllers.clear();
|
|
33560
33766
|
for (const uri of [...this.eventSubscriptions.keys()]) this.closeEventSubscription(uri);
|
|
33767
|
+
this.detachRunStopped();
|
|
33561
33768
|
this.detachRunDeleted();
|
|
33562
33769
|
previousOnClose?.();
|
|
33563
33770
|
};
|
|
@@ -33567,6 +33774,7 @@ var WorkflowScriptResources = class {
|
|
|
33567
33774
|
modernNotifier;
|
|
33568
33775
|
router;
|
|
33569
33776
|
detachRunDeleted;
|
|
33777
|
+
detachRunStopped;
|
|
33570
33778
|
subscriptions = /* @__PURE__ */ new Set();
|
|
33571
33779
|
externalReaders = /* @__PURE__ */ new Map();
|
|
33572
33780
|
eventSubscriptions = /* @__PURE__ */ new Map();
|
|
@@ -33955,6 +34163,43 @@ var WorkflowScriptResources = class {
|
|
|
33955
34163
|
}
|
|
33956
34164
|
};
|
|
33957
34165
|
|
|
34166
|
+
// ../mcp-server/src/workflow-stop.ts
|
|
34167
|
+
function requireDurableStoppedRun(manager, runId) {
|
|
34168
|
+
const persistence = manager.getPersistence();
|
|
34169
|
+
const persisted = persistence.load(runId);
|
|
34170
|
+
if (persisted?.status !== "aborted") {
|
|
34171
|
+
throw new ProtocolError(
|
|
34172
|
+
ProtocolErrorCode.InternalError,
|
|
34173
|
+
`Workflow stop for runId "${runId}" could not be durably acknowledged: the persisted status is ${persisted?.status ?? "missing"}, not aborted.`
|
|
34174
|
+
);
|
|
34175
|
+
}
|
|
34176
|
+
if (persisted.eventLogIncomplete || persisted.eventStreamId === void 0 || persisted.eventSeq === void 0 || persisted.eventSeq < 1) {
|
|
34177
|
+
throw new ProtocolError(
|
|
34178
|
+
ProtocolErrorCode.InternalError,
|
|
34179
|
+
`Workflow stop for runId "${runId}" could not be durably acknowledged: its stopped event is not durably readable.`
|
|
34180
|
+
);
|
|
34181
|
+
}
|
|
34182
|
+
let stoppedEventIsDurable = false;
|
|
34183
|
+
try {
|
|
34184
|
+
const events = persistence.readEvents(runId, {
|
|
34185
|
+
after: persisted.eventSeq - 1,
|
|
34186
|
+
streamId: persisted.eventStreamId,
|
|
34187
|
+
limit: 1
|
|
34188
|
+
});
|
|
34189
|
+
stoppedEventIsDurable = events.events.some(
|
|
34190
|
+
(record2) => record2.seq === persisted.eventSeq && record2.event.type === "stopped"
|
|
34191
|
+
);
|
|
34192
|
+
} catch {
|
|
34193
|
+
stoppedEventIsDurable = false;
|
|
34194
|
+
}
|
|
34195
|
+
if (!stoppedEventIsDurable) {
|
|
34196
|
+
throw new ProtocolError(
|
|
34197
|
+
ProtocolErrorCode.InternalError,
|
|
34198
|
+
`Workflow stop for runId "${runId}" could not be durably acknowledged: its terminal stopped event is missing.`
|
|
34199
|
+
);
|
|
34200
|
+
}
|
|
34201
|
+
}
|
|
34202
|
+
|
|
33958
34203
|
// ../mcp-server/src/server.ts
|
|
33959
34204
|
var SERVER_NAME = "agentprism-workflow";
|
|
33960
34205
|
var DEFAULT_REQUEST_STATE_CODEC = createRequestStateCodec({
|
|
@@ -33962,7 +34207,7 @@ var DEFAULT_REQUEST_STATE_CODEC = createRequestStateCodec({
|
|
|
33962
34207
|
bind: (ctx) => ctx.mcpReq.method
|
|
33963
34208
|
});
|
|
33964
34209
|
var require2 = createRequire(import.meta.url);
|
|
33965
|
-
var SERVER_VERSION = true ? "0.
|
|
34210
|
+
var SERVER_VERSION = true ? "0.37.0" : require2("../package.json").version;
|
|
33966
34211
|
var SERVER_INSTRUCTIONS = [
|
|
33967
34212
|
"This server exposes three model-facing tools for authoring and orchestrating multi-agent work. workflow and repl spawn subagents over the same ACP backends \u2014 the registry built-ins Claude, Codex, OpenCode, and pi, plus any registered custom agents \u2014 and key their durable state by an absolute projectDir (required on the shared daemon; defaults to the server's own project in single-project mode). Backend credentials come from each agent's own login (claude, codex, opencode, pi), so there is nothing auth-shaped to configure here.",
|
|
33968
34213
|
'\u2022 docs \u2014 SELECTIVE VERSION-MATCHED REFERENCE. Omit topic or use topic:"index" for the bounded catalog, then read exactly one workflow/* or repl/* topic. It embeds the selected text/markdown resource, runs no code, opens no backend, and needs no projectDir. Use it when the compact tool descriptions do not contain enough syntax or lifecycle detail.',
|
|
@@ -34549,6 +34794,17 @@ function formatStopSummary(result) {
|
|
|
34549
34794
|
}
|
|
34550
34795
|
return truncateUtf83(lines.join("\n"), 8192, "\u2026[text truncated]");
|
|
34551
34796
|
}
|
|
34797
|
+
function formatPendingStopSummary(result) {
|
|
34798
|
+
const lines = inspectionSummaryLines(result);
|
|
34799
|
+
const owner = result.control.owner;
|
|
34800
|
+
lines.splice(
|
|
34801
|
+
2,
|
|
34802
|
+
0,
|
|
34803
|
+
`Stop request ${result.control.operationId} is durably pending; retry stop, inspect, or await to observe settlement.`,
|
|
34804
|
+
owner === void 0 ? "No live execution owner is currently discoverable; a later lease holder will apply the intent." : `Execution owner: daemon pid ${owner.pid}${owner.version ? ` v${owner.version}` : ""}${owner.lameDuck ? " (draining)" : ""}.`
|
|
34805
|
+
);
|
|
34806
|
+
return truncateUtf83(lines.join("\n"), 8192, "\u2026[text truncated]");
|
|
34807
|
+
}
|
|
34552
34808
|
function formatAgentCancellationSummary(status, cancellation) {
|
|
34553
34809
|
const lines = inspectionSummaryLines(status);
|
|
34554
34810
|
lines.splice(
|
|
@@ -34560,7 +34816,7 @@ function formatAgentCancellationSummary(status, cancellation) {
|
|
|
34560
34816
|
}
|
|
34561
34817
|
function readScriptAtAdmission(scriptPath) {
|
|
34562
34818
|
try {
|
|
34563
|
-
return
|
|
34819
|
+
return readFileSync3(scriptPath, "utf8");
|
|
34564
34820
|
} catch (error51) {
|
|
34565
34821
|
const cause = error51 instanceof Error ? error51.message : String(error51);
|
|
34566
34822
|
throw new ProtocolError(
|
|
@@ -34605,41 +34861,6 @@ async function settleForegroundRun(manager, started) {
|
|
|
34605
34861
|
throw error51;
|
|
34606
34862
|
}
|
|
34607
34863
|
}
|
|
34608
|
-
function requireDurableStoppedRun(manager, runId) {
|
|
34609
|
-
const persistence = manager.getPersistence();
|
|
34610
|
-
const persisted = persistence.load(runId);
|
|
34611
|
-
if (persisted?.status !== "aborted") {
|
|
34612
|
-
throw new ProtocolError(
|
|
34613
|
-
ProtocolErrorCode.InternalError,
|
|
34614
|
-
`Workflow stop for runId "${runId}" could not be durably acknowledged: the persisted status is ${persisted?.status ?? "missing"}, not aborted.`
|
|
34615
|
-
);
|
|
34616
|
-
}
|
|
34617
|
-
if (persisted.eventLogIncomplete || persisted.eventStreamId === void 0 || persisted.eventSeq === void 0 || persisted.eventSeq < 1) {
|
|
34618
|
-
throw new ProtocolError(
|
|
34619
|
-
ProtocolErrorCode.InternalError,
|
|
34620
|
-
`Workflow stop for runId "${runId}" could not be durably acknowledged: its stopped event is not durably readable.`
|
|
34621
|
-
);
|
|
34622
|
-
}
|
|
34623
|
-
let stoppedEventIsDurable = false;
|
|
34624
|
-
try {
|
|
34625
|
-
const events = persistence.readEvents(runId, {
|
|
34626
|
-
after: persisted.eventSeq - 1,
|
|
34627
|
-
streamId: persisted.eventStreamId,
|
|
34628
|
-
limit: 1
|
|
34629
|
-
});
|
|
34630
|
-
stoppedEventIsDurable = events.events.some(
|
|
34631
|
-
(record2) => record2.seq === persisted.eventSeq && record2.event.type === "stopped"
|
|
34632
|
-
);
|
|
34633
|
-
} catch {
|
|
34634
|
-
stoppedEventIsDurable = false;
|
|
34635
|
-
}
|
|
34636
|
-
if (!stoppedEventIsDurable) {
|
|
34637
|
-
throw new ProtocolError(
|
|
34638
|
-
ProtocolErrorCode.InternalError,
|
|
34639
|
-
`Workflow stop for runId "${runId}" could not be durably acknowledged: its terminal stopped event is missing.`
|
|
34640
|
-
);
|
|
34641
|
-
}
|
|
34642
|
-
}
|
|
34643
34864
|
function normalizeTokenUsage(usage) {
|
|
34644
34865
|
if (!usage) return void 0;
|
|
34645
34866
|
return {
|
|
@@ -34663,7 +34884,7 @@ function persistedOutcome(persisted, status) {
|
|
|
34663
34884
|
return {
|
|
34664
34885
|
runId: persisted.runId,
|
|
34665
34886
|
status: status.status,
|
|
34666
|
-
...
|
|
34887
|
+
...status.limits === void 0 ? {} : { limits: status.limits },
|
|
34667
34888
|
...status.status === "completed" && persisted.result !== void 0 ? { result: persisted.result } : {},
|
|
34668
34889
|
tokenUsage: normalizeTokenUsage(persisted.tokenUsage),
|
|
34669
34890
|
logs: persisted.logs,
|
|
@@ -34920,7 +35141,7 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
34920
35141
|
const workflowToolOutputSchema = workflowToolOutputShape;
|
|
34921
35142
|
const workflowToolConfig = {
|
|
34922
35143
|
title: "Discover, validate, run, inspect, await, stop, or narrow-cancel an agent workflow",
|
|
34923
|
-
description: 'Author and operate JavaScript agent workflows through one project-scoped tool. A script\'s first statement must be `export const meta = { name, description, phases? }`. When present, phases must be an array of objects shaped `{ title: string, detail?: string, model?: string }`, never an array of strings. Inside the deterministic script realm use agent(prompt, options?) for one subagent; parallel([thunks]) for a barrier; pipeline(items, ...stages) for streaming stages; checkpoint(prompt, options?) for a human gate; phase(title) and log(message) for progress; and return the final JSON-serializable value. Top-level await is supported. Imports, require, network APIs, Date.now(), and Math.random() are unavailable. Always label agent calls; schema is a plain JSON Schema object for structured results. The only agent option keys are label, phase, model, tier, mode, configOptions, schema, cwd, timeoutMs, retries, isolation:"worktree", resume, agentType, mcpServers, images, meta, promptMeta, and keepSession; unknown keys reject before admission. Every parallel entry must be a thunk: parallel([() => agent(...), () => agent(...)]). For deeper syntax, read docs topic workflow/quickstart and then one related workflow/* topic. Minimal script: `export const meta = { name: "review", description: "Review a target", phases: [{ title: "Review" }] }; phase("Review"); const report = await agent("Review " + args.target, { label: "review" }); return { report };`. Omit model for the server default (explicit AGENTPRISM_DEFAULT_BACKEND, else a zero-token auto-selected project pin), or use a backend name alone to preserve that backend\'s configured default. Before choosing a pinned model, mode, or configOptions, call action:"config" with projectDir and optional harnesses/modelFilter; after choosing a model, pass modelSpecs to read its model-specific options. Set mode only when that selected harness entry\'s modes.availableModes explicitly lists the exact id; modes:null means unsupported, so omit mode\u2014never infer a default from an absent value. Config opens no-prompt sessions, spends zero tokens, and starts no workflow. action:"run" automatically performs static validation, a mocked dry run, and routed config checks before admission. Invalid scripts return bounded diagnostics with status:"rejected" and create no run ID, reserve no background slot, and spend no tokens. Run, resume, inspect, await, or stop an admitted workflow through the same tool. The script orchestrates agent() subagents (and optional checkpoint() gates) over registry built-ins\u2014currently Claude, Codex, OpenCode, and pi\u2014ACP backends, plus registered custom agents. Supply exactly one of inline script or absolute scriptPath; path content is read once and snapshotted at admission. ' + (requireProjectDir ? "config and run REQUIRE projectDir (absolute): it is the discovery cwd and selects the project-scoped run store/default execution cwd. " : "run optionally takes projectDir (absolute) to select the project-scoped run store; default is this server's own project. ") +
|
|
35144
|
+
description: 'Author and operate JavaScript agent workflows through one project-scoped tool. A script\'s first statement must be `export const meta = { name, description, phases? }`. When present, phases must be an array of objects shaped `{ title: string, detail?: string, model?: string }`, never an array of strings. Inside the deterministic script realm use agent(prompt, options?) for one subagent; parallel([thunks]) for a barrier; pipeline(items, ...stages) for streaming stages; checkpoint(prompt, options?) for a human gate; phase(title) and log(message) for progress; and return the final JSON-serializable value. Top-level await is supported. Imports, require, network APIs, Date.now(), and Math.random() are unavailable. Always label agent calls; schema is a plain JSON Schema object for structured results. The only agent option keys are label, phase, model, tier, mode, configOptions, schema, cwd, timeoutMs, idleTimeoutMs, retries, isolation:"worktree", resume, agentType, mcpServers, images, meta, promptMeta, and keepSession; unknown keys reject before admission. Every parallel entry must be a thunk: parallel([() => agent(...), () => agent(...)]). For deeper syntax, read docs topic workflow/quickstart and then one related workflow/* topic. Minimal script: `export const meta = { name: "review", description: "Review a target", phases: [{ title: "Review" }] }; phase("Review"); const report = await agent("Review " + args.target, { label: "review" }); return { report };`. Omit model for the server default (explicit AGENTPRISM_DEFAULT_BACKEND, else a zero-token auto-selected project pin), or use a backend name alone to preserve that backend\'s configured default. Before choosing a pinned model, mode, or configOptions, call action:"config" with projectDir and optional harnesses/modelFilter; after choosing a model, pass modelSpecs to read its model-specific options. Set mode only when that selected harness entry\'s modes.availableModes explicitly lists the exact id; modes:null means unsupported, so omit mode\u2014never infer a default from an absent value. Config opens no-prompt sessions, spends zero tokens, and starts no workflow. action:"run" automatically performs static validation, a mocked dry run, and routed config checks before admission. Invalid scripts return bounded diagnostics with status:"rejected" and create no run ID, reserve no background slot, and spend no tokens. Run, resume, inspect, await, or stop an admitted workflow through the same tool. The script orchestrates agent() subagents (and optional checkpoint() gates) over registry built-ins\u2014currently Claude, Codex, OpenCode, and pi\u2014ACP backends, plus registered custom agents. Supply exactly one of inline script or absolute scriptPath; path content is read once and snapshotted at admission. ' + (requireProjectDir ? "config and run REQUIRE projectDir (absolute): it is the discovery cwd and selects the project-scoped run store/default execution cwd. " : "run optionally takes projectDir (absolute) to select the project-scoped run store; default is this server's own project. ") + `inspect/await/stop locate the project store from runId and never accept projectDir. Foreground is the default and streams progress; background:true returns a durable runId for bounded action:"await" calls. run and await honor _meta.progressToken with notifications/progress while they block. Pass resumeFromRunId to execute a new run from a prior journal prefix. In hosts that render MCP Apps, every call of this tool shows a live self-updating run-monitor panel and the panel reports phase starts, pauses, and terminal outcomes on its own \u2014 do NOT poll action:"inspect" to check on a run there; prefer a single bounded action:"await". Use action:"inspect" with a runId when you need machine-readable status data: a safe bounded status, log tail, and attributed call previews. Use action:"stop" to durably abort through the run's execution owner; cross-generation control may return a durable pending operationId before final settlement. Add callIndex to cancel only that live agent and keep the run live. forceOwner explicitly authorizes terminating a superseded owner and is forbidden with callIndex. labelGlob remains an output filter. A final whole-run stop makes resume safe immediately; pending control must be retried or observed with inspect/await. Every admitted script is readable at workflow://runs/{runId}/script and results include resource links. Background runs are tracked per project, capped at four active/starting runs, and use headless checkpoint semantics; checkpointReplies continue a checkpoint pause in a new run.`,
|
|
34924
35145
|
inputSchema: workflowToolInputSchema,
|
|
34925
35146
|
outputSchema: workflowToolOutputSchema,
|
|
34926
35147
|
annotations: void 0
|
|
@@ -35144,20 +35365,34 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35144
35365
|
`Workflow run "${parsedInput.runId}" is already terminal (${persisted.status}); no agent call is in flight to cancel. Whole-run stop without callIndex is a successful no-op for terminal runs.`
|
|
35145
35366
|
);
|
|
35146
35367
|
}
|
|
35147
|
-
if (!manager.getRun(parsedInput.runId)) {
|
|
35148
|
-
throw new ProtocolError(
|
|
35149
|
-
ProtocolErrorCode.InvalidParams,
|
|
35150
|
-
`Workflow run "${parsedInput.runId}" is persisted as ${persisted.status}, but there is nothing live to cancel in this server process.`
|
|
35151
|
-
);
|
|
35152
|
-
}
|
|
35153
35368
|
let cancellation;
|
|
35154
|
-
|
|
35155
|
-
|
|
35156
|
-
|
|
35157
|
-
|
|
35158
|
-
|
|
35159
|
-
|
|
35160
|
-
|
|
35369
|
+
if (!manager.getRun(parsedInput.runId)) {
|
|
35370
|
+
if (!options.runControl) {
|
|
35371
|
+
throw new ProtocolError(
|
|
35372
|
+
ProtocolErrorCode.InvalidParams,
|
|
35373
|
+
`Workflow run "${parsedInput.runId}" is persisted as ${persisted.status}, but there is nothing live to cancel in this server process.`
|
|
35374
|
+
);
|
|
35375
|
+
}
|
|
35376
|
+
const routed = await options.runControl.control(manager, {
|
|
35377
|
+
runId: parsedInput.runId,
|
|
35378
|
+
callIndex: parsedInput.callIndex
|
|
35379
|
+
});
|
|
35380
|
+
if (routed.kind !== "agent") {
|
|
35381
|
+
throw new ProtocolError(
|
|
35382
|
+
ProtocolErrorCode.InternalError,
|
|
35383
|
+
`Workflow agent cancellation returned an invalid routed outcome for runId "${parsedInput.runId}".`
|
|
35384
|
+
);
|
|
35385
|
+
}
|
|
35386
|
+
cancellation = routed.cancellation;
|
|
35387
|
+
} else {
|
|
35388
|
+
try {
|
|
35389
|
+
cancellation = await manager.cancelAgentCall(parsedInput.runId, parsedInput.callIndex);
|
|
35390
|
+
} catch (error51) {
|
|
35391
|
+
throw new ProtocolError(
|
|
35392
|
+
error51 instanceof WorkflowError && error51.code === WorkflowErrorCode.PERSISTENCE_ERROR ? ProtocolErrorCode.InternalError : ProtocolErrorCode.InvalidParams,
|
|
35393
|
+
error51 instanceof Error ? error51.message : String(error51)
|
|
35394
|
+
);
|
|
35395
|
+
}
|
|
35161
35396
|
}
|
|
35162
35397
|
const status2 = manager.inspectRun(parsedInput.runId, inspectionOptions);
|
|
35163
35398
|
if (!status2) {
|
|
@@ -35189,22 +35424,97 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35189
35424
|
if (!alreadyTerminal) {
|
|
35190
35425
|
const live = manager.getRun(parsedInput.runId);
|
|
35191
35426
|
if (!live) {
|
|
35192
|
-
|
|
35193
|
-
|
|
35194
|
-
|
|
35195
|
-
|
|
35196
|
-
|
|
35197
|
-
|
|
35198
|
-
|
|
35199
|
-
|
|
35200
|
-
|
|
35201
|
-
|
|
35202
|
-
|
|
35203
|
-
|
|
35204
|
-
|
|
35205
|
-
|
|
35427
|
+
if (options.runControl) {
|
|
35428
|
+
const routed = await options.runControl.control(manager, {
|
|
35429
|
+
runId: parsedInput.runId,
|
|
35430
|
+
forceOwner: parsedInput.forceOwner
|
|
35431
|
+
});
|
|
35432
|
+
if (routed.kind !== "whole") {
|
|
35433
|
+
throw new ProtocolError(
|
|
35434
|
+
ProtocolErrorCode.InternalError,
|
|
35435
|
+
`Workflow stop returned an invalid routed outcome for runId "${parsedInput.runId}".`
|
|
35436
|
+
);
|
|
35437
|
+
}
|
|
35438
|
+
if (routed.state === "pending") {
|
|
35439
|
+
const pendingStatus = manager.inspectRun(parsedInput.runId, inspectionOptions);
|
|
35440
|
+
if (!pendingStatus) {
|
|
35441
|
+
throw new ProtocolError(
|
|
35442
|
+
ProtocolErrorCode.InvalidParams,
|
|
35443
|
+
`No workflow run found for runId "${parsedInput.runId}" in this server's project-scoped run store.`
|
|
35444
|
+
);
|
|
35445
|
+
}
|
|
35446
|
+
if (pendingStatus.status !== "pending" && pendingStatus.status !== "running") {
|
|
35447
|
+
throw new ProtocolError(
|
|
35448
|
+
ProtocolErrorCode.InternalError,
|
|
35449
|
+
`Workflow stop intent ${routed.operationId} remained pending but runId "${parsedInput.runId}" is ${pendingStatus.status}.`
|
|
35450
|
+
);
|
|
35451
|
+
}
|
|
35452
|
+
const lineage2 = scriptResources.lineage(parsedInput.runId);
|
|
35453
|
+
const projected2 = addInspectionResourceFields(
|
|
35454
|
+
pendingStatus,
|
|
35455
|
+
{
|
|
35456
|
+
scriptUri: workflowScriptUri(parsedInput.runId),
|
|
35457
|
+
lineage: lineage2,
|
|
35458
|
+
stopped: false,
|
|
35459
|
+
alreadyTerminal: false,
|
|
35460
|
+
control: {
|
|
35461
|
+
state: "pending",
|
|
35462
|
+
operationId: routed.operationId,
|
|
35463
|
+
requestedAt: routed.requestedAt,
|
|
35464
|
+
...routed.owner === void 0 ? {} : { owner: routed.owner }
|
|
35465
|
+
}
|
|
35466
|
+
},
|
|
35467
|
+
inspectionRetentionMetadata(manager, parsedInput.runId, pendingStatus)
|
|
35468
|
+
);
|
|
35469
|
+
const result2 = {
|
|
35470
|
+
...projected2,
|
|
35471
|
+
status: pendingStatus.status
|
|
35472
|
+
};
|
|
35473
|
+
const currentLink2 = scriptResources.links(lineage2).filter((link) => link.uri === workflowScriptUri(parsedInput.runId));
|
|
35474
|
+
return {
|
|
35475
|
+
structuredContent: { ...result2 },
|
|
35476
|
+
content: [{ type: "text", text: formatPendingStopSummary(result2) }, ...currentLink2],
|
|
35477
|
+
isError: false
|
|
35478
|
+
};
|
|
35479
|
+
}
|
|
35480
|
+
stopped = routed.stopped;
|
|
35481
|
+
alreadyTerminal = routed.alreadyTerminal;
|
|
35482
|
+
} else {
|
|
35483
|
+
const cold = manager.stopPersistedRun(parsedInput.runId);
|
|
35484
|
+
stopped = cold.outcome === "stopped";
|
|
35485
|
+
alreadyTerminal = cold.outcome === "already-terminal";
|
|
35486
|
+
if (cold.outcome === "owned-elsewhere") {
|
|
35487
|
+
throw new ProtocolError(
|
|
35488
|
+
ProtocolErrorCode.InvalidParams,
|
|
35489
|
+
`Workflow run "${parsedInput.runId}" is persisted as ${persisted.status} and is owned by another live process; this server has no daemon run-control router.`
|
|
35490
|
+
);
|
|
35491
|
+
}
|
|
35492
|
+
if (cold.outcome === "missing") {
|
|
35493
|
+
throw new ProtocolError(
|
|
35494
|
+
ProtocolErrorCode.InvalidParams,
|
|
35495
|
+
`No workflow run found for runId "${parsedInput.runId}" in this server's project-scoped run store.`
|
|
35496
|
+
);
|
|
35497
|
+
}
|
|
35206
35498
|
}
|
|
35207
35499
|
} else {
|
|
35500
|
+
stopped = manager.stop(parsedInput.runId);
|
|
35501
|
+
if (!stopped) {
|
|
35502
|
+
const current = manager.getPersistence().load(parsedInput.runId);
|
|
35503
|
+
alreadyTerminal = current !== null && isAlreadyTerminalForStop(current.status);
|
|
35504
|
+
if (!alreadyTerminal) {
|
|
35505
|
+
const cold = manager.stopPersistedRun(parsedInput.runId);
|
|
35506
|
+
stopped = cold.outcome === "stopped";
|
|
35507
|
+
alreadyTerminal = cold.outcome === "already-terminal";
|
|
35508
|
+
}
|
|
35509
|
+
if (!stopped && !alreadyTerminal) {
|
|
35510
|
+
throw new ProtocolError(
|
|
35511
|
+
ProtocolErrorCode.InvalidParams,
|
|
35512
|
+
`Workflow run "${parsedInput.runId}" could not be stopped; its persisted status is ${current?.status ?? persisted.status}.`
|
|
35513
|
+
);
|
|
35514
|
+
}
|
|
35515
|
+
}
|
|
35516
|
+
}
|
|
35517
|
+
if (stopped) {
|
|
35208
35518
|
scriptResources.cancelPendingElicitation(parsedInput.runId);
|
|
35209
35519
|
requireDurableStoppedRun(manager, parsedInput.runId);
|
|
35210
35520
|
}
|
|
@@ -35523,6 +35833,7 @@ ${lines.join("\n")}`,
|
|
|
35523
35833
|
concurrency: input.concurrency,
|
|
35524
35834
|
agentRetries: input.agentRetries,
|
|
35525
35835
|
agentTimeoutMs: input.agentTimeoutMs,
|
|
35836
|
+
agentIdleTimeoutMs: input.agentIdleTimeoutMs,
|
|
35526
35837
|
resumeFromRunId: input.resumeFromRunId,
|
|
35527
35838
|
resumePolicy: input.resumePolicy,
|
|
35528
35839
|
checkpointReplies: input.checkpointReplies
|
|
@@ -35693,24 +36004,24 @@ runId: ${started2.runId}
|
|
|
35693
36004
|
}
|
|
35694
36005
|
|
|
35695
36006
|
// ../mcp-server/src/shim/ensure-daemon.ts
|
|
35696
|
-
import { closeSync, mkdirSync as
|
|
36007
|
+
import { closeSync, mkdirSync as mkdirSync3, openSync } from "node:fs";
|
|
35697
36008
|
import { homedir } from "node:os";
|
|
35698
36009
|
import { dirname as dirname2 } from "node:path";
|
|
35699
36010
|
import { spawn } from "node:child_process";
|
|
35700
36011
|
|
|
35701
36012
|
// ../mcp-server/src/daemon/daemon-info.ts
|
|
35702
|
-
import { randomUUID } from "node:crypto";
|
|
36013
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
35703
36014
|
import { createHash as createHash2 } from "node:crypto";
|
|
35704
36015
|
import {
|
|
35705
|
-
chmodSync,
|
|
35706
|
-
mkdirSync,
|
|
35707
|
-
readdirSync as
|
|
35708
|
-
readFileSync as
|
|
36016
|
+
chmodSync as chmodSync2,
|
|
36017
|
+
mkdirSync as mkdirSync2,
|
|
36018
|
+
readdirSync as readdirSync3,
|
|
36019
|
+
readFileSync as readFileSync4,
|
|
35709
36020
|
renameSync as renameSync2,
|
|
35710
|
-
rmSync,
|
|
35711
|
-
writeFileSync
|
|
36021
|
+
rmSync as rmSync2,
|
|
36022
|
+
writeFileSync as writeFileSync2
|
|
35712
36023
|
} from "node:fs";
|
|
35713
|
-
import { dirname, join as
|
|
36024
|
+
import { dirname, join as join3 } from "node:path";
|
|
35714
36025
|
import { workflowHomeDir as workflowHomeDir2 } from "@automatalabs/workflows";
|
|
35715
36026
|
async function probeHealthz(port, timeoutMs = 2e3) {
|
|
35716
36027
|
try {
|
|
@@ -35726,22 +36037,22 @@ async function probeHealthz(port, timeoutMs = 2e3) {
|
|
|
35726
36037
|
}
|
|
35727
36038
|
}
|
|
35728
36039
|
function daemonsDir() {
|
|
35729
|
-
return
|
|
36040
|
+
return join3(workflowHomeDir2(), "daemons");
|
|
35730
36041
|
}
|
|
35731
36042
|
function daemonInfoPath(fingerprint = envFingerprint()) {
|
|
35732
|
-
return
|
|
36043
|
+
return join3(daemonsDir(), `${fingerprint}.json`);
|
|
35733
36044
|
}
|
|
35734
36045
|
function daemonLockPath(fingerprint = envFingerprint()) {
|
|
35735
|
-
return
|
|
36046
|
+
return join3(daemonsDir(), `${fingerprint}.lock`);
|
|
35736
36047
|
}
|
|
35737
36048
|
function daemonInstancePath(pid) {
|
|
35738
|
-
return
|
|
36049
|
+
return join3(daemonsDir(), "instances", `${pid}.json`);
|
|
35739
36050
|
}
|
|
35740
36051
|
function legacyDaemonInfoPath() {
|
|
35741
|
-
return
|
|
36052
|
+
return join3(workflowHomeDir2(), "daemon.json");
|
|
35742
36053
|
}
|
|
35743
36054
|
function daemonLogPath() {
|
|
35744
|
-
return
|
|
36055
|
+
return join3(workflowHomeDir2(), "logs", "daemon.log");
|
|
35745
36056
|
}
|
|
35746
36057
|
function pidIsAlive(pid) {
|
|
35747
36058
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -35755,7 +36066,7 @@ function pidIsAlive(pid) {
|
|
|
35755
36066
|
}
|
|
35756
36067
|
function readInfoFile(path) {
|
|
35757
36068
|
try {
|
|
35758
|
-
const parsed = JSON.parse(
|
|
36069
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
35759
36070
|
if (parsed.name !== DAEMON_NAME || typeof parsed.pid !== "number" || typeof parsed.port !== "number" || typeof parsed.url !== "string" || typeof parsed.version !== "string") {
|
|
35760
36071
|
return void 0;
|
|
35761
36072
|
}
|
|
@@ -35767,12 +36078,15 @@ function readInfoFile(path) {
|
|
|
35767
36078
|
function readDaemonInfo(fingerprint = envFingerprint()) {
|
|
35768
36079
|
return readInfoFile(daemonInfoPath(fingerprint));
|
|
35769
36080
|
}
|
|
36081
|
+
function readDaemonInstance(pid) {
|
|
36082
|
+
return readInfoFile(daemonInstancePath(pid));
|
|
36083
|
+
}
|
|
35770
36084
|
function writeInfoFile(path, info) {
|
|
35771
|
-
|
|
35772
|
-
const tmp = `${path}.${info.pid}.${
|
|
35773
|
-
|
|
36085
|
+
mkdirSync2(dirname(path), { recursive: true });
|
|
36086
|
+
const tmp = `${path}.${info.pid}.${randomUUID2().slice(0, 8)}.tmp`;
|
|
36087
|
+
writeFileSync2(tmp, `${JSON.stringify(info, null, 2)}
|
|
35774
36088
|
`, { mode: 384 });
|
|
35775
|
-
|
|
36089
|
+
chmodSync2(tmp, 384);
|
|
35776
36090
|
renameSync2(tmp, path);
|
|
35777
36091
|
}
|
|
35778
36092
|
function writeDaemonInfo(info) {
|
|
@@ -35784,11 +36098,11 @@ function clearDaemonInfo(pid) {
|
|
|
35784
36098
|
const fingerprint = instance?.envFingerprint ?? envFingerprint();
|
|
35785
36099
|
const current = readDaemonInfo(fingerprint);
|
|
35786
36100
|
try {
|
|
35787
|
-
if (current !== void 0 && current.pid === pid)
|
|
36101
|
+
if (current !== void 0 && current.pid === pid) rmSync2(daemonInfoPath(fingerprint), { force: true });
|
|
35788
36102
|
} catch {
|
|
35789
36103
|
}
|
|
35790
36104
|
try {
|
|
35791
|
-
|
|
36105
|
+
rmSync2(daemonInstancePath(pid), { force: true });
|
|
35792
36106
|
} catch {
|
|
35793
36107
|
}
|
|
35794
36108
|
}
|
|
@@ -35799,20 +36113,20 @@ function isSupersededBy(ownPid, fingerprint = envFingerprint()) {
|
|
|
35799
36113
|
function listDaemonInstances() {
|
|
35800
36114
|
const instances = [];
|
|
35801
36115
|
const seen = /* @__PURE__ */ new Set();
|
|
35802
|
-
const dir =
|
|
36116
|
+
const dir = join3(daemonsDir(), "instances");
|
|
35803
36117
|
let files = [];
|
|
35804
36118
|
try {
|
|
35805
|
-
files =
|
|
36119
|
+
files = readdirSync3(dir);
|
|
35806
36120
|
} catch {
|
|
35807
36121
|
files = [];
|
|
35808
36122
|
}
|
|
35809
36123
|
for (const file2 of files) {
|
|
35810
36124
|
if (!file2.endsWith(".json")) continue;
|
|
35811
|
-
const path =
|
|
36125
|
+
const path = join3(dir, file2);
|
|
35812
36126
|
const info = readInfoFile(path);
|
|
35813
36127
|
if (info === void 0 || !pidIsAlive(info.pid)) {
|
|
35814
36128
|
try {
|
|
35815
|
-
|
|
36129
|
+
rmSync2(path, { force: true });
|
|
35816
36130
|
} catch {
|
|
35817
36131
|
}
|
|
35818
36132
|
continue;
|
|
@@ -35855,23 +36169,23 @@ function compareVersions(a, b) {
|
|
|
35855
36169
|
}
|
|
35856
36170
|
function claimSpawnLock(fingerprint = envFingerprint()) {
|
|
35857
36171
|
const path = daemonLockPath(fingerprint);
|
|
35858
|
-
|
|
35859
|
-
const lock = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString(), token:
|
|
36172
|
+
mkdirSync2(dirname(path), { recursive: true });
|
|
36173
|
+
const lock = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString(), token: randomUUID2() };
|
|
35860
36174
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
35861
36175
|
try {
|
|
35862
|
-
|
|
36176
|
+
writeFileSync2(path, JSON.stringify(lock), { flag: "wx", mode: 384 });
|
|
35863
36177
|
return lock;
|
|
35864
36178
|
} catch (err) {
|
|
35865
36179
|
if (err.code !== "EEXIST") return null;
|
|
35866
36180
|
let holder = null;
|
|
35867
36181
|
try {
|
|
35868
|
-
holder = JSON.parse(
|
|
36182
|
+
holder = JSON.parse(readFileSync4(path, "utf-8"));
|
|
35869
36183
|
} catch {
|
|
35870
36184
|
holder = null;
|
|
35871
36185
|
}
|
|
35872
36186
|
if (holder !== null && pidIsAlive(holder.pid)) return null;
|
|
35873
36187
|
try {
|
|
35874
|
-
|
|
36188
|
+
rmSync2(path, { force: true });
|
|
35875
36189
|
} catch {
|
|
35876
36190
|
return null;
|
|
35877
36191
|
}
|
|
@@ -35882,9 +36196,9 @@ function claimSpawnLock(fingerprint = envFingerprint()) {
|
|
|
35882
36196
|
function releaseSpawnLock(lock, fingerprint = envFingerprint()) {
|
|
35883
36197
|
const path = daemonLockPath(fingerprint);
|
|
35884
36198
|
try {
|
|
35885
|
-
const holder = JSON.parse(
|
|
36199
|
+
const holder = JSON.parse(readFileSync4(path, "utf-8"));
|
|
35886
36200
|
if (holder.token !== lock.token) return;
|
|
35887
|
-
|
|
36201
|
+
rmSync2(path, { force: true });
|
|
35888
36202
|
} catch {
|
|
35889
36203
|
}
|
|
35890
36204
|
}
|
|
@@ -35898,7 +36212,14 @@ async function probeLiveDaemon(fingerprint) {
|
|
|
35898
36212
|
if (info === void 0 || !pidIsAlive(info.pid)) return void 0;
|
|
35899
36213
|
const health = await probeHealthz(info.port);
|
|
35900
36214
|
if (health === void 0 || health.pid !== info.pid) return void 0;
|
|
35901
|
-
return {
|
|
36215
|
+
return {
|
|
36216
|
+
info,
|
|
36217
|
+
sessions: health.sessions,
|
|
36218
|
+
activeRuns: health.activeRuns,
|
|
36219
|
+
inflightRequests: health.inflightRequests ?? 0,
|
|
36220
|
+
controlProtocol: health.controlProtocol === 1 ? 1 : void 0,
|
|
36221
|
+
version: health.version
|
|
36222
|
+
};
|
|
35902
36223
|
}
|
|
35903
36224
|
async function waitForCurrentDaemon(fingerprint, timeoutMs) {
|
|
35904
36225
|
const start = Date.now();
|
|
@@ -35924,7 +36245,7 @@ async function stopDaemon(pid, timeoutMs = 5e3) {
|
|
|
35924
36245
|
}
|
|
35925
36246
|
function spawnDetachedDaemon(args) {
|
|
35926
36247
|
const logPath = daemonLogPath();
|
|
35927
|
-
|
|
36248
|
+
mkdirSync3(dirname2(logPath), { recursive: true });
|
|
35928
36249
|
const logFd = openSync(logPath, "a");
|
|
35929
36250
|
const argv = [args.bundlePath, "--daemon-run"];
|
|
35930
36251
|
if (args.port !== void 0) argv.push("--port", String(args.port));
|
|
@@ -35951,6 +36272,12 @@ async function ensureDaemonRunning(options) {
|
|
|
35951
36272
|
};
|
|
35952
36273
|
const live = await probeLiveDaemon(fingerprint);
|
|
35953
36274
|
if (live !== void 0 && !isStale(live)) return adopt(live);
|
|
36275
|
+
if (live !== void 0 && live.controlProtocol !== 1 && (live.activeRuns > 0 || live.inflightRequests > 0)) {
|
|
36276
|
+
options.log(
|
|
36277
|
+
`[${DAEMON_NAME}] compatibility drain: temporarily adopting stale daemon pid ${live.info.pid} (v${live.version}, ${live.activeRuns} run(s), ${live.inflightRequests} request(s) in flight) because it predates run-control v1`
|
|
36278
|
+
);
|
|
36279
|
+
return { ...live.info, compatibilityDrain: true };
|
|
36280
|
+
}
|
|
35954
36281
|
if (live !== void 0) {
|
|
35955
36282
|
options.log(
|
|
35956
36283
|
`[${DAEMON_NAME}] superseding stale daemon (pid ${live.info.pid}, ${live.sessions} session(s), ${live.activeRuns} run(s)): version v${live.version} \u2192 v${SERVER_VERSION} \u2014 spawning a successor and repointing discovery`
|
|
@@ -35967,6 +36294,12 @@ async function ensureDaemonRunning(options) {
|
|
|
35967
36294
|
try {
|
|
35968
36295
|
const raced = await probeLiveDaemon(fingerprint);
|
|
35969
36296
|
if (raced !== void 0 && !isStale(raced)) return adopt(raced);
|
|
36297
|
+
if (raced !== void 0 && raced.controlProtocol !== 1 && (raced.activeRuns > 0 || raced.inflightRequests > 0)) {
|
|
36298
|
+
options.log(
|
|
36299
|
+
`[${DAEMON_NAME}] compatibility drain retained under the spawn lock for stale daemon pid ${raced.info.pid}`
|
|
36300
|
+
);
|
|
36301
|
+
return { ...raced.info, compatibilityDrain: true };
|
|
36302
|
+
}
|
|
35970
36303
|
const superseding = raced !== void 0;
|
|
35971
36304
|
spawnDaemon({ bundlePath: options.bundlePath, port: options.port, supersede: superseding });
|
|
35972
36305
|
const info = await waitForCurrentDaemon(fingerprint, SPAWN_HEALTH_TIMEOUT_MS);
|
|
@@ -35987,6 +36320,7 @@ async function ensureDaemonRunning(options) {
|
|
|
35987
36320
|
}
|
|
35988
36321
|
|
|
35989
36322
|
// ../mcp-server/src/daemon/run-daemon.ts
|
|
36323
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
35990
36324
|
import { createAcpRunner } from "@automatalabs/workflows";
|
|
35991
36325
|
|
|
35992
36326
|
// ../mcp-server/src/daemon/daemon-lifecycle.ts
|
|
@@ -36006,6 +36340,7 @@ function installDaemonLifecycle(options) {
|
|
|
36006
36340
|
const reaper = setInterval(() => {
|
|
36007
36341
|
const daemon = options.daemon;
|
|
36008
36342
|
const superseded = daemon.isSuperseded();
|
|
36343
|
+
void daemon.processPendingControlIntents?.();
|
|
36009
36344
|
if (superseded) {
|
|
36010
36345
|
if (!supersessionAnnounced) {
|
|
36011
36346
|
supersessionAnnounced = true;
|
|
@@ -36013,11 +36348,9 @@ function installDaemonLifecycle(options) {
|
|
|
36013
36348
|
`[agentprism-daemon] superseded by a newer daemon; draining \u2014 ${daemon.sessions.size} session(s), ${daemon.activeRunCount()} run(s), ${daemon.inflightRequestCount()} request(s) in flight`
|
|
36014
36349
|
);
|
|
36015
36350
|
}
|
|
36016
|
-
|
|
36017
|
-
|
|
36018
|
-
|
|
36019
|
-
log(`[agentprism-daemon] migrated ${migrated.length} idle session(s) to the successor: ${migrated.join(", ")}`);
|
|
36020
|
-
}
|
|
36351
|
+
const migrated = daemon.evictDrainableSessions();
|
|
36352
|
+
if (migrated.length > 0) {
|
|
36353
|
+
log(`[agentprism-daemon] migrated ${migrated.length} idle session(s) to the successor: ${migrated.join(", ")}`);
|
|
36021
36354
|
}
|
|
36022
36355
|
} else {
|
|
36023
36356
|
supersessionAnnounced = false;
|
|
@@ -36026,7 +36359,7 @@ function installDaemonLifecycle(options) {
|
|
|
36026
36359
|
log(`[agentprism-daemon] evicted ${evicted.length} idle session(s): ${evicted.join(", ")}`);
|
|
36027
36360
|
}
|
|
36028
36361
|
}
|
|
36029
|
-
const busy = daemon.sessions.size > 0 || daemon.activeRunCount() > 0 || daemon.activeReplDrainCount() > 0;
|
|
36362
|
+
const busy = daemon.sessions.size > 0 || daemon.activeRunCount() > 0 || daemon.inflightRequestCount() > 0 || daemon.activeReplDrainCount() > 0;
|
|
36030
36363
|
if (busy) {
|
|
36031
36364
|
idleSince = void 0;
|
|
36032
36365
|
return;
|
|
@@ -36069,7 +36402,7 @@ function installDaemonLifecycle(options) {
|
|
|
36069
36402
|
|
|
36070
36403
|
// ../mcp-server/src/daemon/http-daemon.ts
|
|
36071
36404
|
import http from "node:http";
|
|
36072
|
-
import { randomUUID as
|
|
36405
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
36073
36406
|
|
|
36074
36407
|
// ../../node_modules/.pnpm/@hono+node-server@1.19.14_hono@4.12.27/node_modules/@hono/node-server/dist/index.mjs
|
|
36075
36408
|
import { Http2ServerRequest as Http2ServerRequest2, constants as h2constants } from "http2";
|
|
@@ -37032,16 +37365,16 @@ function validateRequest(headers, boundPort, env = process.env) {
|
|
|
37032
37365
|
|
|
37033
37366
|
// ../mcp-server/src/daemon/request-state.ts
|
|
37034
37367
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
37035
|
-
import { chmodSync as
|
|
37036
|
-
import { dirname as dirname3, join as
|
|
37368
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
37369
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
37037
37370
|
import { workflowHomeDir as workflowHomeDir3 } from "@automatalabs/workflows";
|
|
37038
37371
|
function requestStateKeyPath(fingerprint) {
|
|
37039
|
-
return
|
|
37372
|
+
return join4(workflowHomeDir3(), "daemons", `${fingerprint}.request-state-key.json`);
|
|
37040
37373
|
}
|
|
37041
37374
|
function decodeStoredKey(path) {
|
|
37042
37375
|
let parsed;
|
|
37043
37376
|
try {
|
|
37044
|
-
parsed = JSON.parse(
|
|
37377
|
+
parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
37045
37378
|
} catch (error51) {
|
|
37046
37379
|
throw new Error(`Unable to read the daemon requestState key at ${path}: ${String(error51)}`);
|
|
37047
37380
|
}
|
|
@@ -37050,18 +37383,18 @@ function decodeStoredKey(path) {
|
|
|
37050
37383
|
}
|
|
37051
37384
|
const key = Buffer.from(parsed.key, "base64");
|
|
37052
37385
|
if (key.byteLength !== 32) throw new Error(`Invalid daemon requestState key length at ${path}`);
|
|
37053
|
-
|
|
37386
|
+
chmodSync3(path, 384);
|
|
37054
37387
|
return key;
|
|
37055
37388
|
}
|
|
37056
37389
|
function loadOrCreateRequestStateKey(fingerprint) {
|
|
37057
37390
|
const path = requestStateKeyPath(fingerprint);
|
|
37058
|
-
|
|
37391
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
37059
37392
|
const key = randomBytes2(32);
|
|
37060
37393
|
const stored = { version: 1, key: key.toString("base64") };
|
|
37061
37394
|
try {
|
|
37062
|
-
|
|
37395
|
+
writeFileSync3(path, `${JSON.stringify(stored)}
|
|
37063
37396
|
`, { flag: "wx", mode: 384 });
|
|
37064
|
-
|
|
37397
|
+
chmodSync3(path, 384);
|
|
37065
37398
|
return key;
|
|
37066
37399
|
} catch (error51) {
|
|
37067
37400
|
if (error51.code !== "EEXIST") throw error51;
|
|
@@ -37069,6 +37402,337 @@ function loadOrCreateRequestStateKey(fingerprint) {
|
|
|
37069
37402
|
}
|
|
37070
37403
|
}
|
|
37071
37404
|
|
|
37405
|
+
// ../mcp-server/src/daemon/run-control-auth.ts
|
|
37406
|
+
import { createHash as createHash3, createHmac, randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
|
|
37407
|
+
import { chmodSync as chmodSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
37408
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
37409
|
+
import { workflowHomeDir as workflowHomeDir4 } from "@automatalabs/workflows";
|
|
37410
|
+
var RUN_CONTROL_PATH = "/_agentprism/control/v1/run";
|
|
37411
|
+
var RUN_CONTROL_PROTOCOL = 1;
|
|
37412
|
+
var RUN_CONTROL_MAX_CLOCK_SKEW_MS = 3e4;
|
|
37413
|
+
function runControlKeyPath() {
|
|
37414
|
+
return join5(workflowHomeDir4(), "daemons", "run-control-key.json");
|
|
37415
|
+
}
|
|
37416
|
+
function decodeStoredKey2(path) {
|
|
37417
|
+
let parsed;
|
|
37418
|
+
try {
|
|
37419
|
+
parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
37420
|
+
} catch (error51) {
|
|
37421
|
+
throw new Error(`Unable to read the daemon run-control key at ${path}: ${String(error51)}`);
|
|
37422
|
+
}
|
|
37423
|
+
if (parsed.version !== 1 || typeof parsed.key !== "string") {
|
|
37424
|
+
throw new Error(`Invalid daemon run-control key file at ${path}`);
|
|
37425
|
+
}
|
|
37426
|
+
const key = Buffer.from(parsed.key, "base64");
|
|
37427
|
+
if (key.byteLength !== 32) throw new Error(`Invalid daemon run-control key length at ${path}`);
|
|
37428
|
+
chmodSync4(path, 384);
|
|
37429
|
+
return key;
|
|
37430
|
+
}
|
|
37431
|
+
function loadOrCreateRunControlKey() {
|
|
37432
|
+
const path = runControlKeyPath();
|
|
37433
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
37434
|
+
const key = randomBytes3(32);
|
|
37435
|
+
const stored = { version: 1, key: key.toString("base64") };
|
|
37436
|
+
try {
|
|
37437
|
+
writeFileSync4(path, `${JSON.stringify(stored)}
|
|
37438
|
+
`, { flag: "wx", mode: 384 });
|
|
37439
|
+
chmodSync4(path, 384);
|
|
37440
|
+
return key;
|
|
37441
|
+
} catch (error51) {
|
|
37442
|
+
if (error51.code !== "EEXIST") throw error51;
|
|
37443
|
+
return decodeStoredKey2(path);
|
|
37444
|
+
}
|
|
37445
|
+
}
|
|
37446
|
+
function signaturePayload(method, path, timestamp, operationId, body) {
|
|
37447
|
+
const bodyHash = createHash3("sha256").update(body).digest("hex");
|
|
37448
|
+
return `${method.toUpperCase()}
|
|
37449
|
+
${path}
|
|
37450
|
+
${timestamp}
|
|
37451
|
+
${operationId}
|
|
37452
|
+
${bodyHash}`;
|
|
37453
|
+
}
|
|
37454
|
+
function signRunControlRequest(key, method, path, operationId, body, now = Date.now()) {
|
|
37455
|
+
const timestamp = String(now);
|
|
37456
|
+
const signature = createHmac("sha256", key).update(signaturePayload(method, path, timestamp, operationId, body)).digest("hex");
|
|
37457
|
+
return {
|
|
37458
|
+
"x-agentprism-control-timestamp": timestamp,
|
|
37459
|
+
"x-agentprism-control-operation": operationId,
|
|
37460
|
+
"x-agentprism-control-signature": signature
|
|
37461
|
+
};
|
|
37462
|
+
}
|
|
37463
|
+
function verifyRunControlRequest(key, input) {
|
|
37464
|
+
if (!input.timestamp || !input.operationId || !input.signature) return false;
|
|
37465
|
+
const timestamp = Number(input.timestamp);
|
|
37466
|
+
const now = input.now ?? Date.now();
|
|
37467
|
+
if (!Number.isSafeInteger(timestamp) || Math.abs(now - timestamp) > RUN_CONTROL_MAX_CLOCK_SKEW_MS) return false;
|
|
37468
|
+
if (!/^[0-9a-f]{64}$/i.test(input.signature)) return false;
|
|
37469
|
+
const expected = createHmac("sha256", key).update(signaturePayload(input.method, input.path, input.timestamp, input.operationId, input.body)).digest();
|
|
37470
|
+
const actual = Buffer.from(input.signature, "hex");
|
|
37471
|
+
return actual.byteLength === expected.byteLength && timingSafeEqual(actual, expected);
|
|
37472
|
+
}
|
|
37473
|
+
|
|
37474
|
+
// ../mcp-server/src/daemon/run-control.ts
|
|
37475
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
37476
|
+
var FORWARD_TIMEOUT_MS = 5e3;
|
|
37477
|
+
var FORCE_TERM_WAIT_MS = 5e3;
|
|
37478
|
+
var FORCE_KILL_WAIT_MS = 2e3;
|
|
37479
|
+
function terminal(status) {
|
|
37480
|
+
return status === "completed" || status === "failed" || status === "aborted";
|
|
37481
|
+
}
|
|
37482
|
+
function ownerView(owner) {
|
|
37483
|
+
if (!owner) return void 0;
|
|
37484
|
+
return {
|
|
37485
|
+
pid: owner.pid,
|
|
37486
|
+
...owner.daemon?.instanceId === void 0 ? {} : { instanceId: owner.daemon.instanceId },
|
|
37487
|
+
...owner.daemon?.version === void 0 ? {} : { version: owner.daemon.version },
|
|
37488
|
+
...owner.health?.lameDuck === void 0 ? {} : { lameDuck: owner.health.lameDuck },
|
|
37489
|
+
...owner.health?.activeRuns === void 0 ? {} : { activeRuns: owner.health.activeRuns },
|
|
37490
|
+
...owner.daemon?.controlProtocol === 1 ? { controlProtocol: 1 } : {}
|
|
37491
|
+
};
|
|
37492
|
+
}
|
|
37493
|
+
function actionableOwnerMessage(runId, owner, action) {
|
|
37494
|
+
if (!owner) return `Workflow run "${runId}" has no discoverable live execution owner for ${action}.`;
|
|
37495
|
+
const version2 = owner.daemon?.version ? ` v${owner.daemon.version}` : "";
|
|
37496
|
+
const draining = owner.health?.lameDuck ? ", draining" : "";
|
|
37497
|
+
const control = owner.daemon?.controlProtocol === 1 ? "control v1" : "no compatible run-control endpoint";
|
|
37498
|
+
return `Workflow run "${runId}" is executing in daemon pid ${owner.pid}${version2} (${control}${draining}); ${action} could not be delivered.`;
|
|
37499
|
+
}
|
|
37500
|
+
async function waitForPidExit(pid, timeoutMs, alive) {
|
|
37501
|
+
const deadline = Date.now() + timeoutMs;
|
|
37502
|
+
while (Date.now() < deadline) {
|
|
37503
|
+
if (!alive(pid)) return true;
|
|
37504
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
37505
|
+
}
|
|
37506
|
+
return !alive(pid);
|
|
37507
|
+
}
|
|
37508
|
+
var DaemonRunControl = class {
|
|
37509
|
+
constructor(options) {
|
|
37510
|
+
this.options = options;
|
|
37511
|
+
this.log = options.log ?? (() => void 0);
|
|
37512
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
37513
|
+
this.killProcess = options.kill ?? ((pid, signal) => process.kill(pid, signal));
|
|
37514
|
+
this.isPidAlive = options.isPidAlive ?? pidIsAlive;
|
|
37515
|
+
}
|
|
37516
|
+
options;
|
|
37517
|
+
log;
|
|
37518
|
+
fetchImpl;
|
|
37519
|
+
killProcess;
|
|
37520
|
+
isPidAlive;
|
|
37521
|
+
processingPending;
|
|
37522
|
+
async resolveOwner(manager, runId) {
|
|
37523
|
+
const lease = manager.getPersistence().inspectRunLease?.(runId);
|
|
37524
|
+
if (!lease || !this.isPidAlive(lease.pid)) return void 0;
|
|
37525
|
+
const daemon = readDaemonInstance(lease.pid);
|
|
37526
|
+
if (daemon?.instanceId !== void 0 && lease.ownerId !== void 0 && daemon.instanceId !== lease.ownerId) {
|
|
37527
|
+
return { pid: lease.pid, ownerId: lease.ownerId };
|
|
37528
|
+
}
|
|
37529
|
+
const health = daemon === void 0 ? void 0 : await probeHealthz(daemon.port, 1e3);
|
|
37530
|
+
const verifiedHealth = health !== void 0 && health.pid === lease.pid && (lease.ownerId === void 0 || health.instanceId === void 0 || health.instanceId === lease.ownerId) ? health : void 0;
|
|
37531
|
+
return { pid: lease.pid, ownerId: lease.ownerId, ...daemon ? { daemon } : {}, ...verifiedHealth ? { health: verifiedHealth } : {} };
|
|
37532
|
+
}
|
|
37533
|
+
controlCapable(owner) {
|
|
37534
|
+
return owner.daemon?.controlProtocol === 1 && typeof owner.daemon.controlUrl === "string" && (owner.ownerId === void 0 || owner.daemon.instanceId === owner.ownerId);
|
|
37535
|
+
}
|
|
37536
|
+
async post(owner, request) {
|
|
37537
|
+
const body = JSON.stringify(request);
|
|
37538
|
+
const headers = signRunControlRequest(
|
|
37539
|
+
this.options.key,
|
|
37540
|
+
"POST",
|
|
37541
|
+
RUN_CONTROL_PATH,
|
|
37542
|
+
request.operationId,
|
|
37543
|
+
body
|
|
37544
|
+
);
|
|
37545
|
+
const response = await this.fetchImpl(owner.daemon.controlUrl, {
|
|
37546
|
+
method: "POST",
|
|
37547
|
+
headers: { "content-type": "application/json", ...headers },
|
|
37548
|
+
body,
|
|
37549
|
+
signal: AbortSignal.timeout(FORWARD_TIMEOUT_MS)
|
|
37550
|
+
});
|
|
37551
|
+
const value = await response.json();
|
|
37552
|
+
if (!response.ok && value.ok !== false) throw new Error(`run-control owner returned HTTP ${response.status}`);
|
|
37553
|
+
return value;
|
|
37554
|
+
}
|
|
37555
|
+
settledWhole(manager, runId) {
|
|
37556
|
+
const current = manager.getPersistence().load(runId);
|
|
37557
|
+
if (!current || !terminal(current.status)) return void 0;
|
|
37558
|
+
if (current.status === "aborted") {
|
|
37559
|
+
try {
|
|
37560
|
+
requireDurableStoppedRun(manager, runId);
|
|
37561
|
+
return { stopped: true, alreadyTerminal: false };
|
|
37562
|
+
} catch {
|
|
37563
|
+
return { stopped: false, alreadyTerminal: true };
|
|
37564
|
+
}
|
|
37565
|
+
}
|
|
37566
|
+
return { stopped: false, alreadyTerminal: true };
|
|
37567
|
+
}
|
|
37568
|
+
applyWholeIntent(manager, intent) {
|
|
37569
|
+
const before = manager.getPersistence().load(intent.runId);
|
|
37570
|
+
if (!before) return { ok: false, code: "UNKNOWN_RUN", message: `No workflow run found for ${intent.runId}` };
|
|
37571
|
+
if (terminal(before.status)) {
|
|
37572
|
+
acknowledgeWholeStopIntent(manager, intent, this.options.ownInstanceId, "already-terminal");
|
|
37573
|
+
return { ok: true, outcome: "already-terminal" };
|
|
37574
|
+
}
|
|
37575
|
+
const stopped = manager.getRun(intent.runId) ? manager.stop(intent.runId) ? { outcome: "stopped" } : { outcome: "owned-elsewhere" } : manager.stopPersistedRun(intent.runId);
|
|
37576
|
+
if (stopped.outcome === "owned-elsewhere") {
|
|
37577
|
+
return { ok: false, code: "NOT_OWNER", message: `Daemon no longer owns run ${intent.runId}` };
|
|
37578
|
+
}
|
|
37579
|
+
if (stopped.outcome === "missing") {
|
|
37580
|
+
return { ok: false, code: "UNKNOWN_RUN", message: `No workflow run found for ${intent.runId}` };
|
|
37581
|
+
}
|
|
37582
|
+
if (stopped.outcome === "already-terminal") {
|
|
37583
|
+
acknowledgeWholeStopIntent(manager, intent, this.options.ownInstanceId, "already-terminal");
|
|
37584
|
+
return { ok: true, outcome: "already-terminal" };
|
|
37585
|
+
}
|
|
37586
|
+
requireDurableStoppedRun(manager, intent.runId);
|
|
37587
|
+
acknowledgeWholeStopIntent(manager, intent, this.options.ownInstanceId, "stopped");
|
|
37588
|
+
return { ok: true, outcome: "stopped" };
|
|
37589
|
+
}
|
|
37590
|
+
async applyLocal(request) {
|
|
37591
|
+
const context = this.options.projects.storeFor(request.runId);
|
|
37592
|
+
if (!context) return { ok: false, code: "UNKNOWN_RUN", message: `No workflow run found for ${request.runId}` };
|
|
37593
|
+
const manager = context.manager;
|
|
37594
|
+
if (request.action === "stop") {
|
|
37595
|
+
const intent = readWholeStopIntent(manager, request.runId, request.operationId);
|
|
37596
|
+
if (!intent) return { ok: false, code: "INVALID_OPERATION", message: "Durable stop intent is missing" };
|
|
37597
|
+
return this.applyWholeIntent(manager, intent);
|
|
37598
|
+
}
|
|
37599
|
+
if (!manager.getRun(request.runId)) {
|
|
37600
|
+
return { ok: false, code: "NOT_OWNER", message: `Daemon has no live run ${request.runId}` };
|
|
37601
|
+
}
|
|
37602
|
+
try {
|
|
37603
|
+
const cancellation = await manager.cancelAgentCall(request.runId, request.callIndex);
|
|
37604
|
+
return { ok: true, outcome: "agent-cancelled", cancellation };
|
|
37605
|
+
} catch (error51) {
|
|
37606
|
+
return {
|
|
37607
|
+
ok: false,
|
|
37608
|
+
code: "INVALID_OPERATION",
|
|
37609
|
+
message: error51 instanceof Error ? error51.message : String(error51)
|
|
37610
|
+
};
|
|
37611
|
+
}
|
|
37612
|
+
}
|
|
37613
|
+
processPendingIntents() {
|
|
37614
|
+
if (this.processingPending) return this.processingPending;
|
|
37615
|
+
this.processingPending = (async () => {
|
|
37616
|
+
for (const context of this.options.projects.stores()) {
|
|
37617
|
+
for (const intent of listPendingWholeStopIntents(context.manager)) {
|
|
37618
|
+
try {
|
|
37619
|
+
const response = this.applyWholeIntent(context.manager, intent);
|
|
37620
|
+
if (response.ok) {
|
|
37621
|
+
this.log(`[agentprism-daemon] run-control ${intent.operationId} for ${intent.runId}: ${response.outcome}`);
|
|
37622
|
+
}
|
|
37623
|
+
} catch (error51) {
|
|
37624
|
+
this.log(`[agentprism-daemon] run-control ${intent.operationId} for ${intent.runId} failed: ${String(error51)}`);
|
|
37625
|
+
}
|
|
37626
|
+
}
|
|
37627
|
+
}
|
|
37628
|
+
})().finally(() => {
|
|
37629
|
+
this.processingPending = void 0;
|
|
37630
|
+
});
|
|
37631
|
+
return this.processingPending;
|
|
37632
|
+
}
|
|
37633
|
+
async forceOwner(manager, runId, owner) {
|
|
37634
|
+
const lease = manager.getPersistence().inspectRunLease?.(runId);
|
|
37635
|
+
const instance = readDaemonInstance(owner.pid);
|
|
37636
|
+
if (!lease || lease.pid !== owner.pid || !instance || instance.pid !== owner.pid) {
|
|
37637
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Refusing force stop for run "${runId}": owner identity changed.`);
|
|
37638
|
+
}
|
|
37639
|
+
if (owner.pid === this.options.ownPid) {
|
|
37640
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Refusing force stop for run "${runId}": owner is this daemon.`);
|
|
37641
|
+
}
|
|
37642
|
+
if (lease.ownerId !== void 0 && instance.instanceId !== lease.ownerId) {
|
|
37643
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Refusing force stop for run "${runId}": owner instance identity does not match the lease.`);
|
|
37644
|
+
}
|
|
37645
|
+
const current = readDaemonInfo(instance.envFingerprint);
|
|
37646
|
+
if (!current || current.pid === owner.pid || !this.isPidAlive(current.pid)) {
|
|
37647
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Refusing force stop for run "${runId}": owner is not a superseded daemon with a live successor.`);
|
|
37648
|
+
}
|
|
37649
|
+
const health = await probeHealthz(instance.port, 1e3);
|
|
37650
|
+
if (lease.ownerId === void 0 && (health?.pid !== owner.pid || health.startedAt !== instance.startedAt)) {
|
|
37651
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Refusing force stop for run "${runId}": legacy owner identity could not be revalidated.`);
|
|
37652
|
+
}
|
|
37653
|
+
this.log(
|
|
37654
|
+
`[agentprism-daemon] force-stopping superseded owner pid ${owner.pid} for run ${runId}; ${health?.activeRuns ?? "unknown"} owned run(s) may be interrupted`
|
|
37655
|
+
);
|
|
37656
|
+
try {
|
|
37657
|
+
this.killProcess(owner.pid, "SIGTERM");
|
|
37658
|
+
} catch {
|
|
37659
|
+
}
|
|
37660
|
+
if (!await waitForPidExit(owner.pid, FORCE_TERM_WAIT_MS, this.isPidAlive)) {
|
|
37661
|
+
this.killProcess(owner.pid, "SIGKILL");
|
|
37662
|
+
if (!await waitForPidExit(owner.pid, FORCE_KILL_WAIT_MS, this.isPidAlive)) {
|
|
37663
|
+
throw new ProtocolError(ProtocolErrorCode.InternalError, `Forced owner pid ${owner.pid} did not exit.`);
|
|
37664
|
+
}
|
|
37665
|
+
}
|
|
37666
|
+
}
|
|
37667
|
+
async control(manager, input) {
|
|
37668
|
+
let owner = await this.resolveOwner(manager, input.runId);
|
|
37669
|
+
if (input.callIndex !== void 0) {
|
|
37670
|
+
if (!owner || !this.controlCapable(owner)) {
|
|
37671
|
+
throw new ProtocolError(
|
|
37672
|
+
ProtocolErrorCode.InvalidParams,
|
|
37673
|
+
`${actionableOwnerMessage(input.runId, owner, "agent cancellation")} Per-agent cancellation requires the live control-capable owner.`
|
|
37674
|
+
);
|
|
37675
|
+
}
|
|
37676
|
+
let response;
|
|
37677
|
+
try {
|
|
37678
|
+
response = await this.post(owner, {
|
|
37679
|
+
operationId: randomUUID3(),
|
|
37680
|
+
runId: input.runId,
|
|
37681
|
+
action: "cancel-agent",
|
|
37682
|
+
callIndex: input.callIndex
|
|
37683
|
+
});
|
|
37684
|
+
} catch (error51) {
|
|
37685
|
+
throw new ProtocolError(
|
|
37686
|
+
ProtocolErrorCode.InternalError,
|
|
37687
|
+
`${actionableOwnerMessage(input.runId, owner, "agent cancellation")} ${String(error51)}`
|
|
37688
|
+
);
|
|
37689
|
+
}
|
|
37690
|
+
if (!response.ok || response.outcome !== "agent-cancelled") {
|
|
37691
|
+
throw new ProtocolError(
|
|
37692
|
+
response.ok || response.code === "INTERNAL_ERROR" ? ProtocolErrorCode.InternalError : ProtocolErrorCode.InvalidParams,
|
|
37693
|
+
response.ok ? "Owner returned an invalid agent-cancellation response." : response.message
|
|
37694
|
+
);
|
|
37695
|
+
}
|
|
37696
|
+
return { kind: "agent", cancellation: response.cancellation };
|
|
37697
|
+
}
|
|
37698
|
+
const intent = createOrReuseWholeStopIntent(manager, input.runId, this.options.ownInstanceId);
|
|
37699
|
+
if (owner && this.controlCapable(owner)) {
|
|
37700
|
+
try {
|
|
37701
|
+
await this.post(owner, { operationId: intent.operationId, runId: input.runId, action: "stop" });
|
|
37702
|
+
} catch (error51) {
|
|
37703
|
+
this.log(`[agentprism-daemon] run-control ${intent.operationId} forwarding to pid ${owner.pid} failed: ${String(error51)}`);
|
|
37704
|
+
}
|
|
37705
|
+
const settled = this.settledWhole(manager, input.runId);
|
|
37706
|
+
if (settled) return { kind: "whole", state: "settled", ...settled };
|
|
37707
|
+
owner = await this.resolveOwner(manager, input.runId);
|
|
37708
|
+
}
|
|
37709
|
+
if (input.forceOwner && owner) {
|
|
37710
|
+
await this.forceOwner(manager, input.runId, owner);
|
|
37711
|
+
owner = await this.resolveOwner(manager, input.runId);
|
|
37712
|
+
}
|
|
37713
|
+
if (!owner) {
|
|
37714
|
+
const result = manager.stopPersistedRun(input.runId);
|
|
37715
|
+
if (result.outcome === "stopped") {
|
|
37716
|
+
requireDurableStoppedRun(manager, input.runId);
|
|
37717
|
+
acknowledgeWholeStopIntent(manager, intent, this.options.ownInstanceId, "stopped");
|
|
37718
|
+
} else if (result.outcome === "already-terminal") {
|
|
37719
|
+
acknowledgeWholeStopIntent(manager, intent, this.options.ownInstanceId, "already-terminal");
|
|
37720
|
+
} else if (result.outcome === "owned-elsewhere") {
|
|
37721
|
+
owner = await this.resolveOwner(manager, input.runId);
|
|
37722
|
+
}
|
|
37723
|
+
const settled = this.settledWhole(manager, input.runId);
|
|
37724
|
+
if (settled) return { kind: "whole", state: "settled", ...settled };
|
|
37725
|
+
}
|
|
37726
|
+
return {
|
|
37727
|
+
kind: "whole",
|
|
37728
|
+
state: "pending",
|
|
37729
|
+
operationId: intent.operationId,
|
|
37730
|
+
requestedAt: intent.requestedAt,
|
|
37731
|
+
owner: ownerView(owner)
|
|
37732
|
+
};
|
|
37733
|
+
}
|
|
37734
|
+
};
|
|
37735
|
+
|
|
37072
37736
|
// ../mcp-server/src/daemon/session-registry.ts
|
|
37073
37737
|
var SessionRegistry = class {
|
|
37074
37738
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -37206,6 +37870,7 @@ function writeJsonRpcError(res, status, message, code = -32e3) {
|
|
|
37206
37870
|
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code, message }, id: null }));
|
|
37207
37871
|
}
|
|
37208
37872
|
var MAX_MCP_BODY_BYTES = 4 * 1024 * 1024;
|
|
37873
|
+
var MAX_CONTROL_BODY_BYTES = 64 * 1024;
|
|
37209
37874
|
var BODY_REJECTED = /* @__PURE__ */ Symbol("body-rejected");
|
|
37210
37875
|
async function readMcpJsonBody(req, res) {
|
|
37211
37876
|
if (req.method !== "POST") return void 0;
|
|
@@ -37233,17 +37898,103 @@ async function readMcpJsonBody(req, res) {
|
|
|
37233
37898
|
return BODY_REJECTED;
|
|
37234
37899
|
}
|
|
37235
37900
|
}
|
|
37901
|
+
function controlHeader(req, name) {
|
|
37902
|
+
const value = req.headers[name];
|
|
37903
|
+
return Array.isArray(value) ? value[0] : value;
|
|
37904
|
+
}
|
|
37905
|
+
function writeControlResponse(res, status, body) {
|
|
37906
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
37907
|
+
res.end(JSON.stringify(body));
|
|
37908
|
+
}
|
|
37909
|
+
function isInternalRunControlRequest(value) {
|
|
37910
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
37911
|
+
const row = value;
|
|
37912
|
+
const keys = Object.keys(row).sort();
|
|
37913
|
+
if (typeof row.operationId !== "string" || !/^[0-9a-f-]{36}$/i.test(row.operationId) || typeof row.runId !== "string" || !/^[a-z0-9]+-[a-z0-9]+$/.test(row.runId)) return false;
|
|
37914
|
+
if (row.action === "stop") {
|
|
37915
|
+
return keys.join(",") === "action,operationId,runId";
|
|
37916
|
+
}
|
|
37917
|
+
return row.action === "cancel-agent" && Number.isSafeInteger(row.callIndex) && row.callIndex >= 0 && keys.join(",") === "action,callIndex,operationId,runId";
|
|
37918
|
+
}
|
|
37919
|
+
async function handleRunControlRequest(req, res, key, runControl) {
|
|
37920
|
+
if (req.method !== "POST") {
|
|
37921
|
+
writeControlResponse(res, 405, { ok: false, code: "INVALID_REQUEST", message: "Method Not Allowed" });
|
|
37922
|
+
return;
|
|
37923
|
+
}
|
|
37924
|
+
const contentType = controlHeader(req, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
37925
|
+
if (contentType !== "application/json") {
|
|
37926
|
+
writeControlResponse(res, 415, { ok: false, code: "INVALID_REQUEST", message: "Content-Type must be application/json" });
|
|
37927
|
+
return;
|
|
37928
|
+
}
|
|
37929
|
+
const chunks = [];
|
|
37930
|
+
let bytes = 0;
|
|
37931
|
+
for await (const chunk of req) {
|
|
37932
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
37933
|
+
bytes += buffer.byteLength;
|
|
37934
|
+
if (bytes > MAX_CONTROL_BODY_BYTES) {
|
|
37935
|
+
writeControlResponse(res, 413, { ok: false, code: "INVALID_REQUEST", message: "Run-control body exceeds 64 KiB" });
|
|
37936
|
+
return;
|
|
37937
|
+
}
|
|
37938
|
+
chunks.push(buffer);
|
|
37939
|
+
}
|
|
37940
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
37941
|
+
const operationId = controlHeader(req, "x-agentprism-control-operation");
|
|
37942
|
+
if (!verifyRunControlRequest(key, {
|
|
37943
|
+
method: req.method,
|
|
37944
|
+
path: RUN_CONTROL_PATH,
|
|
37945
|
+
body,
|
|
37946
|
+
timestamp: controlHeader(req, "x-agentprism-control-timestamp"),
|
|
37947
|
+
operationId,
|
|
37948
|
+
signature: controlHeader(req, "x-agentprism-control-signature")
|
|
37949
|
+
})) {
|
|
37950
|
+
writeControlResponse(res, 401, { ok: false, code: "UNAUTHORIZED", message: "Invalid run-control signature" });
|
|
37951
|
+
return;
|
|
37952
|
+
}
|
|
37953
|
+
let parsed;
|
|
37954
|
+
try {
|
|
37955
|
+
parsed = JSON.parse(body);
|
|
37956
|
+
} catch {
|
|
37957
|
+
writeControlResponse(res, 400, { ok: false, code: "INVALID_REQUEST", message: "Invalid JSON" });
|
|
37958
|
+
return;
|
|
37959
|
+
}
|
|
37960
|
+
if (!isInternalRunControlRequest(parsed) || parsed.operationId !== operationId) {
|
|
37961
|
+
writeControlResponse(res, 400, { ok: false, code: "INVALID_REQUEST", message: "Invalid run-control request" });
|
|
37962
|
+
return;
|
|
37963
|
+
}
|
|
37964
|
+
let response;
|
|
37965
|
+
try {
|
|
37966
|
+
response = await runControl.applyLocal(parsed);
|
|
37967
|
+
} catch (error51) {
|
|
37968
|
+
writeControlResponse(res, 500, {
|
|
37969
|
+
ok: false,
|
|
37970
|
+
code: "INTERNAL_ERROR",
|
|
37971
|
+
message: error51 instanceof Error ? error51.message : String(error51)
|
|
37972
|
+
});
|
|
37973
|
+
return;
|
|
37974
|
+
}
|
|
37975
|
+
const status = response.ok ? 200 : response.code === "UNKNOWN_RUN" ? 404 : response.code === "NOT_OWNER" ? 409 : response.code === "INTERNAL_ERROR" ? 500 : 400;
|
|
37976
|
+
writeControlResponse(res, status, response);
|
|
37977
|
+
}
|
|
37236
37978
|
async function createDaemon(options) {
|
|
37237
37979
|
const host = options.host ?? "127.0.0.1";
|
|
37238
37980
|
const env = options.env ?? process.env;
|
|
37239
37981
|
const log = options.log ?? ((line) => console.error(line));
|
|
37240
37982
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
37241
37983
|
const ownPid = options.ownPid ?? process.pid;
|
|
37984
|
+
const ownInstanceId = options.ownInstanceId ?? randomUUID4();
|
|
37242
37985
|
const version2 = options.version ?? SERVER_VERSION;
|
|
37243
37986
|
const isSuperseded = options.isSuperseded ?? (() => isSupersededBy(ownPid));
|
|
37244
37987
|
const familyFingerprint = envFingerprint(env);
|
|
37245
37988
|
const sessions = new SessionRegistry();
|
|
37246
|
-
const projects = new WorkflowProjectRegistry(options.runner);
|
|
37989
|
+
const projects = new WorkflowProjectRegistry(options.runner, { leaseOwnerId: ownInstanceId });
|
|
37990
|
+
const runControlKey = loadOrCreateRunControlKey();
|
|
37991
|
+
const runControl = new DaemonRunControl({
|
|
37992
|
+
projects,
|
|
37993
|
+
ownPid,
|
|
37994
|
+
ownInstanceId,
|
|
37995
|
+
key: runControlKey,
|
|
37996
|
+
log
|
|
37997
|
+
});
|
|
37247
37998
|
const replDrainBoundMs = options.replDrainBoundMs ?? options.sessionTtlMs ?? REPL_DRAIN_BOUND_MS;
|
|
37248
37999
|
const replPresence = new ReplPresenceLedger(replDrainBoundMs);
|
|
37249
38000
|
sessions.onConnectionOpened = (sessionId) => replPresence.reconnect(sessionId);
|
|
@@ -37264,7 +38015,7 @@ async function createDaemon(options) {
|
|
|
37264
38015
|
};
|
|
37265
38016
|
modernHandler = createMcpHandler(
|
|
37266
38017
|
() => {
|
|
37267
|
-
const clientId = `modern:${
|
|
38018
|
+
const clientId = `modern:${randomUUID4()}`;
|
|
37268
38019
|
return createWorkflowServer(options.runner, {
|
|
37269
38020
|
projects,
|
|
37270
38021
|
requireProjectDir: true,
|
|
@@ -37276,7 +38027,8 @@ async function createDaemon(options) {
|
|
|
37276
38027
|
protocolEra: "modern",
|
|
37277
38028
|
requestStateCodec,
|
|
37278
38029
|
disconnectReplClientOnClose: true,
|
|
37279
|
-
modernNotifier
|
|
38030
|
+
modernNotifier,
|
|
38031
|
+
runControl
|
|
37280
38032
|
});
|
|
37281
38033
|
},
|
|
37282
38034
|
{
|
|
@@ -37332,7 +38084,7 @@ async function createDaemon(options) {
|
|
|
37332
38084
|
return;
|
|
37333
38085
|
}
|
|
37334
38086
|
const transport = new NodeStreamableHTTPServerTransport({
|
|
37335
|
-
sessionIdGenerator: () =>
|
|
38087
|
+
sessionIdGenerator: () => randomUUID4(),
|
|
37336
38088
|
eventStore: new BoundedEventStore(),
|
|
37337
38089
|
onsessioninitialized: (sid) => {
|
|
37338
38090
|
sessions.add({
|
|
@@ -37354,7 +38106,8 @@ async function createDaemon(options) {
|
|
|
37354
38106
|
replPresence,
|
|
37355
38107
|
replClientId: () => transport.sessionId,
|
|
37356
38108
|
replDrainBoundMs,
|
|
37357
|
-
replEvalBreakChannel: options.evalBreakChannel
|
|
38109
|
+
replEvalBreakChannel: options.evalBreakChannel,
|
|
38110
|
+
runControl
|
|
37358
38111
|
});
|
|
37359
38112
|
await server.connect(transport);
|
|
37360
38113
|
const protocolOnClose = transport.onclose;
|
|
@@ -37381,6 +38134,10 @@ async function createDaemon(options) {
|
|
|
37381
38134
|
return;
|
|
37382
38135
|
}
|
|
37383
38136
|
const url2 = new URL(req.url ?? "/", `http://${host}:${boundPort}`);
|
|
38137
|
+
if (url2.pathname === RUN_CONTROL_PATH) {
|
|
38138
|
+
await handleRunControlRequest(req, res, runControlKey, runControl);
|
|
38139
|
+
return;
|
|
38140
|
+
}
|
|
37384
38141
|
if (url2.pathname === HEALTHZ_PATH && req.method === "GET") {
|
|
37385
38142
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
37386
38143
|
res.end(
|
|
@@ -37394,6 +38151,8 @@ async function createDaemon(options) {
|
|
|
37394
38151
|
activeRuns: projects.activeRunCount(),
|
|
37395
38152
|
envFingerprint: familyFingerprint,
|
|
37396
38153
|
projects: projects.snapshot(),
|
|
38154
|
+
instanceId: ownInstanceId,
|
|
38155
|
+
controlProtocol: RUN_CONTROL_PROTOCOL,
|
|
37397
38156
|
lameDuck: isSuperseded(),
|
|
37398
38157
|
inflightRequests: sessions.inflightCount() + modernInflight
|
|
37399
38158
|
})
|
|
@@ -37426,6 +38185,8 @@ async function createDaemon(options) {
|
|
|
37426
38185
|
port: boundPort,
|
|
37427
38186
|
url: `http://${host}:${boundPort}${MCP_ENDPOINT_PATH}`,
|
|
37428
38187
|
startedAt,
|
|
38188
|
+
instanceId: ownInstanceId,
|
|
38189
|
+
controlUrl: `http://${host}:${boundPort}${RUN_CONTROL_PATH}`,
|
|
37429
38190
|
sessions,
|
|
37430
38191
|
projects,
|
|
37431
38192
|
activeRunCount: () => projects.activeRunCount(),
|
|
@@ -37433,6 +38194,7 @@ async function createDaemon(options) {
|
|
|
37433
38194
|
inflightRequestCount: () => sessions.inflightCount() + modernInflight,
|
|
37434
38195
|
isSuperseded,
|
|
37435
38196
|
evictDrainableSessions: () => sessions.evictDrainable((sessionId) => replPresence.sessionHasBusyWorkspace(sessionId)),
|
|
38197
|
+
processPendingControlIntents: () => runControl.processPendingIntents(),
|
|
37436
38198
|
async close() {
|
|
37437
38199
|
const closed = new Promise((resolvePromise) => {
|
|
37438
38200
|
httpServer.close(() => resolvePromise());
|
|
@@ -37470,6 +38232,7 @@ async function runDaemon(options = {}) {
|
|
|
37470
38232
|
const log = (line) => console.error(line);
|
|
37471
38233
|
const runner = createAcpRunner();
|
|
37472
38234
|
const supersede = options.supersede ?? false;
|
|
38235
|
+
const instanceId = randomUUID5();
|
|
37473
38236
|
let daemon;
|
|
37474
38237
|
const sessionTtlMs = envInt(SESSION_IDLE_TTL_ENV, SESSION_IDLE_TTL_MS);
|
|
37475
38238
|
const replDrainBoundMs = envInt(REPL_DRAIN_BOUND_ENV, REPL_DRAIN_BOUND_MS);
|
|
@@ -37479,7 +38242,7 @@ async function runDaemon(options = {}) {
|
|
|
37479
38242
|
return `port ${port} is still held by ${holder.legacy ? "a legacy " : ""}daemon pid ${holder.info.pid} (v${holder.info.version}, started ${holder.info.startedAt})`;
|
|
37480
38243
|
};
|
|
37481
38244
|
const evalBreakChannel = createEvalBreakChannel2();
|
|
37482
|
-
const daemonOptions = { runner, log, replDrainBoundMs, evalBreakChannel };
|
|
38245
|
+
const daemonOptions = { runner, log, replDrainBoundMs, evalBreakChannel, ownInstanceId: instanceId };
|
|
37483
38246
|
if (supersede) {
|
|
37484
38247
|
let port = options.port ?? 0;
|
|
37485
38248
|
try {
|
|
@@ -37513,6 +38276,9 @@ async function runDaemon(options = {}) {
|
|
|
37513
38276
|
url: daemon.url,
|
|
37514
38277
|
startedAt: daemon.startedAt,
|
|
37515
38278
|
envFingerprint: envFingerprint(),
|
|
38279
|
+
instanceId: daemon.instanceId,
|
|
38280
|
+
controlUrl: daemon.controlUrl,
|
|
38281
|
+
controlProtocol: 1,
|
|
37516
38282
|
...await evalBreakChannel.breakUrl().then((url2) => ({ replBreakUrl: url2 })).catch(() => ({}))
|
|
37517
38283
|
});
|
|
37518
38284
|
installDaemonLifecycle({
|
|
@@ -37575,7 +38341,9 @@ function formatInstance(instance, currentPid) {
|
|
|
37575
38341
|
`${DAEMON_NAME} v${health?.version ?? info.version}`,
|
|
37576
38342
|
` pid: ${info.pid}`,
|
|
37577
38343
|
` url: ${info.url}`,
|
|
37578
|
-
` role: ${role}
|
|
38344
|
+
` role: ${role}`,
|
|
38345
|
+
` instance: ${health?.instanceId ?? info.instanceId ?? "legacy/unknown"}`,
|
|
38346
|
+
` run control: ${health?.controlProtocol === 1 || info.controlProtocol === 1 ? "v1" : "unavailable"}`
|
|
37579
38347
|
];
|
|
37580
38348
|
if (health === void 0) {
|
|
37581
38349
|
lines.push(" health: not responding (pid alive, /healthz unreachable)");
|
|
@@ -37673,7 +38441,7 @@ async function runDaemonCommand(args, options) {
|
|
|
37673
38441
|
const count = Number.isFinite(lines) && lines > 0 ? lines : 100;
|
|
37674
38442
|
let content;
|
|
37675
38443
|
try {
|
|
37676
|
-
content =
|
|
38444
|
+
content = readFileSync7(daemonLogPath(), "utf-8");
|
|
37677
38445
|
} catch {
|
|
37678
38446
|
console.log(`no daemon log at ${daemonLogPath()}`);
|
|
37679
38447
|
return 0;
|
|
@@ -44640,24 +45408,24 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin2(((exports) => {
|
|
|
44640
45408
|
}
|
|
44641
45409
|
}
|
|
44642
45410
|
exports.compileSchema = compileSchema;
|
|
44643
|
-
function resolveRef2(
|
|
45411
|
+
function resolveRef2(root2, baseId, ref) {
|
|
44644
45412
|
var _a3;
|
|
44645
45413
|
ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
|
|
44646
|
-
const schOrFunc =
|
|
45414
|
+
const schOrFunc = root2.refs[ref];
|
|
44647
45415
|
if (schOrFunc) return schOrFunc;
|
|
44648
|
-
let _sch = resolve.call(this,
|
|
45416
|
+
let _sch = resolve.call(this, root2, ref);
|
|
44649
45417
|
if (_sch === void 0) {
|
|
44650
|
-
const schema = (_a3 =
|
|
45418
|
+
const schema = (_a3 = root2.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
|
|
44651
45419
|
const { schemaId } = this.opts;
|
|
44652
45420
|
if (schema) _sch = new SchemaEnv({
|
|
44653
45421
|
schema,
|
|
44654
45422
|
schemaId,
|
|
44655
|
-
root,
|
|
45423
|
+
root: root2,
|
|
44656
45424
|
baseId
|
|
44657
45425
|
});
|
|
44658
45426
|
}
|
|
44659
45427
|
if (_sch === void 0) return;
|
|
44660
|
-
return
|
|
45428
|
+
return root2.refs[ref] = inlineOrCompile.call(this, _sch);
|
|
44661
45429
|
}
|
|
44662
45430
|
exports.resolveRef = resolveRef2;
|
|
44663
45431
|
function inlineOrCompile(sch) {
|
|
@@ -44671,20 +45439,20 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin2(((exports) => {
|
|
|
44671
45439
|
function sameSchemaEnv(s1, s2) {
|
|
44672
45440
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
44673
45441
|
}
|
|
44674
|
-
function resolve(
|
|
45442
|
+
function resolve(root2, ref) {
|
|
44675
45443
|
let sch;
|
|
44676
45444
|
while (typeof (sch = this.refs[ref]) == "string") ref = sch;
|
|
44677
|
-
return sch || this.schemas[ref] || resolveSchema.call(this,
|
|
45445
|
+
return sch || this.schemas[ref] || resolveSchema.call(this, root2, ref);
|
|
44678
45446
|
}
|
|
44679
|
-
function resolveSchema(
|
|
45447
|
+
function resolveSchema(root2, ref) {
|
|
44680
45448
|
const p = this.opts.uriResolver.parse(ref);
|
|
44681
45449
|
const refPath = (0, resolve_1._getFullPath)(this.opts.uriResolver, p);
|
|
44682
|
-
let baseId = (0, resolve_1.getFullPath)(this.opts.uriResolver,
|
|
44683
|
-
if (Object.keys(
|
|
45450
|
+
let baseId = (0, resolve_1.getFullPath)(this.opts.uriResolver, root2.baseId, void 0);
|
|
45451
|
+
if (Object.keys(root2.schema).length > 0 && refPath === baseId) return getJsonPointer.call(this, p, root2);
|
|
44684
45452
|
const id = (0, resolve_1.normalizeId)(refPath);
|
|
44685
45453
|
const schOrRef = this.refs[id] || this.schemas[id];
|
|
44686
45454
|
if (typeof schOrRef == "string") {
|
|
44687
|
-
const sch = resolveSchema.call(this,
|
|
45455
|
+
const sch = resolveSchema.call(this, root2, schOrRef);
|
|
44688
45456
|
if (typeof (sch === null || sch === void 0 ? void 0 : sch.schema) !== "object") return;
|
|
44689
45457
|
return getJsonPointer.call(this, p, sch);
|
|
44690
45458
|
}
|
|
@@ -44698,7 +45466,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin2(((exports) => {
|
|
|
44698
45466
|
return new SchemaEnv({
|
|
44699
45467
|
schema,
|
|
44700
45468
|
schemaId,
|
|
44701
|
-
root,
|
|
45469
|
+
root: root2,
|
|
44702
45470
|
baseId
|
|
44703
45471
|
});
|
|
44704
45472
|
}
|
|
@@ -44712,7 +45480,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin2(((exports) => {
|
|
|
44712
45480
|
"dependencies",
|
|
44713
45481
|
"definitions"
|
|
44714
45482
|
]);
|
|
44715
|
-
function getJsonPointer(parsedRef, { baseId, schema, root }) {
|
|
45483
|
+
function getJsonPointer(parsedRef, { baseId, schema, root: root2 }) {
|
|
44716
45484
|
var _a3;
|
|
44717
45485
|
if (((_a3 = parsedRef.fragment) === null || _a3 === void 0 ? void 0 : _a3[0]) !== "/") return;
|
|
44718
45486
|
for (const part of parsedRef.fragment.slice(1).split("/")) {
|
|
@@ -44726,13 +45494,13 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin2(((exports) => {
|
|
|
44726
45494
|
let env;
|
|
44727
45495
|
if (typeof schema != "boolean" && schema.$ref && !(0, util_1.schemaHasRulesButRef)(schema, this.RULES)) {
|
|
44728
45496
|
const $ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, schema.$ref);
|
|
44729
|
-
env = resolveSchema.call(this,
|
|
45497
|
+
env = resolveSchema.call(this, root2, $ref);
|
|
44730
45498
|
}
|
|
44731
45499
|
const { schemaId } = this.opts;
|
|
44732
45500
|
env = env || new SchemaEnv({
|
|
44733
45501
|
schema,
|
|
44734
45502
|
schemaId,
|
|
44735
|
-
root,
|
|
45503
|
+
root: root2,
|
|
44736
45504
|
baseId
|
|
44737
45505
|
});
|
|
44738
45506
|
if (env.schema !== env.root.schema) return env;
|
|
@@ -45597,11 +46365,11 @@ var require_core$32 = /* @__PURE__ */ __commonJSMin2(((exports) => {
|
|
|
45597
46365
|
while (typeof (sch = getSchEnv.call(this, keyRef)) == "string") keyRef = sch;
|
|
45598
46366
|
if (sch === void 0) {
|
|
45599
46367
|
const { schemaId } = this.opts;
|
|
45600
|
-
const
|
|
46368
|
+
const root2 = new compile_1.SchemaEnv({
|
|
45601
46369
|
schema: {},
|
|
45602
46370
|
schemaId
|
|
45603
46371
|
});
|
|
45604
|
-
sch = compile_1.resolveSchema.call(this,
|
|
46372
|
+
sch = compile_1.resolveSchema.call(this, root2, keyRef);
|
|
45605
46373
|
if (!sch) return;
|
|
45606
46374
|
this.refs[keyRef] = sch;
|
|
45607
46375
|
}
|
|
@@ -45906,16 +46674,16 @@ var require_ref2 = /* @__PURE__ */ __commonJSMin2(((exports) => {
|
|
|
45906
46674
|
code(cxt) {
|
|
45907
46675
|
const { gen, schema: $ref, it } = cxt;
|
|
45908
46676
|
const { baseId, schemaEnv: env, validateName, opts, self } = it;
|
|
45909
|
-
const { root } = env;
|
|
45910
|
-
if (($ref === "#" || $ref === "#/") && baseId ===
|
|
45911
|
-
const schOrEnv = compile_1.resolveRef.call(self,
|
|
46677
|
+
const { root: root2 } = env;
|
|
46678
|
+
if (($ref === "#" || $ref === "#/") && baseId === root2.baseId) return callRootRef();
|
|
46679
|
+
const schOrEnv = compile_1.resolveRef.call(self, root2, baseId, $ref);
|
|
45912
46680
|
if (schOrEnv === void 0) throw new ref_error_1.default(it.opts.uriResolver, baseId, $ref);
|
|
45913
46681
|
if (schOrEnv instanceof compile_1.SchemaEnv) return callValidate(schOrEnv);
|
|
45914
46682
|
return inlineRefSchema(schOrEnv);
|
|
45915
46683
|
function callRootRef() {
|
|
45916
|
-
if (env ===
|
|
45917
|
-
const rootName = gen.scopeValue("root", { ref:
|
|
45918
|
-
return callRef(cxt, (0, codegen_1._)`${rootName}.validate`,
|
|
46684
|
+
if (env === root2) return callRef(cxt, validateName, env, env.$async);
|
|
46685
|
+
const rootName = gen.scopeValue("root", { ref: root2 });
|
|
46686
|
+
return callRef(cxt, (0, codegen_1._)`${rootName}.validate`, root2, root2.$async);
|
|
45919
46687
|
}
|
|
45920
46688
|
function callValidate(sch) {
|
|
45921
46689
|
callRef(cxt, getValidate(cxt, sch), sch, sch.$async);
|
|
@@ -47630,12 +48398,12 @@ var require_dynamicAnchor2 = /* @__PURE__ */ __commonJSMin2(((exports) => {
|
|
|
47630
48398
|
exports.dynamicAnchor = dynamicAnchor;
|
|
47631
48399
|
function _getValidate(cxt) {
|
|
47632
48400
|
const { schemaEnv, schema, self } = cxt.it;
|
|
47633
|
-
const { root, baseId, localRefs, meta: meta3 } = schemaEnv.root;
|
|
48401
|
+
const { root: root2, baseId, localRefs, meta: meta3 } = schemaEnv.root;
|
|
47634
48402
|
const { schemaId } = self.opts;
|
|
47635
48403
|
const sch = new compile_1.SchemaEnv({
|
|
47636
48404
|
schema,
|
|
47637
48405
|
schemaId,
|
|
47638
|
-
root,
|
|
48406
|
+
root: root2,
|
|
47639
48407
|
baseId,
|
|
47640
48408
|
localRefs,
|
|
47641
48409
|
meta: meta3
|
|
@@ -49032,8 +49800,8 @@ async function random(size) {
|
|
|
49032
49800
|
const evenDistCutoff = Math.pow(2, 8) - Math.pow(2, 8) % mask.length;
|
|
49033
49801
|
let result = "";
|
|
49034
49802
|
while (result.length < size) {
|
|
49035
|
-
const
|
|
49036
|
-
for (const randomByte of
|
|
49803
|
+
const randomBytes4 = await getRandomValues(size - result.length);
|
|
49804
|
+
for (const randomByte of randomBytes4) {
|
|
49037
49805
|
if (randomByte < evenDistCutoff) {
|
|
49038
49806
|
result += mask[randomByte % mask.length];
|
|
49039
49807
|
}
|
|
@@ -51157,6 +51925,7 @@ async function runShim(options) {
|
|
|
51157
51925
|
const info = await ensureDaemonRunning({ bundlePath: options.bundlePath, port: options.port, log });
|
|
51158
51926
|
let replBreakUrl = info.replBreakUrl;
|
|
51159
51927
|
let exiting = false;
|
|
51928
|
+
let compatibilityDrainTimer;
|
|
51160
51929
|
function fireOutOfBandBreak(projectDir) {
|
|
51161
51930
|
if (typeof projectDir !== "string" || replBreakUrl === void 0) return;
|
|
51162
51931
|
let key;
|
|
@@ -51277,6 +52046,27 @@ async function runShim(options) {
|
|
|
51277
52046
|
log(`[${DAEMON_NAME} shim] fatal: ${reason}`);
|
|
51278
52047
|
void shutdown(1);
|
|
51279
52048
|
}
|
|
52049
|
+
function armCompatibilityDrain(candidate) {
|
|
52050
|
+
if (compatibilityDrainTimer !== void 0) {
|
|
52051
|
+
clearInterval(compatibilityDrainTimer);
|
|
52052
|
+
compatibilityDrainTimer = void 0;
|
|
52053
|
+
}
|
|
52054
|
+
if (candidate.compatibilityDrain !== true) return;
|
|
52055
|
+
compatibilityDrainTimer = setInterval(() => {
|
|
52056
|
+
void (async () => {
|
|
52057
|
+
if (exiting || reinitializing) return;
|
|
52058
|
+
const health = await probeHealthz(candidate.port, 1e3);
|
|
52059
|
+
if (health === void 0 || health.pid !== candidate.pid || health.activeRuns === 0 && (health.inflightRequests ?? 0) === 0) {
|
|
52060
|
+
if (compatibilityDrainTimer !== void 0) clearInterval(compatibilityDrainTimer);
|
|
52061
|
+
compatibilityDrainTimer = void 0;
|
|
52062
|
+
await startReinitialize(
|
|
52063
|
+
health === void 0 ? "compatibility-drain predecessor became unavailable" : "compatibility-drain predecessor became idle"
|
|
52064
|
+
);
|
|
52065
|
+
}
|
|
52066
|
+
})().catch((error51) => log(`[${DAEMON_NAME} shim] compatibility-drain monitor failed: ${String(error51)}`));
|
|
52067
|
+
}, 1e3);
|
|
52068
|
+
compatibilityDrainTimer.unref();
|
|
52069
|
+
}
|
|
51280
52070
|
async function startReinitialize(reason) {
|
|
51281
52071
|
if (reinitializing || exiting) return;
|
|
51282
52072
|
if (!recoveryAllowed()) {
|
|
@@ -51292,6 +52082,7 @@ async function runShim(options) {
|
|
|
51292
52082
|
retireTransport(http2, reason);
|
|
51293
52083
|
const fresh = await ensureDaemonRunning({ bundlePath: options.bundlePath, port: options.port, log });
|
|
51294
52084
|
replBreakUrl = fresh.replBreakUrl;
|
|
52085
|
+
armCompatibilityDrain(fresh);
|
|
51295
52086
|
http2 = makeHttpTransport(fresh.url);
|
|
51296
52087
|
await http2.start();
|
|
51297
52088
|
if (cachedInitialize === void 0) {
|
|
@@ -51383,6 +52174,8 @@ async function runShim(options) {
|
|
|
51383
52174
|
async function shutdown(code) {
|
|
51384
52175
|
if (exiting) return;
|
|
51385
52176
|
exiting = true;
|
|
52177
|
+
if (compatibilityDrainTimer !== void 0) clearInterval(compatibilityDrainTimer);
|
|
52178
|
+
compatibilityDrainTimer = void 0;
|
|
51386
52179
|
await http2.terminateSession().catch(() => void 0);
|
|
51387
52180
|
await http2.close().catch(() => void 0);
|
|
51388
52181
|
await stdio.close().catch(() => void 0);
|
|
@@ -51391,6 +52184,7 @@ async function runShim(options) {
|
|
|
51391
52184
|
stdio.onclose = () => void shutdown(0);
|
|
51392
52185
|
process.once("SIGINT", () => void shutdown(0));
|
|
51393
52186
|
process.once("SIGTERM", () => void shutdown(0));
|
|
52187
|
+
armCompatibilityDrain(info);
|
|
51394
52188
|
await http2.start();
|
|
51395
52189
|
await stdio.start();
|
|
51396
52190
|
}
|