@agimon-ai/doompi-workflow 0.0.1-alpha.97 → 0.0.1-alpha.99
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 +7 -0
- package/dist/apiContracts.cjs +3 -0
- package/dist/apiContracts.cjs.map +1 -1
- package/dist/apiContracts.d.mts.map +1 -1
- package/dist/apiContracts.mjs +3 -0
- package/dist/apiContracts.mjs.map +1 -1
- package/dist/extensions/pi.cjs +102 -644
- package/dist/extensions/pi.cjs.map +1 -1
- package/dist/extensions/pi.d.mts.map +1 -1
- package/dist/extensions/pi.mjs +49 -591
- package/dist/extensions/pi.mjs.map +1 -1
- package/dist/extensions/server.cjs +1718 -608
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.d.mts.map +1 -1
- package/dist/extensions/server.mjs +1720 -610
- package/dist/extensions/server.mjs.map +1 -1
- package/dist/extensions/web.mjs +644 -218
- package/dist/extensions/web.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.mts +10 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{workflowLaunchCommand-Bhr-PvdD.mjs → workflowLaunchCommand-BLrxmzdd.mjs} +14 -7
- package/dist/workflowLaunchCommand-BLrxmzdd.mjs.map +1 -0
- package/dist/{workflowLaunchCommand-7y8xg5YF.cjs → workflowLaunchCommand-DJyDB96d.cjs} +14 -7
- package/dist/workflowLaunchCommand-DJyDB96d.cjs.map +1 -0
- package/dist/workflowTerminal-Bb4bLtLu.cjs +962 -0
- package/dist/workflowTerminal-Bb4bLtLu.cjs.map +1 -0
- package/dist/workflowTerminal-QzufjHCa.mjs +730 -0
- package/dist/workflowTerminal-QzufjHCa.mjs.map +1 -0
- package/package.json +9 -9
- package/src/prompts/doompi-author-workflow/SKILL.md +26 -0
- package/src/prompts/doompi-use-workflow/SKILL.md +1 -1
- package/src/types/webWorkflowTerminal.ts +16 -0
- package/src/types/webWorkflows.ts +20 -0
- package/dist/workflowLaunchCommand-7y8xg5YF.cjs.map +0 -1
- package/dist/workflowLaunchCommand-Bhr-PvdD.mjs.map +0 -1
- package/dist/workflowTerminal-Bj4KKpoA.mjs +0 -148
- package/dist/workflowTerminal-Bj4KKpoA.mjs.map +0 -1
- package/dist/workflowTerminal-WM_cpxIO.cjs +0 -200
- package/dist/workflowTerminal-WM_cpxIO.cjs.map +0 -1
|
@@ -2,9 +2,10 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_workflowTerminal = require("../workflowTerminal-
|
|
6
|
-
const require_workflowLaunchCommand = require("../workflowLaunchCommand-
|
|
5
|
+
const require_workflowTerminal = require("../workflowTerminal-Bb4bLtLu.cjs");
|
|
6
|
+
const require_workflowLaunchCommand = require("../workflowLaunchCommand-DJyDB96d.cjs");
|
|
7
7
|
let _agimon_ai_doompi_core_extensionFile = require("@agimon-ai/doompi-core/extensionFile");
|
|
8
|
+
let _agimon_ai_doompi_core_childProcess = require("@agimon-ai/doompi-core/childProcess");
|
|
8
9
|
let node_fs = require("node:fs");
|
|
9
10
|
node_fs = require_workflowTerminal.__toESM(node_fs, 1);
|
|
10
11
|
let node_path = require("node:path");
|
|
@@ -13,82 +14,15 @@ let _agimon_ai_workflow_mcp = require("@agimon-ai/workflow-mcp");
|
|
|
13
14
|
let zod = require("zod");
|
|
14
15
|
let node_crypto = require("node:crypto");
|
|
15
16
|
let _agimon_ai_doompi_minor_mode = require("@agimon-ai/doompi-minor-mode");
|
|
17
|
+
let _agimon_ai_doompi_telemetry = require("@agimon-ai/doompi-telemetry");
|
|
16
18
|
let _agimon_ai_doompi_core_serverFacet = require("@agimon-ai/doompi-core/serverFacet");
|
|
17
|
-
let
|
|
18
|
-
node_os = require_workflowTerminal.__toESM(node_os, 1);
|
|
19
|
+
let _agimon_ai_doompi_core_hubChannel = require("@agimon-ai/doompi-core/hubChannel");
|
|
19
20
|
let node_stream = require("node:stream");
|
|
20
21
|
let hono = require("hono");
|
|
21
22
|
let hono_streaming = require("hono/streaming");
|
|
22
23
|
let _agimon_ai_doompi_core_web = require("@agimon-ai/doompi-core/web");
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return left !== void 0 && right !== void 0 && left.size === right.size && left.modifiedAt === right.modifiedAt;
|
|
26
|
-
}
|
|
27
|
-
function createWorkflowCatalogReader(deps) {
|
|
28
|
-
const cache = /* @__PURE__ */ new Map();
|
|
29
|
-
const detailFor = (path) => {
|
|
30
|
-
const stamp = deps.stamp(path);
|
|
31
|
-
const cached = cache.get(path);
|
|
32
|
-
if (cached && unchanged(cached.stamp, stamp)) return cached.detail;
|
|
33
|
-
const detail = deps.summarize(path);
|
|
34
|
-
if (stamp !== void 0) cache.set(path, {
|
|
35
|
-
stamp,
|
|
36
|
-
detail
|
|
37
|
-
});
|
|
38
|
-
return detail;
|
|
39
|
-
};
|
|
40
|
-
return {
|
|
41
|
-
async read(directory) {
|
|
42
|
-
return (await deps.list(directory)).map((entry) => {
|
|
43
|
-
const detail = detailFor(entry.path);
|
|
44
|
-
return {
|
|
45
|
-
path: entry.path,
|
|
46
|
-
relativePath: entry.relativePath,
|
|
47
|
-
name: entry.name === "" ? entry.relativePath : entry.name,
|
|
48
|
-
description: entry.description,
|
|
49
|
-
tags: entry.tags,
|
|
50
|
-
triggers: detail.triggers,
|
|
51
|
-
inputs: detail.inputs,
|
|
52
|
-
jobs: detail.jobs,
|
|
53
|
-
artifacts: detail.artifacts,
|
|
54
|
-
...detail.runners === void 0 ? {} : { runners: detail.runners },
|
|
55
|
-
...detail.error === void 0 ? {} : { error: detail.error }
|
|
56
|
-
};
|
|
57
|
-
});
|
|
58
|
-
},
|
|
59
|
-
forget(keep) {
|
|
60
|
-
for (const path of cache.keys()) if (!keep.has(path)) cache.delete(path);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
/** The launchable rows first, then the ones that would not parse, name ordered. */
|
|
65
|
-
function presentWorkflowCatalog(entries) {
|
|
66
|
-
return [...entries].sort((left, right) => {
|
|
67
|
-
if (left.error === void 0 !== (right.error === void 0)) return left.error === void 0 ? -1 : 1;
|
|
68
|
-
return left.name.localeCompare(right.name);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
//#endregion
|
|
72
|
-
//#region src/services/workflowCatalogDeps/index.ts
|
|
73
|
-
/** Connect the pure catalog projection service to the workflow engine and filesystem. */
|
|
74
|
-
function defaultCatalogDeps() {
|
|
75
|
-
return {
|
|
76
|
-
list: (directory) => (0, _agimon_ai_workflow_mcp.readWorkflowCatalog)(directory),
|
|
77
|
-
summarize: (workflowPath) => (0, _agimon_ai_workflow_mcp.summarizeWorkflow)(workflowPath),
|
|
78
|
-
stamp: (workflowPath) => {
|
|
79
|
-
try {
|
|
80
|
-
const stats = (0, node_fs.statSync)(workflowPath);
|
|
81
|
-
return {
|
|
82
|
-
size: stats.size,
|
|
83
|
-
modifiedAt: stats.mtimeMs
|
|
84
|
-
};
|
|
85
|
-
} catch {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
//#endregion
|
|
24
|
+
let node_os = require("node:os");
|
|
25
|
+
node_os = require_workflowTerminal.__toESM(node_os, 1);
|
|
92
26
|
//#region src/services/workflowRuns/index.ts
|
|
93
27
|
/**
|
|
94
28
|
* MIRRORS @agimon-ai/workflow-mcp's on-disk registry
|
|
@@ -164,8 +98,12 @@ function asOptionalString(value) {
|
|
|
164
98
|
function asOptionalNumber(value) {
|
|
165
99
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
166
100
|
}
|
|
101
|
+
/** Text without its terminal escape codes, as a log file or a reader wants it. */
|
|
102
|
+
function withoutAnsi(text) {
|
|
103
|
+
return text.replace(ANSI_ESCAPE_PATTERN, "");
|
|
104
|
+
}
|
|
167
105
|
function stripAnsi(text) {
|
|
168
|
-
return text
|
|
106
|
+
return withoutAnsi(text).trim();
|
|
169
107
|
}
|
|
170
108
|
/**
|
|
171
109
|
* Validates one run.json into the wire shape plus scoping facts.
|
|
@@ -218,6 +156,18 @@ function parseWorkflowRunRecord(raw) {
|
|
|
218
156
|
...piSessionId === void 0 ? {} : { piSessionId }
|
|
219
157
|
};
|
|
220
158
|
}
|
|
159
|
+
function parseStepRef(value) {
|
|
160
|
+
if (!isRecord(value)) return void 0;
|
|
161
|
+
const kind = asOptionalString(value.kind);
|
|
162
|
+
const id = asOptionalString(value.id);
|
|
163
|
+
if (!kind || !id) return void 0;
|
|
164
|
+
const label = asOptionalString(value.label);
|
|
165
|
+
return {
|
|
166
|
+
kind,
|
|
167
|
+
id,
|
|
168
|
+
...label === void 0 ? {} : { label }
|
|
169
|
+
};
|
|
170
|
+
}
|
|
221
171
|
/**
|
|
222
172
|
* Parses the append-only progress log. Malformed lines are skipped rather
|
|
223
173
|
* than failing the file: a reader routinely catches a torn final line
|
|
@@ -245,6 +195,8 @@ function parseWorkflowProgress(raw) {
|
|
|
245
195
|
const index = asOptionalNumber(parsed.index);
|
|
246
196
|
const total = asOptionalNumber(parsed.total);
|
|
247
197
|
const reason = asOptionalString(parsed.reason);
|
|
198
|
+
const ref = parseStepRef(parsed.ref);
|
|
199
|
+
const group = asOptionalString(parsed.group);
|
|
248
200
|
events.push({
|
|
249
201
|
type,
|
|
250
202
|
status,
|
|
@@ -253,6 +205,8 @@ function parseWorkflowProgress(raw) {
|
|
|
253
205
|
...index === void 0 ? {} : { index },
|
|
254
206
|
...total === void 0 ? {} : { total },
|
|
255
207
|
...reason === void 0 ? {} : { reason },
|
|
208
|
+
...ref === void 0 ? {} : { ref },
|
|
209
|
+
...group === void 0 ? {} : { group },
|
|
256
210
|
at
|
|
257
211
|
});
|
|
258
212
|
}
|
|
@@ -300,9 +254,16 @@ function foldWorkflowProgress(events) {
|
|
|
300
254
|
status: event.status
|
|
301
255
|
};
|
|
302
256
|
job.steps.push(step);
|
|
257
|
+
} else if (event.status === "running" && step.status !== "running") {
|
|
258
|
+
delete step.ref;
|
|
259
|
+
delete step.reason;
|
|
260
|
+
delete step.endedAt;
|
|
261
|
+
delete step.startedAt;
|
|
303
262
|
}
|
|
304
263
|
step.status = event.status;
|
|
305
264
|
if (event.reason !== void 0) step.reason = event.reason;
|
|
265
|
+
if (event.ref !== void 0) step.ref = event.ref;
|
|
266
|
+
if (event.group !== void 0) step.group = event.group;
|
|
306
267
|
if (event.status === "running" && step.startedAt === void 0) step.startedAt = event.at;
|
|
307
268
|
if (STEP_TERMINAL_STATES.has(event.status)) step.endedAt = event.at;
|
|
308
269
|
}
|
|
@@ -403,490 +364,850 @@ function presentWorkflowRuns(runs, now) {
|
|
|
403
364
|
return [...groups.entries()].sort(([left], [right]) => STAGE_ORDER[left] - STAGE_ORDER[right]).flatMap(([, group]) => group.sort((left, right) => lastMovedAt(right) - lastMovedAt(left)).slice(0, 24));
|
|
404
365
|
}
|
|
405
366
|
//#endregion
|
|
406
|
-
//#region src/services/
|
|
367
|
+
//#region src/services/serverLaunch/index.ts
|
|
368
|
+
/** Beside the run's own files, so the run's artifacts list and its deletion both include it. */
|
|
369
|
+
const RUN_LOG_FILENAME = "engine.log";
|
|
370
|
+
/** Exit codes a run ends with when nothing went wrong: finished, or skipped by its triggers. */
|
|
371
|
+
const SUCCESS_EXIT_CODES = /* @__PURE__ */ new Set([0, 2]);
|
|
372
|
+
/** Lines kept before a run has a directory to write them to. */
|
|
373
|
+
const PENDING_LINE_LIMIT = 500;
|
|
374
|
+
/** How long closing a session waits for its in-process runs to stop. */
|
|
375
|
+
const STOP_TIMEOUT_MS = 1e4;
|
|
376
|
+
const SESSION_CLOSED_REASON = "The session that runs this workflow closed.";
|
|
377
|
+
const RUNNING_STAGE = "running";
|
|
378
|
+
const LAUNCH_REQUESTED_EVENT = "doom_workflow.launch_requested";
|
|
379
|
+
const RUN_STARTED_EVENT = "doom_workflow.run_started";
|
|
380
|
+
const RUN_FINISHED_EVENT = "doom_workflow.run_finished";
|
|
381
|
+
const RUN_FAILED_EVENT = "doom_workflow.run_failed";
|
|
382
|
+
const ENGINE_ERROR_EVENT = "doom_workflow.engine_error";
|
|
383
|
+
const ENGINE_WARNING_EVENT = "doom_workflow.engine_warning";
|
|
384
|
+
const RUN_LOG_FAILED_EVENT = "doom_workflow.run_log_failed";
|
|
385
|
+
const RUN_STOP_FAILED_EVENT = "doom_workflow.run_stop_failed";
|
|
386
|
+
function delay$1(ms) {
|
|
387
|
+
return new Promise((settle) => {
|
|
388
|
+
setTimeout(settle, ms).unref?.();
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
function textResult$1(text, isError = false) {
|
|
392
|
+
return {
|
|
393
|
+
content: [{
|
|
394
|
+
type: "text",
|
|
395
|
+
text
|
|
396
|
+
}],
|
|
397
|
+
...isError ? { isError: true } : {}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
407
400
|
/**
|
|
408
|
-
*
|
|
401
|
+
* The engine's log for one run, where a reader can find it.
|
|
409
402
|
*
|
|
410
|
-
* The
|
|
411
|
-
*
|
|
403
|
+
* The engine writes why a step failed as a log line. Without a logger, those
|
|
404
|
+
* lines exist only in the output the run returns at its end, so a run that is
|
|
405
|
+
* still going, or one read later, has nothing to say about its failures. Here
|
|
406
|
+
* every line goes to the run directory's `engine.log`, and errors and warnings
|
|
407
|
+
* also go to telemetry, with the line as the exception so the text survives.
|
|
412
408
|
*/
|
|
413
|
-
function
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
let stat;
|
|
424
|
-
try {
|
|
425
|
-
stat = node_fs.default.statSync(filePath);
|
|
426
|
-
} catch {
|
|
427
|
-
cache.delete(filePath);
|
|
428
|
-
return empty;
|
|
429
|
-
}
|
|
430
|
-
const cached = cache.get(filePath);
|
|
431
|
-
if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) return cached.value;
|
|
432
|
-
let raw;
|
|
433
|
-
try {
|
|
434
|
-
raw = node_fs.default.readFileSync(filePath, "utf8");
|
|
435
|
-
} catch {
|
|
436
|
-
cache.delete(filePath);
|
|
437
|
-
return empty;
|
|
409
|
+
function createRunLog(telemetry) {
|
|
410
|
+
const pending = [];
|
|
411
|
+
let file;
|
|
412
|
+
let writing = Promise.resolve();
|
|
413
|
+
const reportLogFailure = (error) => telemetry?.recordWarning(RUN_LOG_FAILED_EVENT, error) ?? Promise.resolve();
|
|
414
|
+
const append = (line) => {
|
|
415
|
+
if (file === void 0) {
|
|
416
|
+
pending.push(line);
|
|
417
|
+
if (pending.length > PENDING_LINE_LIMIT) pending.shift();
|
|
418
|
+
return;
|
|
438
419
|
}
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
size: stat.size,
|
|
442
|
-
mtimeMs: stat.mtimeMs,
|
|
443
|
-
value
|
|
444
|
-
});
|
|
445
|
-
return value;
|
|
420
|
+
const target = file;
|
|
421
|
+
writing = writing.then(() => node_fs.default.promises.appendFile(target, `${line}\n`, "utf8")).catch(reportLogFailure);
|
|
446
422
|
};
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
423
|
+
return {
|
|
424
|
+
logger: {
|
|
425
|
+
info: (message) => append(withoutAnsi(message)),
|
|
426
|
+
warn: (message) => {
|
|
427
|
+
const line = withoutAnsi(message);
|
|
428
|
+
append(line);
|
|
429
|
+
telemetry?.recordWarning(ENGINE_WARNING_EVENT, new Error(line.trim()));
|
|
430
|
+
},
|
|
431
|
+
error: (message) => {
|
|
432
|
+
const line = withoutAnsi(message);
|
|
433
|
+
append(line);
|
|
434
|
+
telemetry?.recordError(ENGINE_ERROR_EVENT, new Error(line.trim()), {}, { includeException: true });
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
attach(target) {
|
|
438
|
+
file = target;
|
|
439
|
+
const held = pending.splice(0);
|
|
440
|
+
if (held.length === 0) return;
|
|
441
|
+
writing = writing.then(() => node_fs.default.promises.appendFile(target, held.map((line) => `${line}\n`).join(""), "utf8")).catch(reportLogFailure);
|
|
442
|
+
},
|
|
443
|
+
flushed: () => writing
|
|
453
444
|
};
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Runs workflows inside the DoomPi server for one session.
|
|
448
|
+
*
|
|
449
|
+
* It is the same launch the Pi extension runs, through the same executor:
|
|
450
|
+
* the same Agiflow checks, capacity limit and runner check, and the same
|
|
451
|
+
* answer once the run registers. What the server adds is where the run
|
|
452
|
+
* happens, in this process with the host's step executor, and what it keeps:
|
|
453
|
+
* each run's engine log, telemetry for its start and end, a notice when a run
|
|
454
|
+
* fails after its launch already answered, and a stop for every run still
|
|
455
|
+
* going when the session closes.
|
|
456
|
+
*/
|
|
457
|
+
function createServerLauncher(dependencies) {
|
|
458
|
+
const { feature, telemetry } = dependencies;
|
|
459
|
+
const active = /* @__PURE__ */ new Map();
|
|
460
|
+
const runInProcess = async (input) => {
|
|
461
|
+
const launchId = input.env?.[require_workflowTerminal.WORKFLOW_LAUNCH_ID_ENV];
|
|
462
|
+
if (launchId === void 0) throw new Error("An in-process launch needs its launch id.");
|
|
463
|
+
const log = createRunLog(telemetry);
|
|
464
|
+
const run = { settled: Promise.resolve() };
|
|
465
|
+
active.set(launchId, run);
|
|
466
|
+
const runner = input.runner ?? input.cliAgent;
|
|
467
|
+
const execution = (async () => {
|
|
468
|
+
try {
|
|
469
|
+
const result = await feature.createRunService({ logger: log.logger }).run({
|
|
470
|
+
...input,
|
|
471
|
+
...runner === void 0 ? {} : { runner },
|
|
472
|
+
skipLaunch: true,
|
|
473
|
+
onRegistered: (registration) => {
|
|
474
|
+
run.registration = registration;
|
|
475
|
+
log.attach(node_path.default.join(registration.runDir, RUN_LOG_FILENAME));
|
|
476
|
+
telemetry?.recordEvent(RUN_STARTED_EVENT, { outcome: "started" });
|
|
477
|
+
dependencies.onRunsChanged?.();
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
if (SUCCESS_EXIT_CODES.has(result.exitCode)) {
|
|
481
|
+
telemetry?.recordEvent(RUN_FINISHED_EVENT, {
|
|
482
|
+
outcome: "success",
|
|
483
|
+
exit_code: result.exitCode
|
|
484
|
+
});
|
|
485
|
+
return textResult$1(withoutAnsi(result.output) || "Workflow completed.");
|
|
486
|
+
}
|
|
487
|
+
const tail = withoutAnsi(result.output).trim().split("\n").slice(-20).join("\n");
|
|
488
|
+
telemetry?.recordError(RUN_FAILED_EVENT, new Error(tail || `The workflow exited with code ${String(result.exitCode)}.`), { exit_code: result.exitCode }, { includeException: true });
|
|
489
|
+
return textResult$1(`Workflow failed (exit code ${String(result.exitCode)}):\n\n${withoutAnsi(result.output)}`, true);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
telemetry?.recordError(RUN_FAILED_EVENT, error, { outcome: "error" }, { includeException: true });
|
|
492
|
+
throw error;
|
|
493
|
+
} finally {
|
|
494
|
+
await log.flushed();
|
|
495
|
+
active.delete(launchId);
|
|
496
|
+
dependencies.onRunsChanged?.();
|
|
497
|
+
}
|
|
498
|
+
})();
|
|
499
|
+
run.settled = execution.then(() => void 0, () => void 0);
|
|
500
|
+
return execution;
|
|
501
|
+
};
|
|
502
|
+
const executor = require_workflowTerminal.createWorkflowLaunchExecutor({
|
|
503
|
+
environment: dependencies.environment,
|
|
504
|
+
activeRunCount: async () => active.size,
|
|
505
|
+
...dependencies.rejectRunner === void 0 ? {} : { rejectRunner: dependencies.rejectRunner },
|
|
506
|
+
runTool: { execute: runInProcess },
|
|
507
|
+
trackPendingRun: (pending) => pending,
|
|
508
|
+
findLaunchedRun: async ({ launchId }) => {
|
|
509
|
+
const registration = active.get(launchId)?.registration;
|
|
510
|
+
if (registration === void 0) return void 0;
|
|
511
|
+
return feature.registry.readRunByKey(registration.workspace, RUNNING_STAGE, registration.runKey);
|
|
512
|
+
},
|
|
513
|
+
onLateFailure: (error) => {
|
|
514
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
515
|
+
dependencies.notify(`Workflow launch failed after it was reported started.\n${detail}`, "warning");
|
|
516
|
+
},
|
|
517
|
+
onLaunch: () => {
|
|
518
|
+
telemetry?.recordEvent(LAUNCH_REQUESTED_EVENT, { outcome: "requested" });
|
|
519
|
+
dependencies.onRunsChanged?.();
|
|
520
|
+
},
|
|
521
|
+
...dependencies.launchAckPollMs === void 0 ? {} : { launchAckPollMs: dependencies.launchAckPollMs },
|
|
522
|
+
...dependencies.launchAckTimeoutMs === void 0 ? {} : { launchAckTimeoutMs: dependencies.launchAckTimeoutMs }
|
|
523
|
+
});
|
|
524
|
+
const context = { sessionManager: { getSessionId: () => dependencies.sessionId } };
|
|
525
|
+
return {
|
|
526
|
+
launch: (input) => executor.execute(input, context),
|
|
527
|
+
activeRunCount: () => active.size,
|
|
528
|
+
async dispose() {
|
|
529
|
+
const runs = [...active.values()];
|
|
530
|
+
if (runs.length === 0) return;
|
|
531
|
+
await Promise.all(runs.map(async ({ registration }) => {
|
|
532
|
+
if (registration === void 0) return;
|
|
533
|
+
try {
|
|
534
|
+
await feature.registry.requestStop(registration.workspace, registration.runKey, SESSION_CLOSED_REASON, registration.runId);
|
|
535
|
+
} catch (error) {
|
|
536
|
+
await telemetry?.recordWarning(RUN_STOP_FAILED_EVENT, error);
|
|
537
|
+
}
|
|
538
|
+
}));
|
|
539
|
+
await Promise.race([Promise.all(runs.map((run) => run.settled)), delay$1(dependencies.stopTimeoutMs ?? STOP_TIMEOUT_MS)]);
|
|
468
540
|
}
|
|
469
|
-
}
|
|
470
|
-
return runs;
|
|
541
|
+
};
|
|
471
542
|
}
|
|
472
543
|
//#endregion
|
|
544
|
+
//#region src/schemas/runConfig.ts
|
|
545
|
+
/** A YAML list, or one comma-separated string, as `--domains a,b` accepts. */
|
|
546
|
+
const nameList = zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]).transform((value) => (Array.isArray(value) ? value : value.split(",")).map((name) => name.trim()).filter((name) => name.length > 0));
|
|
547
|
+
const singleName = zod.z.string().trim().min(1);
|
|
548
|
+
/**
|
|
549
|
+
* The DoomPi keys of a workflow step's `runConfig`.
|
|
550
|
+
*
|
|
551
|
+
* The engine treats `runConfig` as an opaque map, so this is where a typo such
|
|
552
|
+
* as `majormode` is caught instead of silently running on workspace defaults.
|
|
553
|
+
*/
|
|
554
|
+
const doompiRunConfigSchema = zod.z.object({
|
|
555
|
+
majorMode: singleName.optional().describe("Major mode the step session runs in."),
|
|
556
|
+
minorModes: nameList.optional().describe("Minor modes activated in the step session."),
|
|
557
|
+
profile: singleName.optional().describe("Profile the step session runs as."),
|
|
558
|
+
domains: nameList.optional().describe("Domains selected for the step session. An empty list selects none."),
|
|
559
|
+
model: singleName.optional().describe("Model selector, provider/id."),
|
|
560
|
+
thinking: singleName.optional().describe("Thinking level for the model.")
|
|
561
|
+
}).strict();
|
|
562
|
+
/**
|
|
563
|
+
* The same keys for a templated step, whose `runConfig` is also the data its
|
|
564
|
+
* command template reads: keys DoomPi does not use are left to the template.
|
|
565
|
+
*/
|
|
566
|
+
const doompiTemplateRunConfigSchema = doompiRunConfigSchema.strip();
|
|
567
|
+
/** The runConfig keys DoomPi reads itself, the host keys `workflow-mcp doctor` checks steps against. */
|
|
568
|
+
const DOOMPI_RUN_CONFIG_KEYS = doompiRunConfigSchema.keyof().options;
|
|
569
|
+
//#endregion
|
|
473
570
|
//#region src/types/webWorkflows.ts
|
|
571
|
+
/** A customRun step ran as this child session. */
|
|
572
|
+
const STEP_SESSION_REF_KIND = "session";
|
|
573
|
+
/** A command step ran in this terminal pane. */
|
|
574
|
+
const STEP_PANE_REF_KIND = "pane";
|
|
474
575
|
const WORKFLOW_RUNS_TYPE = "workflow_runs";
|
|
475
576
|
const WORKFLOW_CATALOG_TYPE = "workflow_catalog";
|
|
476
577
|
//#endregion
|
|
477
|
-
//#region src/services/
|
|
478
|
-
const
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
578
|
+
//#region src/services/stepExecutor/index.ts
|
|
579
|
+
const STEP_DISPLAY_ENV = "WORKFLOW_STEP_DISPLAY";
|
|
580
|
+
const WORKFLOW_NAME_ENV = "WORKFLOW_NAME";
|
|
581
|
+
/** The engine's completion signal for terminal agents; a session this host steers never uses it. */
|
|
582
|
+
const WORKFLOW_STATUS_FILE_ENV = "WORKFLOW_STATUS_FILE";
|
|
583
|
+
/** How long a busy step session is left to finish a turn it was given elsewhere, such as web guidance. */
|
|
584
|
+
const BUSY_RETRY_MS = 2e3;
|
|
585
|
+
/** How long ending a step's session may take before the step stops waiting for it. */
|
|
586
|
+
const RELEASE_TIMEOUT_MS = 1e4;
|
|
587
|
+
/** What the runtime says when a turn is already running; the reminder then waits for it. */
|
|
588
|
+
const BUSY_SESSION_PATTERN = /\bbusy\b|LaneBusy/i;
|
|
589
|
+
const STEP_SESSION_STARTED_EVENT = "doom_workflow.step_session_started";
|
|
590
|
+
const STEP_SESSION_FAILED_EVENT = "doom_workflow.step_session_failed";
|
|
591
|
+
const STEP_REMINDER_EVENT = "doom_workflow.step_reminder";
|
|
592
|
+
const STEP_SESSION_RELEASE_FAILED_EVENT = "doom_workflow.step_session_release_failed";
|
|
593
|
+
const STEP_PANE_FAILED_EVENT = "doom_workflow.step_pane_failed";
|
|
594
|
+
const DOOMPI_KEYS = new Set(DOOMPI_RUN_CONFIG_KEYS);
|
|
595
|
+
/**
|
|
596
|
+
* A step's `runConfig`, validated against the keys DoomPi understands.
|
|
597
|
+
*
|
|
598
|
+
* A templated step's may also carry keys its command template reads. A key
|
|
599
|
+
* that is neither, such as `majormode`, fails the step rather than running it
|
|
600
|
+
* on workspace defaults. An engine that does not say what its template reads
|
|
601
|
+
* leaves such keys unchecked.
|
|
602
|
+
*/
|
|
603
|
+
function readDoompiRunConfig(runConfig, template) {
|
|
604
|
+
const parsed = (template === void 0 ? doompiRunConfigSchema : doompiTemplateRunConfigSchema).safeParse(runConfig ?? {});
|
|
605
|
+
if (!parsed.success) {
|
|
606
|
+
const problems = parsed.error.issues.map((issue) => `${issue.path.join(".") || "runConfig"}: ${issue.message}`);
|
|
607
|
+
throw new Error(`Invalid runConfig: ${problems.join("; ")}`);
|
|
497
608
|
}
|
|
498
|
-
if (
|
|
499
|
-
|
|
500
|
-
|
|
609
|
+
if (template?.reads !== void 0 && typeof runConfig === "object" && runConfig !== null) {
|
|
610
|
+
const reads = new Set(template.reads);
|
|
611
|
+
const unknown = Object.keys(runConfig).filter((key) => !DOOMPI_KEYS.has(key) && !reads.has(key));
|
|
612
|
+
if (unknown.length > 0) throw new Error(`Invalid runConfig: ${unknown.join(", ")} is neither a DoomPi setting nor read by the "${template.name}" command.`);
|
|
613
|
+
}
|
|
614
|
+
return parsed.data;
|
|
615
|
+
}
|
|
616
|
+
/** The selection a step session is pinned to; unset keys keep the workspace defaults. */
|
|
617
|
+
function sessionSelection(config) {
|
|
618
|
+
return {
|
|
619
|
+
...config.majorMode === void 0 ? {} : { majorMode: config.majorMode },
|
|
620
|
+
...config.minorModes === void 0 ? {} : { minorModes: config.minorModes },
|
|
621
|
+
...config.profile === void 0 ? {} : { profile: config.profile },
|
|
622
|
+
...config.domains === void 0 ? {} : { domains: config.domains }
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* What a step adds to the host environment.
|
|
627
|
+
*
|
|
628
|
+
* The step environment is the whole process environment plus the run's own
|
|
629
|
+
* keys. Handing all of it to the session would pin a copy of the host's
|
|
630
|
+
* variables into it; the difference is the part the agent actually needs, such
|
|
631
|
+
* as WORKFLOW_RUN_DIR for the artifacts its prompt names.
|
|
632
|
+
*/
|
|
633
|
+
function stepEnvironment(env, host) {
|
|
634
|
+
return Object.fromEntries(Object.entries(env).filter(([key, value]) => host[key] !== value));
|
|
635
|
+
}
|
|
636
|
+
function stepSessionName(env, stepName) {
|
|
637
|
+
const display = env[STEP_DISPLAY_ENV] ?? stepName;
|
|
638
|
+
const workflow = env[WORKFLOW_NAME_ENV];
|
|
639
|
+
return workflow ? `${workflow}: ${display}` : display;
|
|
640
|
+
}
|
|
641
|
+
function asError(error) {
|
|
642
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
643
|
+
}
|
|
644
|
+
function delay(ms) {
|
|
645
|
+
return new Promise((settle) => {
|
|
646
|
+
setTimeout(settle, ms).unref?.();
|
|
501
647
|
});
|
|
648
|
+
}
|
|
649
|
+
/** A promise settled by `settle`, so a stop can end a wait that nothing else would. */
|
|
650
|
+
function signal() {
|
|
651
|
+
let settle = () => void 0;
|
|
502
652
|
return {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
653
|
+
promise: new Promise((resolve) => {
|
|
654
|
+
settle = resolve;
|
|
655
|
+
}),
|
|
656
|
+
settle
|
|
506
657
|
};
|
|
507
658
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
659
|
+
/**
|
|
660
|
+
* An execution that already failed, with why.
|
|
661
|
+
*
|
|
662
|
+
* Returned instead of throwing: the engine reports a thrown start as a spawn
|
|
663
|
+
* error about shells and permissions, while a failed completion keeps this
|
|
664
|
+
* message as the step's reason.
|
|
665
|
+
*/
|
|
666
|
+
function failedExecution(error) {
|
|
667
|
+
return {
|
|
668
|
+
completion: Promise.resolve({ error: asError(error) }),
|
|
669
|
+
stop: async () => void 0
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Runs workflow steps inside DoomPi instead of a shell.
|
|
674
|
+
*
|
|
675
|
+
* A customRun step becomes a child session of the launching session, with the
|
|
676
|
+
* step's major mode, minor modes, profile, domains and model, so its messages
|
|
677
|
+
* stream like any other conversation. Command steps go to their own terminal
|
|
678
|
+
* pane when a launcher is available and fall back to the engine's spawn when
|
|
679
|
+
* it is not.
|
|
680
|
+
*/
|
|
681
|
+
function createStepExecutor(dependencies) {
|
|
682
|
+
const { sessionService, launchPane, telemetry } = dependencies;
|
|
683
|
+
const prompt = sessionService.prompt?.bind(sessionService);
|
|
684
|
+
/**
|
|
685
|
+
* Run a step as a child session of the launching session, with the step's
|
|
686
|
+
* major mode, minor modes, profile, domains and model.
|
|
687
|
+
*
|
|
688
|
+
* The session keeps going until the step has what the workflow needs: each
|
|
689
|
+
* time the agent settles, the engine's decision gate is asked, and while
|
|
690
|
+
* the step still lacks a decision (or its declared artifacts) the session is
|
|
691
|
+
* prompted with the gate's reminder. The gate counts reminders against its
|
|
692
|
+
* cap, so a stuck agent ends and the engine fails the step.
|
|
693
|
+
*/
|
|
694
|
+
async function runSession(request) {
|
|
695
|
+
if (prompt === void 0) return failedExecution("This host cannot prompt sessions, so this step cannot run here.");
|
|
696
|
+
const attributes = {
|
|
697
|
+
"workflow.step.name": request.stepName,
|
|
698
|
+
templated: request.template !== void 0
|
|
699
|
+
};
|
|
700
|
+
let scope;
|
|
701
|
+
let name;
|
|
522
702
|
try {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
703
|
+
const config = readDoompiRunConfig(request.runConfig, request.template);
|
|
704
|
+
name = stepSessionName(request.env, request.stepName);
|
|
705
|
+
const environment = stepEnvironment(request.env, dependencies.hostEnvironment);
|
|
706
|
+
delete environment[WORKFLOW_STATUS_FILE_ENV];
|
|
707
|
+
environment[_agimon_ai_workflow_mcp.WORKFLOW_DECISION_STEERING_ENV] = _agimon_ai_workflow_mcp.WORKFLOW_DECISION_STEERING_HOST;
|
|
708
|
+
scope = await sessionService.create({
|
|
709
|
+
cwd: request.cwd,
|
|
710
|
+
name,
|
|
711
|
+
parentSessionId: dependencies.parentSessionId,
|
|
712
|
+
sessionProvenance: _agimon_ai_doompi_core_hubChannel.WORKFLOW_STEP_SESSION_PROVENANCE,
|
|
713
|
+
selection: sessionSelection(config),
|
|
714
|
+
...config.model === void 0 ? {} : { model: config.model },
|
|
715
|
+
...config.thinking === void 0 ? {} : { thinking: config.thinking },
|
|
716
|
+
...request.systemPrompt === void 0 ? {} : { appendSystemPrompt: request.systemPrompt },
|
|
717
|
+
environment
|
|
526
718
|
});
|
|
527
719
|
} catch (error) {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
720
|
+
telemetry?.recordError(STEP_SESSION_FAILED_EVENT, error, attributes, { includeException: true });
|
|
721
|
+
return failedExecution(error);
|
|
722
|
+
}
|
|
723
|
+
telemetry?.recordEvent(STEP_SESSION_STARTED_EVENT, {
|
|
724
|
+
...attributes,
|
|
725
|
+
outcome: "started"
|
|
726
|
+
});
|
|
727
|
+
const sessionId = scope.sessionId;
|
|
728
|
+
const stopped = signal();
|
|
729
|
+
let stopRequested = false;
|
|
730
|
+
const decided = () => {
|
|
731
|
+
const path = request.env[_agimon_ai_workflow_mcp.WORKFLOW_DECISION_FILE_ENV];
|
|
732
|
+
return path !== void 0 && (0, _agimon_ai_workflow_mcp.readStepDecision)(path) !== void 0;
|
|
733
|
+
};
|
|
734
|
+
/**
|
|
735
|
+
* Give the agent the gate's reminder. A session busy with a turn it was
|
|
736
|
+
* given elsewhere, like web guidance, is left to finish it, then reminded
|
|
737
|
+
* unless it decided meanwhile. Undefined means the step needs no more turns.
|
|
738
|
+
*/
|
|
739
|
+
const remind = async (message) => {
|
|
740
|
+
for (;;) try {
|
|
741
|
+
return await prompt(sessionId, message);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
if (!BUSY_SESSION_PATTERN.test(asError(error).message)) throw error;
|
|
744
|
+
telemetry?.recordEvent(STEP_REMINDER_EVENT, {
|
|
745
|
+
...attributes,
|
|
746
|
+
outcome: "busy"
|
|
747
|
+
});
|
|
748
|
+
await Promise.race([delay(dependencies.busyRetryMs ?? BUSY_RETRY_MS), stopped.promise]);
|
|
749
|
+
if (stopRequested || decided()) return void 0;
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
/**
|
|
753
|
+
* End the session once the step is over. Released, it stays readable in the
|
|
754
|
+
* workflow's view without holding a model, tools and extensions in the host.
|
|
755
|
+
* A host that cannot release it is asked to abort a stopped turn instead.
|
|
756
|
+
*/
|
|
757
|
+
const endSession = async () => {
|
|
758
|
+
const ending = sessionService.release ? sessionService.release(sessionId) : stopRequested ? sessionService.abort?.(sessionId) ?? Promise.resolve() : Promise.resolve();
|
|
759
|
+
const timeout = delay(dependencies.releaseTimeoutMs ?? RELEASE_TIMEOUT_MS).then(() => {
|
|
760
|
+
throw new Error(`The step session did not end within ${String(dependencies.releaseTimeoutMs ?? RELEASE_TIMEOUT_MS)}ms.`);
|
|
761
|
+
});
|
|
762
|
+
try {
|
|
763
|
+
await Promise.race([ending, timeout]);
|
|
764
|
+
} catch (error) {
|
|
765
|
+
telemetry?.recordWarning(STEP_SESSION_RELEASE_FAILED_EVENT, error, attributes);
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
const completion = async () => {
|
|
769
|
+
let receipt = await prompt(sessionId, request.prompt);
|
|
770
|
+
while (receipt !== void 0) {
|
|
771
|
+
await Promise.race([receipt.settled, stopped.promise]);
|
|
772
|
+
if (stopRequested) return { exitCode: 0 };
|
|
773
|
+
const gate = await (0, _agimon_ai_workflow_mcp.gateStepDecision)(request.env);
|
|
774
|
+
if (gate.action === "allow") return { exitCode: 0 };
|
|
775
|
+
telemetry?.recordEvent(STEP_REMINDER_EVENT, {
|
|
776
|
+
...attributes,
|
|
777
|
+
outcome: "sent"
|
|
778
|
+
});
|
|
779
|
+
receipt = await remind(gate.message);
|
|
780
|
+
}
|
|
781
|
+
return { exitCode: 0 };
|
|
782
|
+
};
|
|
783
|
+
return {
|
|
784
|
+
ref: {
|
|
785
|
+
kind: STEP_SESSION_REF_KIND,
|
|
786
|
+
id: sessionId,
|
|
787
|
+
label: name
|
|
788
|
+
},
|
|
789
|
+
completion: completion().catch((error) => ({ error: asError(error) })).then(async (outcome) => {
|
|
790
|
+
await endSession();
|
|
791
|
+
return outcome;
|
|
792
|
+
}),
|
|
793
|
+
stop: async () => {
|
|
794
|
+
stopRequested = true;
|
|
795
|
+
stopped.settle();
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
async custom(request) {
|
|
801
|
+
return runSession({
|
|
802
|
+
cwd: request.cwd,
|
|
803
|
+
env: request.env,
|
|
804
|
+
stepName: request.stepName,
|
|
805
|
+
runConfig: request.runConfig,
|
|
806
|
+
prompt: request.customRun.prompt,
|
|
807
|
+
...request.customRun.systemPrompt === void 0 ? {} : { systemPrompt: request.customRun.systemPrompt }
|
|
808
|
+
});
|
|
809
|
+
},
|
|
810
|
+
async command(request) {
|
|
811
|
+
const template = request.template;
|
|
812
|
+
if (template?.inProcess && prompt !== void 0) return runSession({
|
|
813
|
+
cwd: request.cwd,
|
|
814
|
+
env: request.env,
|
|
815
|
+
stepName: request.stepName,
|
|
816
|
+
runConfig: request.runConfig,
|
|
817
|
+
template: {
|
|
818
|
+
name: template.name,
|
|
819
|
+
...template.reads === void 0 ? {} : { reads: template.reads }
|
|
820
|
+
},
|
|
821
|
+
prompt: template.prompt,
|
|
822
|
+
...template.systemPrompt === void 0 ? {} : { systemPrompt: template.systemPrompt }
|
|
823
|
+
});
|
|
824
|
+
if (launchPane === void 0) return void 0;
|
|
825
|
+
let pane;
|
|
826
|
+
try {
|
|
827
|
+
pane = await launchPane({
|
|
828
|
+
id: dependencies.createId(),
|
|
829
|
+
command: request.command,
|
|
830
|
+
cwd: request.cwd,
|
|
831
|
+
env: request.env
|
|
832
|
+
});
|
|
833
|
+
} catch (error) {
|
|
834
|
+
telemetry?.recordError(STEP_PANE_FAILED_EVENT, error, { "workflow.step.name": request.stepName }, { includeException: true });
|
|
835
|
+
return failedExecution(error);
|
|
836
|
+
}
|
|
837
|
+
if (pane === void 0) return void 0;
|
|
838
|
+
return {
|
|
839
|
+
ref: {
|
|
840
|
+
kind: STEP_PANE_REF_KIND,
|
|
841
|
+
id: pane.target,
|
|
842
|
+
label: request.stepName
|
|
843
|
+
},
|
|
844
|
+
completion: pane.completion.then((result) => result, (error) => ({ error: asError(error) })),
|
|
845
|
+
stop: async () => {
|
|
846
|
+
await pane.stop();
|
|
847
|
+
}
|
|
848
|
+
};
|
|
543
849
|
}
|
|
544
|
-
});
|
|
545
|
-
let control;
|
|
546
|
-
let controlDisposers = [];
|
|
547
|
-
const disposeControl = () => {
|
|
548
|
-
for (const dispose of controlDisposers.splice(0)) dispose();
|
|
549
|
-
control?.dispose();
|
|
550
|
-
control = void 0;
|
|
551
850
|
};
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
851
|
+
}
|
|
852
|
+
/** The shell a command step runs in, the same one the engine spawns it with. */
|
|
853
|
+
const STEP_SHELL = "/bin/zsh";
|
|
854
|
+
/** Namespace for step pane sockets, apart from whole-run terminal sockets. */
|
|
855
|
+
const STEP_PANE_SOCKET_SCOPE = "doompi-workflow-steps";
|
|
856
|
+
const PANE_LOG_SUFFIX = ".log";
|
|
857
|
+
const PANE_CAPTURE_COLUMNS = 120;
|
|
858
|
+
const PANE_CAPTURE_ROWS = 40;
|
|
859
|
+
/** Where a finished step pane's last screen is kept: beside the socket that served it. */
|
|
860
|
+
function stepPaneLogPath(socketPath) {
|
|
861
|
+
return `${socketPath}${PANE_LOG_SUFFIX}`;
|
|
862
|
+
}
|
|
863
|
+
/** The pane's current screen, or undefined once there is no pane left to read. */
|
|
864
|
+
async function lastScreen(service, key) {
|
|
865
|
+
try {
|
|
866
|
+
const viewer = await service.attach(key, PANE_CAPTURE_COLUMNS, PANE_CAPTURE_ROWS);
|
|
867
|
+
const lines = await viewer.snapshot();
|
|
868
|
+
viewer.detach();
|
|
869
|
+
return `${lines.join("\n")}\n`;
|
|
870
|
+
} catch {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Opens each command step in its own RMUX pane, through workflow-mcp's terminal host.
|
|
876
|
+
*
|
|
877
|
+
* The pane gets the step's whole environment, which is what lets an agent's
|
|
878
|
+
* Stop hook end an interactive step through WORKFLOW_STATUS_FILE. A unix
|
|
879
|
+
* socket serves the pane to the run terminal in any scope; the socket path is
|
|
880
|
+
* the pane's target. When the step ends or is stopped, its last screen is kept
|
|
881
|
+
* beside the socket and the pane is released, whatever fails on the way.
|
|
882
|
+
*/
|
|
883
|
+
function createNativeStepPaneLauncher(service = new _agimon_ai_workflow_mcp.NativeTerminalService(), telemetry) {
|
|
884
|
+
const report = (stage) => (error) => telemetry?.recordWarning(STEP_PANE_FAILED_EVENT, error, { stage }) ?? Promise.resolve();
|
|
885
|
+
return async (request) => {
|
|
886
|
+
if (!await service.available()) return void 0;
|
|
887
|
+
await service.spawn({
|
|
888
|
+
command: STEP_SHELL,
|
|
889
|
+
args: ["-c", request.command],
|
|
890
|
+
cwd: request.cwd,
|
|
891
|
+
env: { ...request.env },
|
|
892
|
+
runKey: request.id
|
|
893
|
+
});
|
|
894
|
+
const target = (0, _agimon_ai_workflow_mcp.terminalSocketPath)(STEP_PANE_SOCKET_SCOPE, request.id);
|
|
895
|
+
let server;
|
|
896
|
+
try {
|
|
897
|
+
server = await (0, _agimon_ai_workflow_mcp.serveNativeTerminal)({
|
|
898
|
+
service,
|
|
899
|
+
runKey: request.id,
|
|
900
|
+
socketPath: target
|
|
901
|
+
});
|
|
902
|
+
} catch (error) {
|
|
903
|
+
await service.kill(request.id).catch(report("kill"));
|
|
904
|
+
throw error;
|
|
905
|
+
}
|
|
906
|
+
let finishing;
|
|
907
|
+
const finish = () => finishing ??= (async () => {
|
|
908
|
+
let screen;
|
|
909
|
+
try {
|
|
910
|
+
screen = await lastScreen(service, request.id);
|
|
911
|
+
if (screen !== void 0) await node_fs.default.promises.writeFile(stepPaneLogPath(target), screen, "utf8");
|
|
912
|
+
} catch (error) {
|
|
913
|
+
await report("keep-screen")(error);
|
|
914
|
+
} finally {
|
|
915
|
+
await server.close().catch(report("close-socket"));
|
|
916
|
+
await service.kill(request.id).catch(report("kill"));
|
|
917
|
+
}
|
|
918
|
+
return screen;
|
|
919
|
+
})();
|
|
556
920
|
return {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
},
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
921
|
+
target,
|
|
922
|
+
completion: service.ended(request.id).then(async (exitCode) => {
|
|
923
|
+
const screen = (await finish())?.trimEnd();
|
|
924
|
+
return screen ? {
|
|
925
|
+
exitCode,
|
|
926
|
+
outputTail: screen
|
|
927
|
+
} : { exitCode };
|
|
928
|
+
}),
|
|
929
|
+
stop: async () => {
|
|
930
|
+
await finish();
|
|
931
|
+
return true;
|
|
932
|
+
}
|
|
569
933
|
};
|
|
570
934
|
};
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
935
|
+
}
|
|
936
|
+
//#endregion
|
|
937
|
+
//#region src/services/webWorkflowCatalog/index.ts
|
|
938
|
+
function unchanged(left, right) {
|
|
939
|
+
return left !== void 0 && right !== void 0 && left.size === right.size && left.modifiedAt === right.modifiedAt;
|
|
940
|
+
}
|
|
941
|
+
function createWorkflowCatalogReader(deps) {
|
|
942
|
+
const cache = /* @__PURE__ */ new Map();
|
|
943
|
+
const detailFor = (path) => {
|
|
944
|
+
const stamp = deps.stamp(path);
|
|
945
|
+
const cached = cache.get(path);
|
|
946
|
+
if (cached && unchanged(cached.stamp, stamp)) return cached.detail;
|
|
947
|
+
const detail = deps.summarize(path);
|
|
948
|
+
if (stamp !== void 0) cache.set(path, {
|
|
949
|
+
stamp,
|
|
950
|
+
detail
|
|
577
951
|
});
|
|
578
|
-
|
|
952
|
+
return detail;
|
|
579
953
|
};
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
954
|
+
return {
|
|
955
|
+
async read(directory) {
|
|
956
|
+
return (await deps.list(directory)).map((entry) => {
|
|
957
|
+
const detail = detailFor(entry.path);
|
|
958
|
+
return {
|
|
959
|
+
path: entry.path,
|
|
960
|
+
relativePath: entry.relativePath,
|
|
961
|
+
name: entry.name === "" ? entry.relativePath : entry.name,
|
|
962
|
+
description: entry.description,
|
|
963
|
+
tags: entry.tags,
|
|
964
|
+
triggers: detail.triggers,
|
|
965
|
+
inputs: detail.inputs,
|
|
966
|
+
jobs: detail.jobs,
|
|
967
|
+
artifacts: detail.artifacts,
|
|
968
|
+
...detail.runners === void 0 ? {} : { runners: detail.runners },
|
|
969
|
+
...detail.commands === void 0 ? {} : { commands: detail.commands },
|
|
970
|
+
...detail.choices === void 0 ? {} : { choices: detail.choices },
|
|
971
|
+
...detail.error === void 0 ? {} : { error: detail.error }
|
|
972
|
+
};
|
|
973
|
+
});
|
|
600
974
|
},
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
975
|
+
forget(keep) {
|
|
976
|
+
for (const path of cache.keys()) if (!keep.has(path)) cache.delete(path);
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
/** The launchable rows first, then the ones that would not parse, name ordered. */
|
|
981
|
+
function presentWorkflowCatalog(entries) {
|
|
982
|
+
return [...entries].sort((left, right) => {
|
|
983
|
+
if (left.error === void 0 !== (right.error === void 0)) return left.error === void 0 ? -1 : 1;
|
|
984
|
+
return left.name.localeCompare(right.name);
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
//#endregion
|
|
988
|
+
//#region src/services/workflowCatalogDeps/index.ts
|
|
989
|
+
/**
|
|
990
|
+
* Connect the pure catalog projection service to the workflow engine and filesystem.
|
|
991
|
+
*
|
|
992
|
+
* A workflow that parses can still be one DoomPi would fail mid-run, such as
|
|
993
|
+
* a step whose runConfig misspells `majorMode`. The engine's doctor, told the
|
|
994
|
+
* keys DoomPi reads, finds those; its errors mark the workflow as one that
|
|
995
|
+
* cannot launch, in the catalog and at every launch, rather than at the step.
|
|
996
|
+
*/
|
|
997
|
+
function defaultCatalogDeps() {
|
|
998
|
+
return {
|
|
999
|
+
list: (directory) => (0, _agimon_ai_workflow_mcp.readWorkflowCatalog)(directory),
|
|
1000
|
+
summarize: (workflowPath) => {
|
|
1001
|
+
const detail = (0, _agimon_ai_workflow_mcp.summarizeWorkflow)(workflowPath);
|
|
1002
|
+
if (detail.error !== void 0) return detail;
|
|
1003
|
+
const errors = (0, _agimon_ai_workflow_mcp.diagnoseWorkflow)(workflowPath, { runConfigKeys: DOOMPI_RUN_CONFIG_KEYS }).diagnostics.filter((diagnostic) => diagnostic.level === "error");
|
|
1004
|
+
if (errors.length === 0) return detail;
|
|
1005
|
+
return {
|
|
1006
|
+
...detail,
|
|
1007
|
+
error: errors.map((diagnostic) => diagnostic.location === void 0 ? diagnostic.message : `${diagnostic.location}: ${diagnostic.message}`).join("\n")
|
|
1008
|
+
};
|
|
1009
|
+
},
|
|
1010
|
+
stamp: (workflowPath) => {
|
|
1011
|
+
try {
|
|
1012
|
+
const stats = (0, node_fs.statSync)(workflowPath);
|
|
1013
|
+
return {
|
|
1014
|
+
size: stats.size,
|
|
1015
|
+
modifiedAt: stats.mtimeMs
|
|
1016
|
+
};
|
|
1017
|
+
} catch {
|
|
1018
|
+
return;
|
|
611
1019
|
}
|
|
612
|
-
throw new Error(`Unknown workflow mode action: ${actionId}`);
|
|
613
1020
|
}
|
|
614
|
-
}
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
//#endregion
|
|
1024
|
+
//#region src/services/stepPaneTerminal/index.ts
|
|
1025
|
+
const PROGRESS_FILENAME = "progress.ndjson";
|
|
1026
|
+
const NATIVE_LAUNCHER = "native";
|
|
1027
|
+
/** Runs whose folded refs are kept between reads; a screen stream reads its run twice a second. */
|
|
1028
|
+
const REFS_CACHE_LIMIT = 256;
|
|
1029
|
+
const PANE_CAPABILITIES = {
|
|
1030
|
+
readable: true,
|
|
1031
|
+
writable: true,
|
|
1032
|
+
resizable: false
|
|
1033
|
+
};
|
|
1034
|
+
const PANE_LOG_CAPABILITIES = {
|
|
1035
|
+
readable: true,
|
|
1036
|
+
writable: false,
|
|
1037
|
+
resizable: false,
|
|
1038
|
+
reason: "That step has finished; this is the output it left."
|
|
1039
|
+
};
|
|
1040
|
+
const SESSION_STEP_REASON = "This step runs as an agent session; its conversation appears here as soon as the session starts.";
|
|
1041
|
+
const PANE_GONE_LINE = "The step pane has closed.";
|
|
1042
|
+
/**
|
|
1043
|
+
* The steps a run is on and its latest pane, folded from its progress log.
|
|
1044
|
+
*
|
|
1045
|
+
* Later events win, as in the engine's own summary. Steps of a parallel group
|
|
1046
|
+
* run at once, so every running step is kept, and the current one is the
|
|
1047
|
+
* latest-started step still running. A step that starts again is a new
|
|
1048
|
+
* attempt: its previous session or pane is not this attempt's, so the ref is
|
|
1049
|
+
* dropped until the new one is recorded. The latest pane is kept after it
|
|
1050
|
+
* finishes so a run between steps, or one that has ended, can still show what
|
|
1051
|
+
* its last command printed.
|
|
1052
|
+
*/
|
|
1053
|
+
function stepRefsFrom(events) {
|
|
1054
|
+
const steps = /* @__PURE__ */ new Map();
|
|
1055
|
+
const known = /* @__PURE__ */ new Map();
|
|
1056
|
+
let lastPane;
|
|
1057
|
+
for (const event of events) {
|
|
1058
|
+
if (event.type !== "step" || event.step === void 0) continue;
|
|
1059
|
+
const key = `${event.job}\u0000${event.step}`;
|
|
1060
|
+
const previous = steps.get(key);
|
|
1061
|
+
const running = event.status === "running";
|
|
1062
|
+
const restarted = running && previous?.running !== true;
|
|
1063
|
+
if (restarted) steps.delete(key);
|
|
1064
|
+
const ref = event.ref ?? (restarted ? void 0 : previous?.ref);
|
|
1065
|
+
steps.set(key, {
|
|
1066
|
+
running,
|
|
1067
|
+
...ref === void 0 ? {} : { ref }
|
|
1068
|
+
});
|
|
1069
|
+
if (event.ref !== void 0) known.set(event.ref.id, event.ref);
|
|
1070
|
+
if (event.ref?.kind === "pane") lastPane = event.ref;
|
|
1071
|
+
}
|
|
1072
|
+
const running = [...steps.values()].filter((step) => step.running && step.ref !== void 0).map((step) => step.ref);
|
|
1073
|
+
const current = running.at(-1);
|
|
615
1074
|
return {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
1075
|
+
...current === void 0 ? {} : { current },
|
|
1076
|
+
...running.length === 0 ? {} : { running },
|
|
1077
|
+
...lastPane === void 0 ? {} : { lastPane },
|
|
1078
|
+
...known.size === 0 ? {} : { known: [...known.values()] }
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Reads a run's refs, folding its progress log again only when the log changed.
|
|
1083
|
+
*
|
|
1084
|
+
* Every screen, keystroke and capability check asks for them, and the log grows
|
|
1085
|
+
* with every fix loop, so an unchanged file (same size and modification time)
|
|
1086
|
+
* answers from the last fold.
|
|
1087
|
+
*/
|
|
1088
|
+
function createStepRefsReader() {
|
|
1089
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1090
|
+
return (file) => {
|
|
1091
|
+
let stats;
|
|
1092
|
+
try {
|
|
1093
|
+
stats = node_fs.default.statSync(file);
|
|
1094
|
+
} catch {
|
|
1095
|
+
return {};
|
|
1096
|
+
}
|
|
1097
|
+
const cached = cache.get(file);
|
|
1098
|
+
if (cached !== void 0 && cached.size === stats.size && cached.mtimeMs === stats.mtimeMs) return cached.refs;
|
|
1099
|
+
let raw;
|
|
1100
|
+
try {
|
|
1101
|
+
raw = node_fs.default.readFileSync(file, "utf8");
|
|
1102
|
+
} catch {
|
|
1103
|
+
return {};
|
|
1104
|
+
}
|
|
1105
|
+
const refs = stepRefsFrom(parseWorkflowProgress(raw));
|
|
1106
|
+
cache.delete(file);
|
|
1107
|
+
cache.set(file, {
|
|
1108
|
+
size: stats.size,
|
|
1109
|
+
mtimeMs: stats.mtimeMs,
|
|
1110
|
+
refs
|
|
1111
|
+
});
|
|
1112
|
+
if (cache.size > REFS_CACHE_LIMIT) cache.delete(cache.keys().next().value);
|
|
1113
|
+
return refs;
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Real dependencies: the registry's run directory, and the step executor's pane sockets.
|
|
1118
|
+
*
|
|
1119
|
+
* A pane is reached through the socket its step was served on, so any scope
|
|
1120
|
+
* of the server can read it, and a finished one through the screen kept
|
|
1121
|
+
* beside that socket.
|
|
1122
|
+
*/
|
|
1123
|
+
function createStepPaneTerminalDependencies(registry) {
|
|
1124
|
+
const native = (0, _agimon_ai_workflow_mcp.connectNativeTerminal)();
|
|
1125
|
+
const readRefs = createStepRefsReader();
|
|
1126
|
+
return {
|
|
1127
|
+
stepRefs: (record) => readRefs(node_path.default.join(registry.runDirectoryFor(record), PROGRESS_FILENAME)),
|
|
1128
|
+
paneClient: () => ({
|
|
1129
|
+
capture: (target) => native.snapshot(target, {}).then((lines) => lines.join("\n"), () => void 0),
|
|
1130
|
+
input: (target, text) => native.write(target, text).then(() => true, () => false)
|
|
1131
|
+
}),
|
|
1132
|
+
paneLog: (_record, pane) => node_fs.default.promises.readFile(stepPaneLogPath(pane.id), "utf8").catch(() => void 0)
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
/** Removes the screens a run's finished panes left behind, once the run itself is deleted. */
|
|
1136
|
+
async function removeStepPaneLogs(refs) {
|
|
1137
|
+
const panes = (refs.known ?? []).filter((ref) => ref.kind === STEP_PANE_REF_KIND);
|
|
1138
|
+
await Promise.all(panes.map((pane) => node_fs.default.promises.rm(stepPaneLogPath(pane.id), { force: true })));
|
|
1139
|
+
}
|
|
1140
|
+
function lastLines(text, count) {
|
|
1141
|
+
const lines = text.replace(/\n+$/, "").split("\n");
|
|
1142
|
+
return count === void 0 ? lines : lines.slice(-count);
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* A run terminal that follows the step a host executor placed.
|
|
1146
|
+
*
|
|
1147
|
+
* A run executing in the server has no launcher pane of its own; each command
|
|
1148
|
+
* step gets one instead. A request naming a step reads that step's pane, which
|
|
1149
|
+
* is how each step of a parallel group gets its own screen and keyboard.
|
|
1150
|
+
* Without one it follows the current step. While a customRun step runs, there
|
|
1151
|
+
* is no terminal to show, so it says where the agent's messages are. Between
|
|
1152
|
+
* steps and after the run, an in-process run shows what its latest command
|
|
1153
|
+
* printed. A run delegated to a terminal launcher keeps the engine's facade.
|
|
1154
|
+
*/
|
|
1155
|
+
function createStepPaneTerminal(facade, dependencies) {
|
|
1156
|
+
const inProcess = (record) => record.launcher?.type === NATIVE_LAUNCHER && record.terminalSocket === void 0;
|
|
1157
|
+
const resolveStep = (target) => {
|
|
1158
|
+
const refs = dependencies.stepRefs(target.record);
|
|
1159
|
+
const isRunning = (ref) => refs.running?.some((candidate) => candidate.id === ref.id) === true;
|
|
1160
|
+
if (target.step !== void 0) {
|
|
1161
|
+
const named = refs.known?.find((ref) => ref.id === target.step);
|
|
1162
|
+
if (named !== void 0) return {
|
|
1163
|
+
ref: named,
|
|
1164
|
+
running: isRunning(named)
|
|
622
1165
|
};
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
1166
|
+
}
|
|
1167
|
+
if (refs.current !== void 0) return {
|
|
1168
|
+
ref: refs.current,
|
|
1169
|
+
running: true
|
|
1170
|
+
};
|
|
1171
|
+
if (refs.lastPane !== void 0 && inProcess(target.record)) return {
|
|
1172
|
+
ref: refs.lastPane,
|
|
1173
|
+
running: false
|
|
1174
|
+
};
|
|
1175
|
+
};
|
|
1176
|
+
const readLog = async (record, pane, lines) => {
|
|
1177
|
+
const log = await dependencies.paneLog(record, pane);
|
|
1178
|
+
return log === void 0 ? [PANE_GONE_LINE] : lastLines(log, lines);
|
|
1179
|
+
};
|
|
1180
|
+
return {
|
|
1181
|
+
capabilities(target) {
|
|
1182
|
+
const step = resolveStep(target);
|
|
1183
|
+
if (step === void 0) return facade.capabilities(target.record);
|
|
1184
|
+
if (step.ref.kind === "session") return {
|
|
1185
|
+
readable: false,
|
|
1186
|
+
writable: false,
|
|
1187
|
+
resizable: false,
|
|
1188
|
+
reason: SESSION_STEP_REASON
|
|
1189
|
+
};
|
|
1190
|
+
return step.running ? PANE_CAPABILITIES : PANE_LOG_CAPABILITIES;
|
|
1191
|
+
},
|
|
1192
|
+
async screen(target, options) {
|
|
1193
|
+
const step = resolveStep(target);
|
|
1194
|
+
if (step === void 0) return facade.screen(target.record, options);
|
|
1195
|
+
if (step.ref.kind === "session") return [SESSION_STEP_REASON];
|
|
1196
|
+
if (step.running) {
|
|
1197
|
+
const screen = await dependencies.paneClient(target.record).capture(step.ref.id);
|
|
1198
|
+
if (screen !== void 0) return lastLines(screen, options.lines);
|
|
628
1199
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
resources: [
|
|
641
|
-
{
|
|
642
|
-
when: {
|
|
643
|
-
state: { "minor-mode": WORKFLOW_MODE_ID },
|
|
644
|
-
attribution: {
|
|
645
|
-
kind: "minor",
|
|
646
|
-
mode: WORKFLOW_MODE_ID
|
|
647
|
-
}
|
|
648
|
-
},
|
|
649
|
-
name: "doompi-author-workflow",
|
|
650
|
-
kind: "skill",
|
|
651
|
-
read: () => require_workflowTerminal.readWorkflowSkill("doompi-author-workflow")
|
|
652
|
-
},
|
|
653
|
-
{
|
|
654
|
-
when: {
|
|
655
|
-
state: { "minor-mode": WORKFLOW_MODE_ID },
|
|
656
|
-
attribution: {
|
|
657
|
-
kind: "minor",
|
|
658
|
-
mode: WORKFLOW_MODE_ID
|
|
659
|
-
}
|
|
660
|
-
},
|
|
661
|
-
name: "doompi-use-workflow",
|
|
662
|
-
kind: "skill",
|
|
663
|
-
read: () => require_workflowTerminal.readWorkflowSkill("doompi-use-workflow")
|
|
664
|
-
},
|
|
665
|
-
{
|
|
666
|
-
when: {
|
|
667
|
-
state: { "minor-mode": WORKFLOW_MODE_ID },
|
|
668
|
-
attribution: {
|
|
669
|
-
kind: "minor",
|
|
670
|
-
mode: WORKFLOW_MODE_ID
|
|
671
|
-
}
|
|
672
|
-
},
|
|
673
|
-
name: "workflow-recovery",
|
|
674
|
-
kind: "skill",
|
|
675
|
-
read: () => require_workflowTerminal.readWorkflowSkill("workflow-recovery")
|
|
676
|
-
}
|
|
677
|
-
],
|
|
678
|
-
activities: [{
|
|
679
|
-
when: {
|
|
680
|
-
state: { "minor-mode": WORKFLOW_MODE_ID },
|
|
681
|
-
attribution: {
|
|
682
|
-
kind: "minor",
|
|
683
|
-
mode: WORKFLOW_MODE_ID
|
|
684
|
-
}
|
|
685
|
-
},
|
|
686
|
-
name: SOURCE,
|
|
687
|
-
async start(executionContext) {
|
|
688
|
-
const reportFailure = (error) => {
|
|
689
|
-
process.emitWarning(`Could not publish headless workflow state: ${String(error)}`);
|
|
690
|
-
};
|
|
691
|
-
const stopFor = (ownedControl) => () => {
|
|
692
|
-
publishLifecycle(executionContext).then((active) => {
|
|
693
|
-
if (!active && !modeSelected() && control === ownedControl) disposeControl();
|
|
694
|
-
}).catch(reportFailure);
|
|
695
|
-
};
|
|
696
|
-
if (control !== void 0) {
|
|
697
|
-
const ownedControl = control;
|
|
698
|
-
await publishLifecycle(executionContext);
|
|
699
|
-
return stopFor(ownedControl);
|
|
700
|
-
}
|
|
701
|
-
control = feature.createRunControl({});
|
|
702
|
-
const ownedControl = control;
|
|
703
|
-
const refresh = () => {
|
|
704
|
-
publishLifecycle(executionContext).then((active) => {
|
|
705
|
-
if (!active && !modeSelected() && control === ownedControl) disposeControl();
|
|
706
|
-
}).catch(reportFailure);
|
|
707
|
-
};
|
|
708
|
-
controlDisposers = [
|
|
709
|
-
ownedControl.on("runStarted", refresh),
|
|
710
|
-
ownedControl.on("runUpdated", refresh),
|
|
711
|
-
ownedControl.on("runFinished", refresh)
|
|
712
|
-
];
|
|
713
|
-
try {
|
|
714
|
-
await ownedControl.start();
|
|
715
|
-
await publishLifecycle(executionContext);
|
|
716
|
-
return stopFor(ownedControl);
|
|
717
|
-
} catch (error) {
|
|
718
|
-
if (control === ownedControl) disposeControl();
|
|
719
|
-
throw error;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
}],
|
|
723
|
-
tools: [
|
|
724
|
-
{
|
|
725
|
-
when: {
|
|
726
|
-
state: { "minor-mode": WORKFLOW_MODE_ID },
|
|
727
|
-
attribution: {
|
|
728
|
-
kind: "minor",
|
|
729
|
-
mode: WORKFLOW_MODE_ID
|
|
730
|
-
}
|
|
731
|
-
},
|
|
732
|
-
name: LIST_TOOL,
|
|
733
|
-
label: "List Workflows",
|
|
734
|
-
description: "List workflow definitions in this repository, not existing runs.",
|
|
735
|
-
promptGuidelines: ["Use list_workflows to discover definitions, not to find an existing run."],
|
|
736
|
-
parameters: zod.z.toJSONSchema(feature.listWorkflowsTool.getInputSchema()),
|
|
737
|
-
executionMode: "serial",
|
|
738
|
-
async execute(_toolCallId, parameters, signal, _onUpdate, context) {
|
|
739
|
-
signal?.throwIfAborted();
|
|
740
|
-
const input = parameters;
|
|
741
|
-
return callResult(await feature.listWorkflowsTool.execute({
|
|
742
|
-
...input,
|
|
743
|
-
directory: input?.directory === void 0 ? context.cwd : typeof input.directory === "string" ? (0, node_path.resolve)(context.cwd, input.directory) : input.directory
|
|
744
|
-
}));
|
|
745
|
-
}
|
|
746
|
-
},
|
|
747
|
-
{
|
|
748
|
-
when: {
|
|
749
|
-
state: { "minor-mode": WORKFLOW_MODE_ID },
|
|
750
|
-
attribution: {
|
|
751
|
-
kind: "minor",
|
|
752
|
-
mode: WORKFLOW_MODE_ID
|
|
753
|
-
}
|
|
754
|
-
},
|
|
755
|
-
name: LAUNCH_TOOL,
|
|
756
|
-
label: "Launch Workflow",
|
|
757
|
-
description: "Start a workflow run. A successful launch is not a completed workflow.",
|
|
758
|
-
promptGuidelines: ["Launch returns when a run starts, not when it finishes. Use the exact run key and separate workspace from its launch result for workflow_run status, not Agiflow project or job identifiers.", "Do not relaunch a healthy running workflow merely because it has not completed."],
|
|
759
|
-
parameters: zod.z.toJSONSchema(feature.runTool.getInputSchema()),
|
|
760
|
-
executionMode: "serial",
|
|
761
|
-
async execute(_toolCallId, parameters, signal, _onUpdate, context) {
|
|
762
|
-
signal?.throwIfAborted();
|
|
763
|
-
const input = parameters;
|
|
764
|
-
return callResult(await launch({
|
|
765
|
-
...input,
|
|
766
|
-
...typeof input.workflowPath === "string" ? { workflowPath: (0, node_path.resolve)(context.cwd, input.workflowPath) } : {}
|
|
767
|
-
}));
|
|
768
|
-
}
|
|
769
|
-
},
|
|
770
|
-
{
|
|
771
|
-
when: {
|
|
772
|
-
state: { "minor-mode": WORKFLOW_MODE_ID },
|
|
773
|
-
attribution: {
|
|
774
|
-
kind: "minor",
|
|
775
|
-
mode: WORKFLOW_MODE_ID
|
|
776
|
-
}
|
|
777
|
-
},
|
|
778
|
-
name: RUN_TOOL,
|
|
779
|
-
label: "Workflow Run",
|
|
780
|
-
description: "Inspect a session-owned workflow by exact run key and separate workspace, or request cooperative control.",
|
|
781
|
-
promptGuidelines: [
|
|
782
|
-
"Use workflow_run status for the recorded stage, outcome, position, jobs, and steps. A successful status lookup is not evidence that the workflow completed.",
|
|
783
|
-
"For a healthy running workflow, summarize the recorded progress. Do not automatically search CLI commands or filesystem logs, relaunch, or diagnose failure merely because it remains running.",
|
|
784
|
-
"Investigate further when the run reports failure or staleness, or when the user requests diagnostics. Only status, pause, resume, and stop are supported here; stop only on user request.",
|
|
785
|
-
"Use exact registry runKey and separate workspace from the launch or status result. Agiflow project and job identifiers are not workflow run keys."
|
|
786
|
-
],
|
|
787
|
-
parameters: {
|
|
788
|
-
type: "object",
|
|
789
|
-
properties: {
|
|
790
|
-
action: {
|
|
791
|
-
type: "string",
|
|
792
|
-
enum: [
|
|
793
|
-
"status",
|
|
794
|
-
"pause",
|
|
795
|
-
"resume",
|
|
796
|
-
"stop"
|
|
797
|
-
]
|
|
798
|
-
},
|
|
799
|
-
runKey: {
|
|
800
|
-
type: "string",
|
|
801
|
-
minLength: 1,
|
|
802
|
-
description: "Exact registry run key from the launch result, without the workspace prefix. Not an Agiflow job ID."
|
|
803
|
-
},
|
|
804
|
-
workspace: {
|
|
805
|
-
type: "string",
|
|
806
|
-
description: "Workspace from the launch result, passed separately from runKey."
|
|
807
|
-
},
|
|
808
|
-
reason: { type: "string" },
|
|
809
|
-
expectedRunId: { type: "string" }
|
|
810
|
-
},
|
|
811
|
-
required: ["action", "runKey"],
|
|
812
|
-
additionalProperties: false
|
|
813
|
-
},
|
|
814
|
-
executionMode: "serial",
|
|
815
|
-
async execute(_toolCallId, parameters, signal) {
|
|
816
|
-
signal?.throwIfAborted();
|
|
817
|
-
const input = parameters;
|
|
818
|
-
const runs = readWorkflowRuns({ environment: host.context.environment }).filter((run) => runBelongsToSession(run, host.context.sessionId) && run.view.runKey === input.runKey && (input.workspace === void 0 || run.view.workspace === input.workspace));
|
|
819
|
-
if (runs.length !== 1) return {
|
|
820
|
-
...callResult({ error: runs.length === 0 ? "No matching workflow run was found in this session. This is a lookup failure, not evidence that the workflow failed. Use the exact runKey and separate workspace from its launch result, not Agiflow job identifiers." : "Specify the workspace for this run key." }),
|
|
821
|
-
isError: true
|
|
822
|
-
};
|
|
823
|
-
const run = runs[0].view;
|
|
824
|
-
if (input.action === "status") return callResult(run);
|
|
825
|
-
if (!control) return callResult({ error: "Workflow activity is not active." });
|
|
826
|
-
if (typeof input.expectedRunId !== "string" || input.expectedRunId.length === 0) return callResult({ error: "expectedRunId is required for workflow control." });
|
|
827
|
-
return callResult(input.action === "pause" ? await control.pause(input.runKey, input.expectedRunId, input.workspace, input.reason) : input.action === "resume" ? await control.resume(input.runKey, input.expectedRunId, input.workspace) : await control.stop(input.runKey, input.expectedRunId, input.workspace, input.reason));
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
],
|
|
831
|
-
commands: [{
|
|
832
|
-
when: {
|
|
833
|
-
state: { "minor-mode": WORKFLOW_MODE_ID },
|
|
834
|
-
attribution: {
|
|
835
|
-
kind: "minor",
|
|
836
|
-
mode: WORKFLOW_MODE_ID
|
|
837
|
-
}
|
|
838
|
-
},
|
|
839
|
-
name: "workflow-launch",
|
|
840
|
-
description: "Launch a workflow from a slash command.",
|
|
841
|
-
async execute(args, execution) {
|
|
842
|
-
const parsed = require_workflowLaunchCommand.parseWorkflowLaunchCommand(args);
|
|
843
|
-
if ("error" in parsed) {
|
|
844
|
-
await execution.client.notify({
|
|
845
|
-
body: parsed.error,
|
|
846
|
-
level: "error"
|
|
847
|
-
});
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
try {
|
|
851
|
-
const entry = require_workflowLaunchCommand.resolveWorkflowEntry(await catalogReader.read(execution.cwd), parsed.workflow);
|
|
852
|
-
if (!entry || entry.error) {
|
|
853
|
-
await execution.client.notify({
|
|
854
|
-
body: entry?.error ?? `Workflow not found: ${parsed.workflow}`,
|
|
855
|
-
level: "error"
|
|
856
|
-
});
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
const problems = require_workflowLaunchCommand.validateWorkflowLaunch(entry, parsed);
|
|
860
|
-
if (problems.length > 0) {
|
|
861
|
-
await execution.client.notify({
|
|
862
|
-
body: problems.join("\n"),
|
|
863
|
-
level: "error"
|
|
864
|
-
});
|
|
865
|
-
return;
|
|
866
|
-
}
|
|
867
|
-
const result = callResult(await launch({
|
|
868
|
-
workflowPath: entry.path,
|
|
869
|
-
...parsed.runner === void 0 ? {} : { runner: parsed.runner },
|
|
870
|
-
...Object.keys(parsed.inputs).length === 0 ? {} : { inputs: parsed.inputs },
|
|
871
|
-
...parsed.prompt === void 0 ? {} : { prompt: parsed.prompt }
|
|
872
|
-
}));
|
|
873
|
-
const rendered = result.content.find((item) => item.type === "text");
|
|
874
|
-
await execution.client.notify({
|
|
875
|
-
body: rendered?.type === "text" ? rendered.text : "Workflow launch requested.",
|
|
876
|
-
level: result.isError ? "error" : "info"
|
|
877
|
-
});
|
|
878
|
-
} catch (error) {
|
|
879
|
-
await execution.client.notify({
|
|
880
|
-
body: String(error),
|
|
881
|
-
level: "error"
|
|
882
|
-
});
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
}],
|
|
886
|
-
onDispose() {
|
|
887
|
-
disposeControl();
|
|
888
|
-
runProvider?.dispose();
|
|
889
|
-
runProvider = void 0;
|
|
1200
|
+
return readLog(target.record, step.ref, options.lines);
|
|
1201
|
+
},
|
|
1202
|
+
async write(target, data) {
|
|
1203
|
+
const step = resolveStep(target);
|
|
1204
|
+
if (step === void 0) return facade.write(target.record, data);
|
|
1205
|
+
if (step.ref.kind !== "pane" || !step.running) throw new Error(PANE_GONE_LINE);
|
|
1206
|
+
if (!await dependencies.paneClient(target.record).input(step.ref.id, data)) throw new Error(PANE_GONE_LINE);
|
|
1207
|
+
},
|
|
1208
|
+
async resize(target, columns, rows) {
|
|
1209
|
+
if (resolveStep(target) !== void 0) return false;
|
|
1210
|
+
return facade.resize(target.record, columns, rows);
|
|
890
1211
|
}
|
|
891
1212
|
};
|
|
892
1213
|
}
|
|
@@ -912,6 +1233,20 @@ function createWorkflowServerRuntime(host, serverHost) {
|
|
|
912
1233
|
*/
|
|
913
1234
|
/** One run, which every route below addresses. */
|
|
914
1235
|
const RUN = "/runs/:workspace/:runKey";
|
|
1236
|
+
/**
|
|
1237
|
+
* The artifact route's parameter, as Hono spells a multi-segment match.
|
|
1238
|
+
*
|
|
1239
|
+
* An artifact is named by its path inside the run directory, so `reports/a.md`
|
|
1240
|
+
* is one parameter across two segments. The client's `params` substitution
|
|
1241
|
+
* fills a single segment and percent-encodes the slash with it, so the page
|
|
1242
|
+
* substitutes this one itself; see the page's `_lib/terminalApi.ts`.
|
|
1243
|
+
*/
|
|
1244
|
+
const ARTIFACT_NAME_PARAM = ":name{.+}";
|
|
1245
|
+
/**
|
|
1246
|
+
* The step whose pane a terminal route reads: a step ref's id. Absent, the
|
|
1247
|
+
* route follows the run's current step, which is one of a parallel group's.
|
|
1248
|
+
*/
|
|
1249
|
+
const STEP_TARGET_PARAM = "step";
|
|
915
1250
|
var apiRoutes_default = (0, _agimon_ai_doompi_core_web.defineApiRoutes)({
|
|
916
1251
|
/**
|
|
917
1252
|
* The terminal, as server-sent events.
|
|
@@ -922,6 +1257,7 @@ var apiRoutes_default = (0, _agimon_ai_doompi_core_web.defineApiRoutes)({
|
|
|
922
1257
|
screen: {
|
|
923
1258
|
method: "GET",
|
|
924
1259
|
path: `${RUN}/screen/stream`,
|
|
1260
|
+
query: [STEP_TARGET_PARAM],
|
|
925
1261
|
stream: true
|
|
926
1262
|
},
|
|
927
1263
|
control: {
|
|
@@ -943,6 +1279,26 @@ var apiRoutes_default = (0, _agimon_ai_doompi_core_web.defineApiRoutes)({
|
|
|
943
1279
|
path: RUN,
|
|
944
1280
|
response: (0, _agimon_ai_doompi_core_web.apiResponse)()
|
|
945
1281
|
},
|
|
1282
|
+
stop: {
|
|
1283
|
+
method: "POST",
|
|
1284
|
+
path: `${RUN}/stop`,
|
|
1285
|
+
response: (0, _agimon_ai_doompi_core_web.apiResponse)()
|
|
1286
|
+
},
|
|
1287
|
+
/** Guidance for the agent session the running step is in, like steering a Team sub-agent. */
|
|
1288
|
+
steer: {
|
|
1289
|
+
method: "POST",
|
|
1290
|
+
path: `${RUN}/steer`,
|
|
1291
|
+
response: (0, _agimon_ai_doompi_core_web.apiResponse)()
|
|
1292
|
+
},
|
|
1293
|
+
/**
|
|
1294
|
+
* Launch a workflow into the mounting session's own runtime, which then owns
|
|
1295
|
+
* the run. A dispatcher session reaches its root's session mount through this.
|
|
1296
|
+
*/
|
|
1297
|
+
launch: {
|
|
1298
|
+
method: "POST",
|
|
1299
|
+
path: "/launch",
|
|
1300
|
+
response: (0, _agimon_ai_doompi_core_web.apiResponse)()
|
|
1301
|
+
},
|
|
946
1302
|
artifacts: {
|
|
947
1303
|
method: "GET",
|
|
948
1304
|
path: `${RUN}/artifacts`,
|
|
@@ -951,7 +1307,7 @@ var apiRoutes_default = (0, _agimon_ai_doompi_core_web.defineApiRoutes)({
|
|
|
951
1307
|
/** One artifact: its metadata and text, or its raw bytes under `raw=1`. */
|
|
952
1308
|
artifact: {
|
|
953
1309
|
method: "GET",
|
|
954
|
-
path: `${RUN}/artifacts
|
|
1310
|
+
path: `${RUN}/artifacts/${ARTIFACT_NAME_PARAM}`,
|
|
955
1311
|
query: ["raw", "download"],
|
|
956
1312
|
response: (0, _agimon_ai_doompi_core_web.apiResponse)()
|
|
957
1313
|
}
|
|
@@ -991,6 +1347,8 @@ const STREAM_TICK_MS = 500;
|
|
|
991
1347
|
const SETTLED_POLL_TICKS = 4;
|
|
992
1348
|
/** Bytes of one textual artifact returned as JSON; binary previews use a stream. */
|
|
993
1349
|
const MAX_ARTIFACT_BYTES = 524288;
|
|
1350
|
+
/** Recorded with a stop the web panel asks for, so the run log says where it came from. */
|
|
1351
|
+
const WEB_STOP_REASON = "Stopped from the web workflow panel.";
|
|
994
1352
|
/** Path segments a client supplies are names, never paths. */
|
|
995
1353
|
const SAFE_SEGMENT = /^[\w.@-]+$/;
|
|
996
1354
|
const ARTIFACT_MIME_TYPES = {
|
|
@@ -1007,6 +1365,7 @@ const ARTIFACT_MIME_TYPES = {
|
|
|
1007
1365
|
".js": "text/javascript",
|
|
1008
1366
|
".json": "application/json",
|
|
1009
1367
|
".jsx": "text/jsx",
|
|
1368
|
+
".log": "text/plain",
|
|
1010
1369
|
".m4a": "audio/mp4",
|
|
1011
1370
|
".markdown": "text/markdown",
|
|
1012
1371
|
".md": "text/markdown",
|
|
@@ -1047,9 +1406,16 @@ async function jsonBody(context) {
|
|
|
1047
1406
|
function isSafeSegment(value) {
|
|
1048
1407
|
return value !== "" && value !== "../adapters" && value !== ".." && SAFE_SEGMENT.test(value);
|
|
1049
1408
|
}
|
|
1050
|
-
/**
|
|
1051
|
-
|
|
1052
|
-
|
|
1409
|
+
/**
|
|
1410
|
+
* One terminal's identity for caches and leases, stable across stage moves: a
|
|
1411
|
+
* run's, or one step's pane in it, which keeps its own screen and keyboard.
|
|
1412
|
+
*/
|
|
1413
|
+
function runIdentityOf(workspace, runKey, step) {
|
|
1414
|
+
return step === void 0 ? `${workspace}/${runKey}` : `${workspace}/${runKey}#${step}`;
|
|
1415
|
+
}
|
|
1416
|
+
/** The step a terminal request names, from its query or body; absent follows the current step. */
|
|
1417
|
+
function stepOf(value) {
|
|
1418
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
1053
1419
|
}
|
|
1054
1420
|
/**
|
|
1055
1421
|
* This package's HTTP surface: one workflow run's terminal, and the files its
|
|
@@ -1063,8 +1429,9 @@ function runIdentityOf(workspace, runKey) {
|
|
|
1063
1429
|
function createWorkflowHubApi(options = {}) {
|
|
1064
1430
|
const registry = options.registry ?? new _agimon_ai_workflow_mcp.WorkflowRegistryService();
|
|
1065
1431
|
const facade = options.terminal ?? new _agimon_ai_workflow_mcp.WorkflowTerminalService();
|
|
1432
|
+
const stepPanes = createStepPaneTerminalDependencies(registry);
|
|
1066
1433
|
const terminal = require_workflowTerminal.createWorkflowTerminalService({
|
|
1067
|
-
terminal: facade,
|
|
1434
|
+
terminal: createStepPaneTerminal(facade, stepPanes),
|
|
1068
1435
|
now: options.now ?? (() => Date.now())
|
|
1069
1436
|
});
|
|
1070
1437
|
const app = new hono.Hono();
|
|
@@ -1081,17 +1448,24 @@ function createWorkflowHubApi(options = {}) {
|
|
|
1081
1448
|
const runKey = context.req.param("runKey");
|
|
1082
1449
|
const record = await runOf(workspace, runKey);
|
|
1083
1450
|
if (record === void 0) return context.json(missing(workspace, runKey), 404);
|
|
1084
|
-
const
|
|
1451
|
+
const step = stepOf(context.req.query(STEP_TARGET_PARAM));
|
|
1452
|
+
const identity = runIdentityOf(workspace, runKey, step);
|
|
1085
1453
|
return (0, hono_streaming.streamSSE)(context, async (stream) => {
|
|
1086
1454
|
let settledTicks = 0;
|
|
1087
1455
|
let running = true;
|
|
1456
|
+
let latest = record;
|
|
1088
1457
|
stream.onAbort(() => {
|
|
1089
1458
|
running = false;
|
|
1090
1459
|
});
|
|
1091
1460
|
while (running) {
|
|
1092
1461
|
const current = await runOf(workspace, runKey) ?? record;
|
|
1093
|
-
|
|
1094
|
-
const
|
|
1462
|
+
latest = current;
|
|
1463
|
+
const target = {
|
|
1464
|
+
record: current,
|
|
1465
|
+
...step === void 0 ? {} : { step }
|
|
1466
|
+
};
|
|
1467
|
+
const capabilities = terminal.capabilities(target);
|
|
1468
|
+
const lines = capabilities.readable ? await terminal.screen(identity, target, SCREEN_LINES) : [];
|
|
1095
1469
|
const settled = current.stage !== "running";
|
|
1096
1470
|
if (settled) settledTicks += 1;
|
|
1097
1471
|
const ended = settled && settledTicks >= SETTLED_POLL_TICKS;
|
|
@@ -1107,7 +1481,7 @@ function createWorkflowHubApi(options = {}) {
|
|
|
1107
1481
|
if (ended) break;
|
|
1108
1482
|
await stream.sleep(STREAM_TICK_MS);
|
|
1109
1483
|
}
|
|
1110
|
-
|
|
1484
|
+
if (latest.stage !== "running") terminal.forgetRun(identity);
|
|
1111
1485
|
});
|
|
1112
1486
|
});
|
|
1113
1487
|
app.post(apiRoutes_default.control.path, async (context) => {
|
|
@@ -1115,9 +1489,13 @@ function createWorkflowHubApi(options = {}) {
|
|
|
1115
1489
|
const runKey = context.req.param("runKey");
|
|
1116
1490
|
const record = await runOf(workspace, runKey);
|
|
1117
1491
|
if (record === void 0) return context.json(missing(workspace, runKey), 404);
|
|
1118
|
-
const identity = runIdentityOf(workspace, runKey);
|
|
1119
1492
|
const body = await jsonBody(context);
|
|
1120
|
-
const
|
|
1493
|
+
const step = stepOf(body.step);
|
|
1494
|
+
const identity = runIdentityOf(workspace, runKey, step);
|
|
1495
|
+
const capabilities = terminal.capabilities({
|
|
1496
|
+
record,
|
|
1497
|
+
...step === void 0 ? {} : { step }
|
|
1498
|
+
});
|
|
1121
1499
|
if (body.release === true) {
|
|
1122
1500
|
if (typeof body.token === "string") terminal.releaseControl(identity, body.token);
|
|
1123
1501
|
return context.json({ held: false });
|
|
@@ -1147,8 +1525,12 @@ function createWorkflowHubApi(options = {}) {
|
|
|
1147
1525
|
if (record === void 0) return context.json(missing(workspace, runKey), 404);
|
|
1148
1526
|
const body = await jsonBody(context);
|
|
1149
1527
|
if (typeof body.token !== "string" || typeof body.data !== "string") return context.json({ error: "A keystroke needs a control token and its data." }, 400);
|
|
1528
|
+
const step = stepOf(body.step);
|
|
1150
1529
|
try {
|
|
1151
|
-
await terminal.write(runIdentityOf(workspace, runKey
|
|
1530
|
+
await terminal.write(runIdentityOf(workspace, runKey, step), {
|
|
1531
|
+
record,
|
|
1532
|
+
...step === void 0 ? {} : { step }
|
|
1533
|
+
}, body.token, body.data);
|
|
1152
1534
|
} catch (error) {
|
|
1153
1535
|
return context.json({ error: error instanceof Error ? error.message : String(error) }, 409);
|
|
1154
1536
|
}
|
|
@@ -1159,26 +1541,72 @@ function createWorkflowHubApi(options = {}) {
|
|
|
1159
1541
|
const runKey = context.req.param("runKey");
|
|
1160
1542
|
const record = await runOf(workspace, runKey);
|
|
1161
1543
|
if (record === void 0) return context.json(missing(workspace, runKey), 404);
|
|
1162
|
-
const
|
|
1544
|
+
const body = await jsonBody(context);
|
|
1545
|
+
const { token, columns, rows } = body;
|
|
1163
1546
|
if (typeof token !== "string" || typeof columns !== "number" || typeof rows !== "number") return context.json({ error: "A resize needs a control token, columns and rows." }, 400);
|
|
1547
|
+
const step = stepOf(body.step);
|
|
1164
1548
|
try {
|
|
1165
|
-
const resized = await terminal.resize(runIdentityOf(workspace, runKey
|
|
1549
|
+
const resized = await terminal.resize(runIdentityOf(workspace, runKey, step), {
|
|
1550
|
+
record,
|
|
1551
|
+
...step === void 0 ? {} : { step }
|
|
1552
|
+
}, token, columns, rows);
|
|
1166
1553
|
return context.json({ resized });
|
|
1167
1554
|
} catch (error) {
|
|
1168
1555
|
return context.json({ error: error instanceof Error ? error.message : String(error) }, 409);
|
|
1169
1556
|
}
|
|
1170
1557
|
});
|
|
1558
|
+
app.post(apiRoutes_default.stop.path, async (context) => {
|
|
1559
|
+
const workspace = context.req.param("workspace");
|
|
1560
|
+
const runKey = context.req.param("runKey");
|
|
1561
|
+
const record = await runOf(workspace, runKey);
|
|
1562
|
+
if (record === void 0) return context.json(missing(workspace, runKey), 404);
|
|
1563
|
+
if (record.stage !== "running" || record.runId === void 0) return context.json({ error: "Only a running workflow can be stopped." }, 409);
|
|
1564
|
+
try {
|
|
1565
|
+
await registry.requestStop(workspace, runKey, WEB_STOP_REASON, record.runId);
|
|
1566
|
+
} catch (error) {
|
|
1567
|
+
return context.json({ error: error instanceof Error ? error.message : String(error) }, 409);
|
|
1568
|
+
}
|
|
1569
|
+
return context.json({ requested: true });
|
|
1570
|
+
});
|
|
1571
|
+
app.post(apiRoutes_default.steer.path, async (context) => {
|
|
1572
|
+
const workspace = context.req.param("workspace");
|
|
1573
|
+
const runKey = context.req.param("runKey");
|
|
1574
|
+
const record = await runOf(workspace, runKey);
|
|
1575
|
+
if (record === void 0) return context.json(missing(workspace, runKey), 404);
|
|
1576
|
+
const body = await jsonBody(context);
|
|
1577
|
+
const message = typeof body.message === "string" ? body.message.trim() : "";
|
|
1578
|
+
if (message === "") return context.json({ error: "Guidance needs a message." }, 400);
|
|
1579
|
+
if (record.stage !== "running") return context.json({ error: "Only a running workflow can be steered." }, 409);
|
|
1580
|
+
const refs = stepPanes.stepRefs(record);
|
|
1581
|
+
const step = typeof body.step === "string" ? body.step : void 0;
|
|
1582
|
+
const current = step === void 0 ? refs.current : (refs.running ?? []).find((candidate) => candidate.id === step);
|
|
1583
|
+
if (current?.kind !== "session") return context.json({ error: "The running step is not an agent session." }, 409);
|
|
1584
|
+
if (options.steer === void 0) return context.json({ error: "Steering is available from the session that launched this run." }, 409);
|
|
1585
|
+
try {
|
|
1586
|
+
await options.steer(current.id, message);
|
|
1587
|
+
} catch (error) {
|
|
1588
|
+
return context.json({ error: error instanceof Error ? error.message : String(error) }, 409);
|
|
1589
|
+
}
|
|
1590
|
+
return context.json({ delivered: true });
|
|
1591
|
+
});
|
|
1592
|
+
app.post(apiRoutes_default.launch.path, async (context) => {
|
|
1593
|
+
if (options.launch === void 0) return context.json({ error: "Launching needs the mount of a session with a workflow runtime." }, 409);
|
|
1594
|
+
const response = await options.launch(await jsonBody(context));
|
|
1595
|
+
return context.json(response);
|
|
1596
|
+
});
|
|
1171
1597
|
app.delete(apiRoutes_default.remove.path, async (context) => {
|
|
1172
1598
|
const workspace = context.req.param("workspace");
|
|
1173
1599
|
const runKey = context.req.param("runKey");
|
|
1174
1600
|
const record = await runOf(workspace, runKey);
|
|
1175
1601
|
if (record === void 0) return context.json(missing(workspace, runKey), 404);
|
|
1176
1602
|
if (record.stage === "running") return context.json({ error: "A running workflow must be stopped before it can be deleted." }, 409);
|
|
1603
|
+
const refs = stepPanes.stepRefs(record);
|
|
1177
1604
|
try {
|
|
1178
1605
|
node_fs.default.rmSync(registry.runDirectoryFor(record), {
|
|
1179
1606
|
recursive: true,
|
|
1180
1607
|
force: true
|
|
1181
1608
|
});
|
|
1609
|
+
await removeStepPaneLogs(refs);
|
|
1182
1610
|
} catch (error) {
|
|
1183
1611
|
return context.json({ error: error instanceof Error ? error.message : String(error) }, 409);
|
|
1184
1612
|
}
|
|
@@ -1305,80 +1733,756 @@ function readArtifact(resolved, requested) {
|
|
|
1305
1733
|
} finally {
|
|
1306
1734
|
node_fs.default.closeSync(handle);
|
|
1307
1735
|
}
|
|
1308
|
-
} catch {
|
|
1309
|
-
return;
|
|
1310
|
-
}
|
|
1311
|
-
return {
|
|
1312
|
-
path: requested,
|
|
1313
|
-
size: stats.size,
|
|
1314
|
-
modifiedAt: stats.mtime.toISOString(),
|
|
1315
|
-
mimeType,
|
|
1316
|
-
...text === void 0 ? {} : { text },
|
|
1317
|
-
truncated: text !== void 0 && stats.size > MAX_ARTIFACT_BYTES
|
|
1736
|
+
} catch {
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
return {
|
|
1740
|
+
path: requested,
|
|
1741
|
+
size: stats.size,
|
|
1742
|
+
modifiedAt: stats.mtime.toISOString(),
|
|
1743
|
+
mimeType,
|
|
1744
|
+
...text === void 0 ? {} : { text },
|
|
1745
|
+
truncated: text !== void 0 && stats.size > MAX_ARTIFACT_BYTES
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
/** Streams media with byte-range support so browser audio, video, and PDF controls can seek. */
|
|
1749
|
+
function streamArtifact(resolved, requested, rangeHeader, download) {
|
|
1750
|
+
const stats = statOf(resolved);
|
|
1751
|
+
if (stats === void 0 || stats.isDirectory()) return void 0;
|
|
1752
|
+
const mimeType = artifactMimeType(requested);
|
|
1753
|
+
let start = 0;
|
|
1754
|
+
let end = Math.max(0, stats.size - 1);
|
|
1755
|
+
let partial = false;
|
|
1756
|
+
const match = rangeHeader?.match(/^bytes=(\d*)-(\d*)$/);
|
|
1757
|
+
if (match !== null && match !== void 0 && stats.size > 0) {
|
|
1758
|
+
const requestedStart = match[1] === "" ? void 0 : Number(match[1]);
|
|
1759
|
+
const requestedEnd = match[2] === "" ? void 0 : Number(match[2]);
|
|
1760
|
+
if (requestedStart === void 0 && requestedEnd !== void 0) start = Math.max(0, stats.size - requestedEnd);
|
|
1761
|
+
else if (requestedStart !== void 0) {
|
|
1762
|
+
start = requestedStart;
|
|
1763
|
+
end = requestedEnd === void 0 ? end : Math.min(requestedEnd, end);
|
|
1764
|
+
}
|
|
1765
|
+
if (start >= stats.size || start > end) return new Response(null, {
|
|
1766
|
+
status: 416,
|
|
1767
|
+
headers: { "content-range": `bytes */${String(stats.size)}` }
|
|
1768
|
+
});
|
|
1769
|
+
partial = true;
|
|
1770
|
+
}
|
|
1771
|
+
const filename = node_path.default.basename(requested).replace(/["\\\r\n]/g, "_");
|
|
1772
|
+
const headers = new Headers({
|
|
1773
|
+
"accept-ranges": "bytes",
|
|
1774
|
+
"content-disposition": `${download || mimeType === "text/html" ? "attachment" : "inline"}; filename="${filename}"`,
|
|
1775
|
+
"content-length": String(stats.size === 0 ? 0 : end - start + 1),
|
|
1776
|
+
"content-type": mimeType,
|
|
1777
|
+
"x-content-type-options": "nosniff"
|
|
1778
|
+
});
|
|
1779
|
+
if (partial) headers.set("content-range", `bytes ${String(start)}-${String(end)}/${String(stats.size)}`);
|
|
1780
|
+
if (stats.size === 0) return new Response(null, {
|
|
1781
|
+
status: 200,
|
|
1782
|
+
headers
|
|
1783
|
+
});
|
|
1784
|
+
const body = node_stream.Readable.toWeb(node_fs.default.createReadStream(resolved, {
|
|
1785
|
+
start,
|
|
1786
|
+
end
|
|
1787
|
+
}));
|
|
1788
|
+
return new Response(body, {
|
|
1789
|
+
status: partial ? 206 : 200,
|
|
1790
|
+
headers
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* This package's API, mounted by a session with its runtime's `launch` and by
|
|
1795
|
+
* the hub without one.
|
|
1796
|
+
*/
|
|
1797
|
+
function createWorkflowApi(launch) {
|
|
1798
|
+
return {
|
|
1799
|
+
basePath: WORKFLOW_API_BASE_PATH,
|
|
1800
|
+
start(context) {
|
|
1801
|
+
const steer = context.sessionService?.steer?.bind(context.sessionService);
|
|
1802
|
+
const app = createWorkflowHubApi({
|
|
1803
|
+
...steer === void 0 ? {} : { steer: (sessionId, message) => steer(sessionId, message) },
|
|
1804
|
+
...launch === void 0 ? {} : { launch }
|
|
1805
|
+
});
|
|
1806
|
+
return {
|
|
1807
|
+
fetch: (request) => app.fetch(request),
|
|
1808
|
+
close: () => void 0
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
/** The named export a host imports from this package's built hub entry. */
|
|
1814
|
+
const api = createWorkflowApi();
|
|
1815
|
+
//#endregion
|
|
1816
|
+
//#region src/services/workflowWatcher/index.ts
|
|
1817
|
+
/**
|
|
1818
|
+
* Read the durable workflow registry once.
|
|
1819
|
+
*
|
|
1820
|
+
* The registry is an initial snapshot only. Live delivery belongs to the
|
|
1821
|
+
* session lifecycle and is routed through the host-owned direct event bus.
|
|
1822
|
+
*/
|
|
1823
|
+
function readWorkflowRuns(options = {}) {
|
|
1824
|
+
const environment = options.environment;
|
|
1825
|
+
const homeDir = options.homeDir ?? resolveWorkflowHome({
|
|
1826
|
+
envValue: environment?.["WORKFLOW_MCP_HOME"],
|
|
1827
|
+
homeDir: node_os.default.homedir()
|
|
1828
|
+
});
|
|
1829
|
+
const workspacesDir = node_path.default.join(homeDir, WORKSPACES_DIR_NAME);
|
|
1830
|
+
const recordCache = /* @__PURE__ */ new Map();
|
|
1831
|
+
const progressCache = /* @__PURE__ */ new Map();
|
|
1832
|
+
const cachedParse = (cache, filePath, parse, empty) => {
|
|
1833
|
+
let stat;
|
|
1834
|
+
try {
|
|
1835
|
+
stat = node_fs.default.statSync(filePath);
|
|
1836
|
+
} catch {
|
|
1837
|
+
cache.delete(filePath);
|
|
1838
|
+
return empty;
|
|
1839
|
+
}
|
|
1840
|
+
const cached = cache.get(filePath);
|
|
1841
|
+
if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) return cached.value;
|
|
1842
|
+
let raw;
|
|
1843
|
+
try {
|
|
1844
|
+
raw = node_fs.default.readFileSync(filePath, "utf8");
|
|
1845
|
+
} catch {
|
|
1846
|
+
cache.delete(filePath);
|
|
1847
|
+
return empty;
|
|
1848
|
+
}
|
|
1849
|
+
const value = parse(raw);
|
|
1850
|
+
cache.set(filePath, {
|
|
1851
|
+
size: stat.size,
|
|
1852
|
+
mtimeMs: stat.mtimeMs,
|
|
1853
|
+
value
|
|
1854
|
+
});
|
|
1855
|
+
return value;
|
|
1856
|
+
};
|
|
1857
|
+
const listDir = (directory) => {
|
|
1858
|
+
try {
|
|
1859
|
+
return node_fs.default.readdirSync(directory).sort();
|
|
1860
|
+
} catch {
|
|
1861
|
+
return [];
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1864
|
+
const runs = [];
|
|
1865
|
+
for (const workspace of listDir(workspacesDir)) for (const stage of WORKFLOW_STAGES) {
|
|
1866
|
+
const stageDir = node_path.default.join(workspacesDir, workspace, stage);
|
|
1867
|
+
for (const runKey of listDir(stageDir)) {
|
|
1868
|
+
const runDir = node_path.default.join(stageDir, runKey);
|
|
1869
|
+
const recordPath = node_path.default.join(runDir, RUN_RECORD_FILE_NAME);
|
|
1870
|
+
const progressPath = node_path.default.join(runDir, PROGRESS_FILE_NAME);
|
|
1871
|
+
const parsed = cachedParse(recordCache, recordPath, parseWorkflowRunRecord, void 0);
|
|
1872
|
+
if (parsed === void 0) continue;
|
|
1873
|
+
const jobs = cachedParse(progressCache, progressPath, (raw) => foldWorkflowProgress(parseWorkflowProgress(raw)), []);
|
|
1874
|
+
runs.push({
|
|
1875
|
+
...parsed,
|
|
1876
|
+
view: completeWorkflowRunView(parsed.view, jobs)
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
return runs;
|
|
1881
|
+
}
|
|
1882
|
+
//#endregion
|
|
1883
|
+
//#region src/services/serverRuntime/index.ts
|
|
1884
|
+
const SOURCE = "@agimon-ai/doompi-workflow";
|
|
1885
|
+
const WORKFLOW_MODE_ID = "workflow";
|
|
1886
|
+
const LIST_TOOL = "list_workflows";
|
|
1887
|
+
const LAUNCH_TOOL = "launch_workflow";
|
|
1888
|
+
const RUN_TOOL = "workflow_run";
|
|
1889
|
+
/** Origin of a request dispatched in process to another mount; nothing resolves it. */
|
|
1890
|
+
const IN_PROCESS_ORIGIN = "http://doompi.local";
|
|
1891
|
+
/** A burst of job and step events is published once, this long after the first. */
|
|
1892
|
+
const RUNS_PUBLISH_DEBOUNCE_MS = 100;
|
|
1893
|
+
/** A notification body's limit; a longer failure is cut to it. */
|
|
1894
|
+
const NOTICE_BODY_LIMIT = 4e3;
|
|
1895
|
+
/**
|
|
1896
|
+
* A launch refused because the workflow needs fixing. The doctor's findings
|
|
1897
|
+
* name only a job and a step, so the notice names the workflow and says
|
|
1898
|
+
* nothing ran; in a conversation it otherwise reads as part of whatever run
|
|
1899
|
+
* was reported just before it.
|
|
1900
|
+
*/
|
|
1901
|
+
function needsFixingNotice(workflow, error) {
|
|
1902
|
+
return `Workflow "${workflow}" was not launched: it needs fixing.\n${error}`;
|
|
1903
|
+
}
|
|
1904
|
+
function textResult(text, isError = false) {
|
|
1905
|
+
return {
|
|
1906
|
+
content: [{
|
|
1907
|
+
type: "text",
|
|
1908
|
+
text
|
|
1909
|
+
}],
|
|
1910
|
+
...isError ? { isError: true } : {}
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
function isLaunchResponse(value) {
|
|
1914
|
+
return typeof value === "object" && value !== null && "text" in value && typeof value.text === "string";
|
|
1915
|
+
}
|
|
1916
|
+
function errorOf(value) {
|
|
1917
|
+
return typeof value === "object" && value !== null && "error" in value && typeof value.error === "string" ? value.error : void 0;
|
|
1918
|
+
}
|
|
1919
|
+
function errorMessage(error) {
|
|
1920
|
+
return error instanceof Error ? error.message : String(error);
|
|
1921
|
+
}
|
|
1922
|
+
/** The text of a launch's tool result, as the launch tool and the API report it. */
|
|
1923
|
+
function launchResultOf(result) {
|
|
1924
|
+
return textResult(result.content.map((item) => item.type === "text" ? item.text : "").filter((line) => line !== "").join("\n") || "Workflow launch requested.", result.isError === true);
|
|
1925
|
+
}
|
|
1926
|
+
function callResult(value) {
|
|
1927
|
+
const content = [];
|
|
1928
|
+
if (typeof value === "object" && value !== null && "content" in value && Array.isArray(value.content)) for (const item of value.content) {
|
|
1929
|
+
if (typeof item !== "object" || item === null || !("type" in item)) continue;
|
|
1930
|
+
const record = item;
|
|
1931
|
+
if (record.type === "text" && typeof record.text === "string") content.push({
|
|
1932
|
+
type: "text",
|
|
1933
|
+
text: record.text
|
|
1934
|
+
});
|
|
1935
|
+
else if (record.type === "image" && typeof record.data === "string" && typeof record.mimeType === "string") content.push({
|
|
1936
|
+
type: "image",
|
|
1937
|
+
data: record.data,
|
|
1938
|
+
mimeType: record.mimeType
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
if (content.length === 0) content.push({
|
|
1942
|
+
type: "text",
|
|
1943
|
+
text: JSON.stringify(value, null, 2)
|
|
1944
|
+
});
|
|
1945
|
+
return {
|
|
1946
|
+
content,
|
|
1947
|
+
details: value,
|
|
1948
|
+
...typeof value === "object" && value !== null && "isError" in value && value.isError === true ? { isError: true } : {}
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
function createWorkflowServerRuntime(host, serverHost) {
|
|
1952
|
+
if (serverHost.context.directEvents === void 0) throw new Error("Workflow headless facet requires the session direct event bus.");
|
|
1953
|
+
const directEvents = serverHost.context.directEvents;
|
|
1954
|
+
let runProvider;
|
|
1955
|
+
const catalogDeps = defaultCatalogDeps();
|
|
1956
|
+
const catalogReader = createWorkflowCatalogReader(catalogDeps);
|
|
1957
|
+
const telemetry = (0, _agimon_ai_doompi_telemetry.createDoomTelemetry)({
|
|
1958
|
+
serviceName: "doom-workflow",
|
|
1959
|
+
packageName: SOURCE,
|
|
1960
|
+
cwd: host.context.cwd,
|
|
1961
|
+
env: host.context.environment,
|
|
1962
|
+
enableLogs: true,
|
|
1963
|
+
enableTraces: true
|
|
1964
|
+
});
|
|
1965
|
+
/** This session's runs, published to its panel and counted as its background work. */
|
|
1966
|
+
const publishRuns = (executionContext) => {
|
|
1967
|
+
const records = readWorkflowRuns({ environment: executionContext.environment }).filter((run) => runBelongsToSession(run, executionContext.sessionId));
|
|
1968
|
+
const activeItems = records.filter((run) => run.view.stage === "running" && run.view.stale !== true).map((run) => ({
|
|
1969
|
+
id: `${run.view.workspace}/${run.view.runKey}`,
|
|
1970
|
+
sessionId: executionContext.sessionId
|
|
1971
|
+
}));
|
|
1972
|
+
runProvider?.update(activeItems);
|
|
1973
|
+
const runs = presentWorkflowRuns(records.map((run) => run.view), Date.now());
|
|
1974
|
+
directEvents.publish(WORKFLOW_RUNS_TYPE, executionContext.sessionId, { runs });
|
|
1975
|
+
return activeItems.length > 0;
|
|
1976
|
+
};
|
|
1977
|
+
/** The workflows this session can launch. Runs moving on do not change them, so their events skip this. */
|
|
1978
|
+
const publishCatalog = async (executionContext) => {
|
|
1979
|
+
try {
|
|
1980
|
+
directEvents.publish(WORKFLOW_CATALOG_TYPE, executionContext.sessionId, {
|
|
1981
|
+
cwd: executionContext.cwd,
|
|
1982
|
+
workflows: presentWorkflowCatalog(await catalogReader.read(executionContext.cwd))
|
|
1983
|
+
});
|
|
1984
|
+
} catch (error) {
|
|
1985
|
+
const warning = error instanceof Error ? error.message : String(error);
|
|
1986
|
+
directEvents.publish(WORKFLOW_CATALOG_TYPE, executionContext.sessionId, {
|
|
1987
|
+
cwd: executionContext.cwd,
|
|
1988
|
+
workflows: [],
|
|
1989
|
+
warning
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
};
|
|
1993
|
+
const publishLifecycle = async (executionContext) => {
|
|
1994
|
+
const active = publishRuns(executionContext);
|
|
1995
|
+
await publishCatalog(executionContext);
|
|
1996
|
+
return active;
|
|
1997
|
+
};
|
|
1998
|
+
let runsTimer;
|
|
1999
|
+
/** Publishes the runs once for a burst of job and step events, which a fix loop can produce by the dozen. */
|
|
2000
|
+
const publishRunsSoon = (executionContext, after) => {
|
|
2001
|
+
if (runsTimer !== void 0) return;
|
|
2002
|
+
runsTimer = setTimeout(() => {
|
|
2003
|
+
runsTimer = void 0;
|
|
2004
|
+
try {
|
|
2005
|
+
after?.(publishRuns(executionContext));
|
|
2006
|
+
} catch (error) {
|
|
2007
|
+
telemetry.recordWarning("doom_workflow.publish_failed", error);
|
|
2008
|
+
}
|
|
2009
|
+
}, RUNS_PUBLISH_DEBOUNCE_MS);
|
|
2010
|
+
runsTimer.unref?.();
|
|
2011
|
+
};
|
|
2012
|
+
const sessionService = serverHost.context.sessionService;
|
|
2013
|
+
const feature = (0, _agimon_ai_workflow_mcp.createEmbeddedWorkflowFeature)(sessionService === void 0 ? {} : { stepExecutor: createStepExecutor({
|
|
2014
|
+
sessionService,
|
|
2015
|
+
parentSessionId: host.context.sessionId,
|
|
2016
|
+
hostEnvironment: process.env,
|
|
2017
|
+
launchPane: createNativeStepPaneLauncher(void 0, telemetry),
|
|
2018
|
+
createId: () => `workflow-${(0, node_crypto.randomUUID)()}`,
|
|
2019
|
+
telemetry
|
|
2020
|
+
}) });
|
|
2021
|
+
const launcher = createServerLauncher({
|
|
2022
|
+
feature,
|
|
2023
|
+
sessionId: host.context.sessionId,
|
|
2024
|
+
environment: host.context.environment,
|
|
2025
|
+
telemetry,
|
|
2026
|
+
notify: (body, level) => host.context.client.notify({
|
|
2027
|
+
body: body.slice(0, NOTICE_BODY_LIMIT),
|
|
2028
|
+
level
|
|
2029
|
+
}),
|
|
2030
|
+
onRunsChanged: () => publishRunsSoon(host.context)
|
|
2031
|
+
});
|
|
2032
|
+
/**
|
|
2033
|
+
* Why this session cannot run a launch, or undefined when it can.
|
|
2034
|
+
*
|
|
2035
|
+
* The checks the slash command makes through the catalog, made for every
|
|
2036
|
+
* launch whatever asked for it: the launch tool, another session, or the
|
|
2037
|
+
* browser. The workflow must be a file in the repository the launch comes
|
|
2038
|
+
* from, a worktree's own when it runs in one, and its runner, command,
|
|
2039
|
+
* choice, inputs and prompt must be ones it declares.
|
|
2040
|
+
*/
|
|
2041
|
+
const refuseLaunch = (input, where) => {
|
|
2042
|
+
if (typeof input.workflowPath !== "string" || input.workflowPath === "") return "A launch needs a workflowPath.";
|
|
2043
|
+
const workflowPath = (0, node_path.resolve)(where.cwd, input.workflowPath);
|
|
2044
|
+
const inside = (0, node_path.relative)(where.repoRoot, workflowPath);
|
|
2045
|
+
if (inside.startsWith("..") || (0, node_path.isAbsolute)(inside)) return `Workflow ${input.workflowPath} is outside this session's repository, so it cannot be launched here.`;
|
|
2046
|
+
const detail = catalogDeps.summarize(workflowPath);
|
|
2047
|
+
if (detail.error !== void 0) return needsFixingNotice(inside, detail.error);
|
|
2048
|
+
const problems = require_workflowLaunchCommand.validateWorkflowLaunch(detail, {
|
|
2049
|
+
inputs: input.inputs ?? {},
|
|
2050
|
+
...input.prompt === void 0 ? {} : { prompt: input.prompt },
|
|
2051
|
+
...(input.runner ?? input.cliAgent) === void 0 ? {} : { runner: input.runner ?? input.cliAgent },
|
|
2052
|
+
...input.command === void 0 ? {} : { command: input.command },
|
|
2053
|
+
...input.choice === void 0 ? {} : { choice: input.choice }
|
|
2054
|
+
});
|
|
2055
|
+
return problems.length === 0 ? void 0 : problems.join("\n");
|
|
2056
|
+
};
|
|
2057
|
+
/**
|
|
2058
|
+
* Run a workflow in this process and answer once it has registered.
|
|
2059
|
+
*
|
|
2060
|
+
* The launch goes through the same executor the Pi extension uses; see
|
|
2061
|
+
* `createServerLauncher` for what the server adds around it.
|
|
2062
|
+
*/
|
|
2063
|
+
const launchHere = async (parameters, where = host.context) => {
|
|
2064
|
+
let input;
|
|
2065
|
+
try {
|
|
2066
|
+
input = feature.runTool.getInputSchema().parse(parameters);
|
|
2067
|
+
} catch (error) {
|
|
2068
|
+
return textResult(`Error: ${errorMessage(error)}`, true);
|
|
2069
|
+
}
|
|
2070
|
+
const refusal = refuseLaunch(input, where);
|
|
2071
|
+
if (refusal !== void 0) return textResult(`Error: ${refusal}`, true);
|
|
2072
|
+
try {
|
|
2073
|
+
return launchResultOf(await launcher.launch({
|
|
2074
|
+
...input,
|
|
2075
|
+
workflowPath: (0, node_path.resolve)(where.cwd, input.workflowPath)
|
|
2076
|
+
}));
|
|
2077
|
+
} catch (error) {
|
|
2078
|
+
return textResult(`Error: ${errorMessage(error)}`, true);
|
|
2079
|
+
}
|
|
2080
|
+
};
|
|
2081
|
+
/**
|
|
2082
|
+
* Hand a launch to the session that owns this session's runs, through that
|
|
2083
|
+
* session's own mount of this package's API. The owner runs it, so the run
|
|
2084
|
+
* and its step sessions outlive this session, as a dispatcher's launches must.
|
|
2085
|
+
*/
|
|
2086
|
+
const launchIn = async (owner, parameters) => {
|
|
2087
|
+
if (serverHost.context.requestApi === void 0) return textResult(`Error: this host cannot reach session ${owner} to launch the workflow there.`, true);
|
|
2088
|
+
const response = await serverHost.context.requestApi({
|
|
2089
|
+
scope: "session",
|
|
2090
|
+
sessionId: owner
|
|
2091
|
+
}, WORKFLOW_API_BASE_PATH, new Request(`${IN_PROCESS_ORIGIN}${apiRoutes_default.launch.path}`, {
|
|
2092
|
+
method: "POST",
|
|
2093
|
+
headers: { "content-type": "application/json" },
|
|
2094
|
+
body: JSON.stringify(parameters)
|
|
2095
|
+
}));
|
|
2096
|
+
const body = await response.json().catch((error) => ({ error: String(error) }));
|
|
2097
|
+
if (response.ok && isLaunchResponse(body)) return textResult(body.text, body.isError === true);
|
|
2098
|
+
return textResult(`Error: session ${owner} did not launch the workflow (${errorOf(body) ?? `HTTP ${String(response.status)}`}).`, true);
|
|
2099
|
+
};
|
|
2100
|
+
/**
|
|
2101
|
+
* A workflow dispatcher launches for its root session, the way a CLI
|
|
2102
|
+
* dispatcher child does: the same markers name it, and the root runs what
|
|
2103
|
+
* it launches. Any other session runs its own launches.
|
|
2104
|
+
*/
|
|
2105
|
+
const dispatcher = require_workflowTerminal.isWorkflowDispatcherProcess(host.context.environment);
|
|
2106
|
+
const launchOwner = () => {
|
|
2107
|
+
if (!dispatcher) return void 0;
|
|
2108
|
+
const parent = require_workflowTerminal.resolveDispatcherParentSession(host.context.environment);
|
|
2109
|
+
if (parent === void 0) return void 0;
|
|
2110
|
+
const owner = (0, _agimon_ai_doompi_core_childProcess.resolveRootSessionId)(parent, host.context.environment);
|
|
2111
|
+
return owner === host.context.sessionId ? void 0 : owner;
|
|
2112
|
+
};
|
|
2113
|
+
const launch = async (parameters, where) => {
|
|
2114
|
+
const owner = launchOwner();
|
|
2115
|
+
return owner === void 0 ? launchHere(parameters, where) : launchIn(owner, parameters);
|
|
2116
|
+
};
|
|
2117
|
+
/** A launch another session handed this one: this session runs and owns it. */
|
|
2118
|
+
const launchFromApi = async (parameters) => {
|
|
2119
|
+
const result = await launchHere(parameters);
|
|
2120
|
+
publishRunsSoon(host.context);
|
|
2121
|
+
return {
|
|
2122
|
+
text: result.content.map((item) => item.text).join("\n"),
|
|
2123
|
+
...result.isError === true ? { isError: true } : {}
|
|
2124
|
+
};
|
|
2125
|
+
};
|
|
2126
|
+
let control;
|
|
2127
|
+
let controlDisposers = [];
|
|
2128
|
+
const disposeControl = () => {
|
|
2129
|
+
for (const dispose of controlDisposers.splice(0)) dispose();
|
|
2130
|
+
control?.dispose();
|
|
2131
|
+
control = void 0;
|
|
2132
|
+
};
|
|
2133
|
+
let modeOwner;
|
|
2134
|
+
const modeSelected = () => (host.context.selection.state?.["minor-mode"] ?? []).includes(WORKFLOW_MODE_ID);
|
|
2135
|
+
const modeState = () => {
|
|
2136
|
+
const active = modeSelected();
|
|
2137
|
+
return {
|
|
2138
|
+
activation: active ? "active" : "inactive",
|
|
2139
|
+
condition: "ready",
|
|
2140
|
+
...active ? { detail: "workflow tools available" } : {},
|
|
2141
|
+
actions: [{
|
|
2142
|
+
id: "activate",
|
|
2143
|
+
enabled: !active,
|
|
2144
|
+
...!active ? {} : { disabledReason: "Workflow mode is active." }
|
|
2145
|
+
}, {
|
|
2146
|
+
id: "deactivate",
|
|
2147
|
+
enabled: active,
|
|
2148
|
+
...active ? {} : { disabledReason: "Workflow mode is inactive." }
|
|
2149
|
+
}]
|
|
2150
|
+
};
|
|
2151
|
+
};
|
|
2152
|
+
const selectMode = async (enabled) => {
|
|
2153
|
+
const modes = (host.context.selection.state?.["minor-mode"] ?? []).filter((mode) => mode !== WORKFLOW_MODE_ID);
|
|
2154
|
+
await host.changeSelection({
|
|
2155
|
+
axis: "state",
|
|
2156
|
+
key: "minor-mode",
|
|
2157
|
+
values: enabled ? [...modes, WORKFLOW_MODE_ID] : modes
|
|
2158
|
+
});
|
|
2159
|
+
modeOwner?.publish();
|
|
2160
|
+
};
|
|
2161
|
+
modeOwner = (0, _agimon_ai_doompi_minor_mode.defineMinorMode)({
|
|
2162
|
+
descriptor: {
|
|
2163
|
+
source: SOURCE,
|
|
2164
|
+
id: WORKFLOW_MODE_ID,
|
|
2165
|
+
label: "Workflow",
|
|
2166
|
+
description: "Workflow discovery, launch, inspection, control, and recovery tools.",
|
|
2167
|
+
order: 20,
|
|
2168
|
+
actions: [{
|
|
2169
|
+
id: "activate",
|
|
2170
|
+
label: "Activate",
|
|
2171
|
+
description: "Enable workflow tools for this session.",
|
|
2172
|
+
contexts: ["headless"],
|
|
2173
|
+
parameters: []
|
|
2174
|
+
}, {
|
|
2175
|
+
id: "deactivate",
|
|
2176
|
+
label: "Deactivate",
|
|
2177
|
+
description: "Disable workflow tools without stopping active runs.",
|
|
2178
|
+
contexts: ["headless"],
|
|
2179
|
+
parameters: []
|
|
2180
|
+
}]
|
|
2181
|
+
},
|
|
2182
|
+
state: modeState,
|
|
2183
|
+
async handleAction(_runtime, actionId, _argumentsValue, execution) {
|
|
2184
|
+
execution.signal.throwIfAborted();
|
|
2185
|
+
if (actionId === "activate") {
|
|
2186
|
+
await selectMode(true);
|
|
2187
|
+
return { message: "Workflow mode activated." };
|
|
2188
|
+
}
|
|
2189
|
+
if (actionId === "deactivate") {
|
|
2190
|
+
await selectMode(false);
|
|
2191
|
+
return { message: "Workflow mode deactivated." };
|
|
2192
|
+
}
|
|
2193
|
+
throw new Error(`Unknown workflow mode action: ${actionId}`);
|
|
2194
|
+
}
|
|
2195
|
+
}).createOwner(void 0);
|
|
2196
|
+
return {
|
|
2197
|
+
api: [createWorkflowApi(launchFromApi)],
|
|
2198
|
+
services: [(0, _agimon_ai_doompi_minor_mode.serverMinorModes)([modeOwner]), (context) => {
|
|
2199
|
+
const provider = require_workflowTerminal.registerRunProvider(context);
|
|
2200
|
+
runProvider = provider;
|
|
2201
|
+
return () => {
|
|
2202
|
+
provider.dispose();
|
|
2203
|
+
if (runProvider === provider) runProvider = void 0;
|
|
2204
|
+
};
|
|
2205
|
+
}],
|
|
2206
|
+
hooks: [{
|
|
2207
|
+
event: "tool_execution_end",
|
|
2208
|
+
handle: async (_event, executionContext) => {
|
|
2209
|
+
await publishLifecycle(executionContext);
|
|
2210
|
+
}
|
|
2211
|
+
}, {
|
|
2212
|
+
when: {
|
|
2213
|
+
state: { "minor-mode": WORKFLOW_MODE_ID },
|
|
2214
|
+
attribution: {
|
|
2215
|
+
kind: "minor",
|
|
2216
|
+
mode: WORKFLOW_MODE_ID
|
|
2217
|
+
}
|
|
2218
|
+
},
|
|
2219
|
+
event: "session_shutdown",
|
|
2220
|
+
handle: disposeControl
|
|
2221
|
+
}],
|
|
2222
|
+
resources: [
|
|
2223
|
+
{
|
|
2224
|
+
when: {
|
|
2225
|
+
state: { "minor-mode": WORKFLOW_MODE_ID },
|
|
2226
|
+
attribution: {
|
|
2227
|
+
kind: "minor",
|
|
2228
|
+
mode: WORKFLOW_MODE_ID
|
|
2229
|
+
}
|
|
2230
|
+
},
|
|
2231
|
+
name: "doompi-author-workflow",
|
|
2232
|
+
kind: "skill",
|
|
2233
|
+
read: () => require_workflowTerminal.readWorkflowSkill("doompi-author-workflow")
|
|
2234
|
+
},
|
|
2235
|
+
{
|
|
2236
|
+
when: {
|
|
2237
|
+
state: { "minor-mode": WORKFLOW_MODE_ID },
|
|
2238
|
+
attribution: {
|
|
2239
|
+
kind: "minor",
|
|
2240
|
+
mode: WORKFLOW_MODE_ID
|
|
2241
|
+
}
|
|
2242
|
+
},
|
|
2243
|
+
name: "doompi-use-workflow",
|
|
2244
|
+
kind: "skill",
|
|
2245
|
+
read: () => require_workflowTerminal.readWorkflowSkill("doompi-use-workflow")
|
|
2246
|
+
},
|
|
2247
|
+
{
|
|
2248
|
+
when: {
|
|
2249
|
+
state: { "minor-mode": WORKFLOW_MODE_ID },
|
|
2250
|
+
attribution: {
|
|
2251
|
+
kind: "minor",
|
|
2252
|
+
mode: WORKFLOW_MODE_ID
|
|
2253
|
+
}
|
|
2254
|
+
},
|
|
2255
|
+
name: "workflow-recovery",
|
|
2256
|
+
kind: "skill",
|
|
2257
|
+
read: () => require_workflowTerminal.readWorkflowSkill("workflow-recovery")
|
|
2258
|
+
}
|
|
2259
|
+
],
|
|
2260
|
+
activities: [{
|
|
2261
|
+
when: {
|
|
2262
|
+
state: { "minor-mode": WORKFLOW_MODE_ID },
|
|
2263
|
+
attribution: {
|
|
2264
|
+
kind: "minor",
|
|
2265
|
+
mode: WORKFLOW_MODE_ID
|
|
2266
|
+
}
|
|
2267
|
+
},
|
|
2268
|
+
name: SOURCE,
|
|
2269
|
+
async start(executionContext) {
|
|
2270
|
+
const reportFailure = (error) => {
|
|
2271
|
+
process.emitWarning(`Could not publish headless workflow state: ${String(error)}`);
|
|
2272
|
+
};
|
|
2273
|
+
const stopFor = (ownedControl) => () => {
|
|
2274
|
+
publishLifecycle(executionContext).then((active) => {
|
|
2275
|
+
if (!active && !modeSelected() && control === ownedControl) disposeControl();
|
|
2276
|
+
}).catch(reportFailure);
|
|
2277
|
+
};
|
|
2278
|
+
if (control !== void 0) {
|
|
2279
|
+
const ownedControl = control;
|
|
2280
|
+
await publishLifecycle(executionContext);
|
|
2281
|
+
return stopFor(ownedControl);
|
|
2282
|
+
}
|
|
2283
|
+
control = feature.createRunControl({});
|
|
2284
|
+
const ownedControl = control;
|
|
2285
|
+
const refresh = () => {
|
|
2286
|
+
publishRunsSoon(executionContext, (active) => {
|
|
2287
|
+
if (!active && !modeSelected() && control === ownedControl) disposeControl();
|
|
2288
|
+
});
|
|
2289
|
+
};
|
|
2290
|
+
controlDisposers = [
|
|
2291
|
+
ownedControl.on("runStarted", refresh),
|
|
2292
|
+
ownedControl.on("runUpdated", refresh),
|
|
2293
|
+
ownedControl.on("runFinished", refresh),
|
|
2294
|
+
ownedControl.on("job", refresh),
|
|
2295
|
+
ownedControl.on("step", refresh)
|
|
2296
|
+
];
|
|
2297
|
+
try {
|
|
2298
|
+
await ownedControl.start();
|
|
2299
|
+
await publishLifecycle(executionContext);
|
|
2300
|
+
return stopFor(ownedControl);
|
|
2301
|
+
} catch (error) {
|
|
2302
|
+
if (control === ownedControl) disposeControl();
|
|
2303
|
+
throw error;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}],
|
|
2307
|
+
tools: [
|
|
2308
|
+
{
|
|
2309
|
+
when: {
|
|
2310
|
+
state: { "minor-mode": WORKFLOW_MODE_ID },
|
|
2311
|
+
attribution: {
|
|
2312
|
+
kind: "minor",
|
|
2313
|
+
mode: WORKFLOW_MODE_ID
|
|
2314
|
+
}
|
|
2315
|
+
},
|
|
2316
|
+
name: LIST_TOOL,
|
|
2317
|
+
label: "List Workflows",
|
|
2318
|
+
description: "List workflow definitions in this repository, not existing runs.",
|
|
2319
|
+
promptGuidelines: ["Use list_workflows to discover definitions, not to find an existing run."],
|
|
2320
|
+
parameters: zod.z.toJSONSchema(feature.listWorkflowsTool.getInputSchema()),
|
|
2321
|
+
executionMode: "serial",
|
|
2322
|
+
async execute(_toolCallId, parameters, signal, _onUpdate, context) {
|
|
2323
|
+
signal?.throwIfAborted();
|
|
2324
|
+
const input = parameters;
|
|
2325
|
+
return callResult(await feature.listWorkflowsTool.execute({
|
|
2326
|
+
...input,
|
|
2327
|
+
directory: input?.directory === void 0 ? context.cwd : typeof input.directory === "string" ? (0, node_path.resolve)(context.cwd, input.directory) : input.directory
|
|
2328
|
+
}));
|
|
2329
|
+
}
|
|
2330
|
+
},
|
|
2331
|
+
{
|
|
2332
|
+
when: {
|
|
2333
|
+
state: { "minor-mode": WORKFLOW_MODE_ID },
|
|
2334
|
+
attribution: {
|
|
2335
|
+
kind: "minor",
|
|
2336
|
+
mode: WORKFLOW_MODE_ID
|
|
2337
|
+
}
|
|
2338
|
+
},
|
|
2339
|
+
name: LAUNCH_TOOL,
|
|
2340
|
+
label: "Launch Workflow",
|
|
2341
|
+
description: "Start a workflow run. A successful launch is not a completed workflow.",
|
|
2342
|
+
promptGuidelines: ["Launch returns when a run starts, not when it finishes. Use the exact run key and separate workspace from its launch result for workflow_run status, not Agiflow project or job identifiers.", "Do not relaunch a healthy running workflow merely because it has not completed."],
|
|
2343
|
+
parameters: zod.z.toJSONSchema(feature.runTool.getInputSchema()),
|
|
2344
|
+
executionMode: "serial",
|
|
2345
|
+
async execute(_toolCallId, parameters, signal, _onUpdate, context) {
|
|
2346
|
+
signal?.throwIfAborted();
|
|
2347
|
+
const input = parameters;
|
|
2348
|
+
return callResult(await launch({
|
|
2349
|
+
...input,
|
|
2350
|
+
...typeof input.workflowPath === "string" ? { workflowPath: (0, node_path.resolve)(context.cwd, input.workflowPath) } : {}
|
|
2351
|
+
}, context));
|
|
2352
|
+
}
|
|
2353
|
+
},
|
|
2354
|
+
{
|
|
2355
|
+
when: {
|
|
2356
|
+
state: { "minor-mode": WORKFLOW_MODE_ID },
|
|
2357
|
+
attribution: {
|
|
2358
|
+
kind: "minor",
|
|
2359
|
+
mode: WORKFLOW_MODE_ID
|
|
2360
|
+
}
|
|
2361
|
+
},
|
|
2362
|
+
name: RUN_TOOL,
|
|
2363
|
+
label: "Workflow Run",
|
|
2364
|
+
description: "Inspect a session-owned workflow by exact run key and separate workspace, or request cooperative control.",
|
|
2365
|
+
promptGuidelines: [
|
|
2366
|
+
"Use workflow_run status for the recorded stage, outcome, position, jobs, and steps. A successful status lookup is not evidence that the workflow completed.",
|
|
2367
|
+
"For a healthy running workflow, summarize the recorded progress. Do not automatically search CLI commands or filesystem logs, relaunch, or diagnose failure merely because it remains running.",
|
|
2368
|
+
"Investigate further when the run reports failure or staleness, or when the user requests diagnostics. Only status, pause, resume, and stop are supported here; stop only on user request.",
|
|
2369
|
+
"Use exact registry runKey and separate workspace from the launch or status result. Agiflow project and job identifiers are not workflow run keys."
|
|
2370
|
+
],
|
|
2371
|
+
parameters: {
|
|
2372
|
+
type: "object",
|
|
2373
|
+
properties: {
|
|
2374
|
+
action: {
|
|
2375
|
+
type: "string",
|
|
2376
|
+
enum: [
|
|
2377
|
+
"status",
|
|
2378
|
+
"pause",
|
|
2379
|
+
"resume",
|
|
2380
|
+
"stop"
|
|
2381
|
+
]
|
|
2382
|
+
},
|
|
2383
|
+
runKey: {
|
|
2384
|
+
type: "string",
|
|
2385
|
+
minLength: 1,
|
|
2386
|
+
description: "Exact registry run key from the launch result, without the workspace prefix. Not an Agiflow job ID."
|
|
2387
|
+
},
|
|
2388
|
+
workspace: {
|
|
2389
|
+
type: "string",
|
|
2390
|
+
description: "Workspace from the launch result, passed separately from runKey."
|
|
2391
|
+
},
|
|
2392
|
+
reason: { type: "string" },
|
|
2393
|
+
expectedRunId: { type: "string" }
|
|
2394
|
+
},
|
|
2395
|
+
required: ["action", "runKey"],
|
|
2396
|
+
additionalProperties: false
|
|
2397
|
+
},
|
|
2398
|
+
executionMode: "serial",
|
|
2399
|
+
async execute(_toolCallId, parameters, signal) {
|
|
2400
|
+
signal?.throwIfAborted();
|
|
2401
|
+
const input = parameters;
|
|
2402
|
+
const runs = readWorkflowRuns({ environment: host.context.environment }).filter((run) => runBelongsToSession(run, host.context.sessionId) && run.view.runKey === input.runKey && (input.workspace === void 0 || run.view.workspace === input.workspace));
|
|
2403
|
+
if (runs.length !== 1) return {
|
|
2404
|
+
...callResult({ error: runs.length === 0 ? "No matching workflow run was found in this session. This is a lookup failure, not evidence that the workflow failed. Use the exact runKey and separate workspace from its launch result, not Agiflow job identifiers." : "Specify the workspace for this run key." }),
|
|
2405
|
+
isError: true
|
|
2406
|
+
};
|
|
2407
|
+
const run = runs[0].view;
|
|
2408
|
+
if (input.action === "status") return callResult(run);
|
|
2409
|
+
if (!control) return callResult({ error: "Workflow activity is not active." });
|
|
2410
|
+
if (typeof input.expectedRunId !== "string" || input.expectedRunId.length === 0) return callResult({ error: "expectedRunId is required for workflow control." });
|
|
2411
|
+
return callResult(input.action === "pause" ? await control.pause(input.runKey, input.expectedRunId, input.workspace, input.reason) : input.action === "resume" ? await control.resume(input.runKey, input.expectedRunId, input.workspace) : await control.stop(input.runKey, input.expectedRunId, input.workspace, input.reason));
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
],
|
|
2415
|
+
commands: [{
|
|
2416
|
+
when: {
|
|
2417
|
+
state: { "minor-mode": WORKFLOW_MODE_ID },
|
|
2418
|
+
attribution: {
|
|
2419
|
+
kind: "minor",
|
|
2420
|
+
mode: WORKFLOW_MODE_ID
|
|
2421
|
+
}
|
|
2422
|
+
},
|
|
2423
|
+
name: "workflow-launch",
|
|
2424
|
+
description: "Launch a workflow from a slash command.",
|
|
2425
|
+
async execute(args, execution) {
|
|
2426
|
+
const parsed = require_workflowLaunchCommand.parseWorkflowLaunchCommand(args);
|
|
2427
|
+
if ("error" in parsed) {
|
|
2428
|
+
await execution.client.notify({
|
|
2429
|
+
body: parsed.error,
|
|
2430
|
+
level: "error"
|
|
2431
|
+
});
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
try {
|
|
2435
|
+
const entry = require_workflowLaunchCommand.resolveWorkflowEntry(await catalogReader.read(execution.cwd), parsed.workflow);
|
|
2436
|
+
if (!entry || entry.error) {
|
|
2437
|
+
await execution.client.notify({
|
|
2438
|
+
body: entry?.error ? needsFixingNotice(entry.name, entry.error) : `Workflow not found: ${parsed.workflow}`,
|
|
2439
|
+
level: "error"
|
|
2440
|
+
});
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
const problems = require_workflowLaunchCommand.validateWorkflowLaunch(entry, parsed);
|
|
2444
|
+
if (problems.length > 0) {
|
|
2445
|
+
await execution.client.notify({
|
|
2446
|
+
body: problems.join("\n"),
|
|
2447
|
+
level: "error"
|
|
2448
|
+
});
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
const result = callResult(await launch({
|
|
2452
|
+
workflowPath: entry.path,
|
|
2453
|
+
...parsed.runner === void 0 ? {} : { runner: parsed.runner },
|
|
2454
|
+
...parsed.command === void 0 ? {} : { command: parsed.command },
|
|
2455
|
+
...parsed.choice === void 0 ? {} : { choice: parsed.choice },
|
|
2456
|
+
...Object.keys(parsed.inputs).length === 0 ? {} : { inputs: parsed.inputs },
|
|
2457
|
+
...parsed.prompt === void 0 ? {} : { prompt: parsed.prompt }
|
|
2458
|
+
}, execution));
|
|
2459
|
+
const rendered = result.content.find((item) => item.type === "text");
|
|
2460
|
+
await execution.client.notify({
|
|
2461
|
+
body: rendered?.type === "text" ? rendered.text : "Workflow launch requested.",
|
|
2462
|
+
level: result.isError ? "error" : "info"
|
|
2463
|
+
});
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
await execution.client.notify({
|
|
2466
|
+
body: String(error),
|
|
2467
|
+
level: "error"
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
}],
|
|
2472
|
+
...dispatcher ? { toolRestrictions: [{ excludedTools: [RUN_TOOL] }] } : {},
|
|
2473
|
+
async onDispose() {
|
|
2474
|
+
disposeControl();
|
|
2475
|
+
if (runsTimer !== void 0) clearTimeout(runsTimer);
|
|
2476
|
+
await launcher.dispose();
|
|
2477
|
+
runProvider?.dispose();
|
|
2478
|
+
runProvider = void 0;
|
|
2479
|
+
await telemetry.shutdown();
|
|
2480
|
+
}
|
|
1318
2481
|
};
|
|
1319
2482
|
}
|
|
1320
|
-
/** Streams media with byte-range support so browser audio, video, and PDF controls can seek. */
|
|
1321
|
-
function streamArtifact(resolved, requested, rangeHeader, download) {
|
|
1322
|
-
const stats = statOf(resolved);
|
|
1323
|
-
if (stats === void 0 || stats.isDirectory()) return void 0;
|
|
1324
|
-
const mimeType = artifactMimeType(requested);
|
|
1325
|
-
let start = 0;
|
|
1326
|
-
let end = Math.max(0, stats.size - 1);
|
|
1327
|
-
let partial = false;
|
|
1328
|
-
const match = rangeHeader?.match(/^bytes=(\d*)-(\d*)$/);
|
|
1329
|
-
if (match !== null && match !== void 0 && stats.size > 0) {
|
|
1330
|
-
const requestedStart = match[1] === "" ? void 0 : Number(match[1]);
|
|
1331
|
-
const requestedEnd = match[2] === "" ? void 0 : Number(match[2]);
|
|
1332
|
-
if (requestedStart === void 0 && requestedEnd !== void 0) start = Math.max(0, stats.size - requestedEnd);
|
|
1333
|
-
else if (requestedStart !== void 0) {
|
|
1334
|
-
start = requestedStart;
|
|
1335
|
-
end = requestedEnd === void 0 ? end : Math.min(requestedEnd, end);
|
|
1336
|
-
}
|
|
1337
|
-
if (start >= stats.size || start > end) return new Response(null, {
|
|
1338
|
-
status: 416,
|
|
1339
|
-
headers: { "content-range": `bytes */${String(stats.size)}` }
|
|
1340
|
-
});
|
|
1341
|
-
partial = true;
|
|
1342
|
-
}
|
|
1343
|
-
const filename = node_path.default.basename(requested).replace(/["\\\r\n]/g, "_");
|
|
1344
|
-
const headers = new Headers({
|
|
1345
|
-
"accept-ranges": "bytes",
|
|
1346
|
-
"content-disposition": `${download || mimeType === "text/html" ? "attachment" : "inline"}; filename="${filename}"`,
|
|
1347
|
-
"content-length": String(stats.size === 0 ? 0 : end - start + 1),
|
|
1348
|
-
"content-type": mimeType,
|
|
1349
|
-
"x-content-type-options": "nosniff"
|
|
1350
|
-
});
|
|
1351
|
-
if (partial) headers.set("content-range", `bytes ${String(start)}-${String(end)}/${String(stats.size)}`);
|
|
1352
|
-
if (stats.size === 0) return new Response(null, {
|
|
1353
|
-
status: 200,
|
|
1354
|
-
headers
|
|
1355
|
-
});
|
|
1356
|
-
const body = node_stream.Readable.toWeb(node_fs.default.createReadStream(resolved, {
|
|
1357
|
-
start,
|
|
1358
|
-
end
|
|
1359
|
-
}));
|
|
1360
|
-
return new Response(body, {
|
|
1361
|
-
status: partial ? 206 : 200,
|
|
1362
|
-
headers
|
|
1363
|
-
});
|
|
1364
|
-
}
|
|
1365
|
-
/** The named export a host imports from this package's built hub entry. */
|
|
1366
|
-
const api = {
|
|
1367
|
-
basePath: WORKFLOW_API_BASE_PATH,
|
|
1368
|
-
start(_context) {
|
|
1369
|
-
const app = createWorkflowHubApi();
|
|
1370
|
-
return {
|
|
1371
|
-
fetch: (request) => app.fetch(request),
|
|
1372
|
-
close: () => void 0
|
|
1373
|
-
};
|
|
1374
|
-
}
|
|
1375
|
-
};
|
|
1376
2483
|
//#endregion
|
|
1377
2484
|
//#region src/extensions/workspaces/sessions/(backend)/_lib/index.server.ts
|
|
1378
|
-
var index_server_default$1 = (({ agent, host }) => ({
|
|
1379
|
-
api: [api],
|
|
1380
|
-
...agent ? createWorkflowServerRuntime(agent, host) : {}
|
|
1381
|
-
}));
|
|
2485
|
+
var index_server_default$1 = (({ agent, host }) => agent ? createWorkflowServerRuntime(agent, host) : { api: [api] });
|
|
1382
2486
|
//#endregion
|
|
1383
2487
|
//#region src/extensions/workspaces/sessions/(backend)/root.server.ts
|
|
1384
2488
|
var root_server_default = (0, _agimon_ai_doompi_core_extensionFile.defineRoot)((context) => {
|
|
@@ -1549,6 +2653,9 @@ var doompi_use_workflow_server_default = (0, _agimon_ai_doompi_core_extensionFil
|
|
|
1549
2653
|
//#region src/extensions/workspaces/sessions/(backend)/resource/workflow-recovery.server.ts
|
|
1550
2654
|
var workflow_recovery_server_default = (0, _agimon_ai_doompi_core_extensionFile.defineRoutedContribution)((context) => context.root.resources?.[2], { cardinality: "optional" });
|
|
1551
2655
|
//#endregion
|
|
2656
|
+
//#region src/extensions/workspaces/sessions/(backend)/tool-restriction/dispatcher.server.ts
|
|
2657
|
+
var dispatcher_server_default = (0, _agimon_ai_doompi_core_extensionFile.defineRoutedContribution)((context) => context.root.toolRestrictions?.[0], { cardinality: "optional" });
|
|
2658
|
+
//#endregion
|
|
1552
2659
|
//#region src/extensions/workspaces/sessions/(backend)/tool/launch-workflow.server.ts
|
|
1553
2660
|
var launch_workflow_server_default = (0, _agimon_ai_doompi_core_extensionFile.defineRoutedContribution)((context) => context.root.tools?.[1], { cardinality: "optional" });
|
|
1554
2661
|
//#endregion
|
|
@@ -1611,6 +2718,9 @@ const facet = (0, _agimon_ai_doompi_core_serverFacet.defineServerPlugin)({
|
|
|
1611
2718
|
at(workflow_recovery_server_default, contextSession)
|
|
1612
2719
|
]);
|
|
1613
2720
|
},
|
|
2721
|
+
get toolRestrictions() {
|
|
2722
|
+
return defined([at(dispatcher_server_default, contextSession)]);
|
|
2723
|
+
},
|
|
1614
2724
|
get tools() {
|
|
1615
2725
|
return defined([
|
|
1616
2726
|
via({ name: "launch_workflow" }, at(launch_workflow_server_default, contextSession)),
|