@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.
Files changed (41) hide show
  1. package/README.md +7 -0
  2. package/dist/apiContracts.cjs +3 -0
  3. package/dist/apiContracts.cjs.map +1 -1
  4. package/dist/apiContracts.d.mts.map +1 -1
  5. package/dist/apiContracts.mjs +3 -0
  6. package/dist/apiContracts.mjs.map +1 -1
  7. package/dist/extensions/pi.cjs +102 -644
  8. package/dist/extensions/pi.cjs.map +1 -1
  9. package/dist/extensions/pi.d.mts.map +1 -1
  10. package/dist/extensions/pi.mjs +49 -591
  11. package/dist/extensions/pi.mjs.map +1 -1
  12. package/dist/extensions/server.cjs +1718 -608
  13. package/dist/extensions/server.cjs.map +1 -1
  14. package/dist/extensions/server.d.mts.map +1 -1
  15. package/dist/extensions/server.mjs +1720 -610
  16. package/dist/extensions/server.mjs.map +1 -1
  17. package/dist/extensions/web.mjs +644 -218
  18. package/dist/extensions/web.mjs.map +1 -1
  19. package/dist/index.cjs +1 -1
  20. package/dist/index.d.mts +10 -3
  21. package/dist/index.d.mts.map +1 -1
  22. package/dist/index.mjs +1 -1
  23. package/dist/{workflowLaunchCommand-Bhr-PvdD.mjs → workflowLaunchCommand-BLrxmzdd.mjs} +14 -7
  24. package/dist/workflowLaunchCommand-BLrxmzdd.mjs.map +1 -0
  25. package/dist/{workflowLaunchCommand-7y8xg5YF.cjs → workflowLaunchCommand-DJyDB96d.cjs} +14 -7
  26. package/dist/workflowLaunchCommand-DJyDB96d.cjs.map +1 -0
  27. package/dist/workflowTerminal-Bb4bLtLu.cjs +962 -0
  28. package/dist/workflowTerminal-Bb4bLtLu.cjs.map +1 -0
  29. package/dist/workflowTerminal-QzufjHCa.mjs +730 -0
  30. package/dist/workflowTerminal-QzufjHCa.mjs.map +1 -0
  31. package/package.json +9 -9
  32. package/src/prompts/doompi-author-workflow/SKILL.md +26 -0
  33. package/src/prompts/doompi-use-workflow/SKILL.md +1 -1
  34. package/src/types/webWorkflowTerminal.ts +16 -0
  35. package/src/types/webWorkflows.ts +20 -0
  36. package/dist/workflowLaunchCommand-7y8xg5YF.cjs.map +0 -1
  37. package/dist/workflowLaunchCommand-Bhr-PvdD.mjs.map +0 -1
  38. package/dist/workflowTerminal-Bj4KKpoA.mjs +0 -148
  39. package/dist/workflowTerminal-Bj4KKpoA.mjs.map +0 -1
  40. package/dist/workflowTerminal-WM_cpxIO.cjs +0 -200
  41. 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-WM_cpxIO.cjs");
6
- const require_workflowLaunchCommand = require("../workflowLaunchCommand-7y8xg5YF.cjs");
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 node_os = require("node:os");
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
- //#region src/services/webWorkflowCatalog/index.ts
24
- function unchanged(left, right) {
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.replace(ANSI_ESCAPE_PATTERN, "").trim();
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/workflowWatcher/index.ts
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
- * Read the durable workflow registry once.
401
+ * The engine's log for one run, where a reader can find it.
409
402
  *
410
- * The registry is an initial snapshot only. Live delivery belongs to the
411
- * session lifecycle and is routed through the host-owned direct event bus.
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 readWorkflowRuns(options = {}) {
414
- const environment = options.environment;
415
- const homeDir = options.homeDir ?? resolveWorkflowHome({
416
- envValue: environment?.["WORKFLOW_MCP_HOME"],
417
- homeDir: node_os.default.homedir()
418
- });
419
- const workspacesDir = node_path.default.join(homeDir, WORKSPACES_DIR_NAME);
420
- const recordCache = /* @__PURE__ */ new Map();
421
- const progressCache = /* @__PURE__ */ new Map();
422
- const cachedParse = (cache, filePath, parse, empty) => {
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 value = parse(raw);
440
- cache.set(filePath, {
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
- const listDir = (directory) => {
448
- try {
449
- return node_fs.default.readdirSync(directory).sort();
450
- } catch {
451
- return [];
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
- const runs = [];
455
- for (const workspace of listDir(workspacesDir)) for (const stage of WORKFLOW_STAGES) {
456
- const stageDir = node_path.default.join(workspacesDir, workspace, stage);
457
- for (const runKey of listDir(stageDir)) {
458
- const runDir = node_path.default.join(stageDir, runKey);
459
- const recordPath = node_path.default.join(runDir, RUN_RECORD_FILE_NAME);
460
- const progressPath = node_path.default.join(runDir, PROGRESS_FILE_NAME);
461
- const parsed = cachedParse(recordCache, recordPath, parseWorkflowRunRecord, void 0);
462
- if (parsed === void 0) continue;
463
- const jobs = cachedParse(progressCache, progressPath, (raw) => foldWorkflowProgress(parseWorkflowProgress(raw)), []);
464
- runs.push({
465
- ...parsed,
466
- view: completeWorkflowRunView(parsed.view, jobs)
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/serverRuntime/index.ts
478
- const SOURCE = "@agimon-ai/doompi-workflow";
479
- const WORKFLOW_MODE_ID = "workflow";
480
- const LIST_TOOL = "list_workflows";
481
- const LAUNCH_TOOL = "launch_workflow";
482
- const RUN_TOOL = "workflow_run";
483
- function callResult(value) {
484
- const content = [];
485
- if (typeof value === "object" && value !== null && "content" in value && Array.isArray(value.content)) for (const item of value.content) {
486
- if (typeof item !== "object" || item === null || !("type" in item)) continue;
487
- const record = item;
488
- if (record.type === "text" && typeof record.text === "string") content.push({
489
- type: "text",
490
- text: record.text
491
- });
492
- else if (record.type === "image" && typeof record.data === "string" && typeof record.mimeType === "string") content.push({
493
- type: "image",
494
- data: record.data,
495
- mimeType: record.mimeType
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 (content.length === 0) content.push({
499
- type: "text",
500
- text: JSON.stringify(value, null, 2)
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
- content,
504
- details: value,
505
- ...typeof value === "object" && value !== null && "isError" in value && value.isError === true ? { isError: true } : {}
653
+ promise: new Promise((resolve) => {
654
+ settle = resolve;
655
+ }),
656
+ settle
506
657
  };
507
658
  }
508
- function createWorkflowServerRuntime(host, serverHost) {
509
- if (serverHost.context.directEvents === void 0) throw new Error("Workflow headless facet requires the session direct event bus.");
510
- const directEvents = serverHost.context.directEvents;
511
- let runProvider;
512
- const catalogReader = createWorkflowCatalogReader(defaultCatalogDeps());
513
- const publishLifecycle = async (executionContext) => {
514
- const records = readWorkflowRuns({ environment: executionContext.environment }).filter((run) => runBelongsToSession(run, executionContext.sessionId));
515
- const activeItems = records.filter((run) => run.view.stage === "running" && run.view.stale !== true).map((run) => ({
516
- id: `${run.view.workspace}/${run.view.runKey}`,
517
- sessionId: executionContext.sessionId
518
- }));
519
- runProvider?.update(activeItems);
520
- const runs = presentWorkflowRuns(records.map((run) => run.view), Date.now());
521
- directEvents.publish(WORKFLOW_RUNS_TYPE, executionContext.sessionId, { runs });
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
- directEvents.publish(WORKFLOW_CATALOG_TYPE, executionContext.sessionId, {
524
- cwd: executionContext.cwd,
525
- workflows: presentWorkflowCatalog(await catalogReader.read(executionContext.cwd))
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
- const warning = error instanceof Error ? error.message : String(error);
529
- directEvents.publish(WORKFLOW_CATALOG_TYPE, executionContext.sessionId, {
530
- cwd: executionContext.cwd,
531
- workflows: [],
532
- warning
533
- });
534
- }
535
- return activeItems.length > 0;
536
- };
537
- const feature = (0, _agimon_ai_workflow_mcp.createEmbeddedWorkflowFeature)();
538
- const launch = (parameters) => feature.runTool.execute({
539
- ...parameters,
540
- env: {
541
- ...parameters.env,
542
- [PI_SESSION_ENV]: host.context.sessionId
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
- let modeOwner;
553
- const modeSelected = () => (host.context.selection.state?.["minor-mode"] ?? []).includes(WORKFLOW_MODE_ID);
554
- const modeState = () => {
555
- const active = modeSelected();
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
- activation: active ? "active" : "inactive",
558
- condition: "ready",
559
- ...active ? { detail: "workflow tools available" } : {},
560
- actions: [{
561
- id: "activate",
562
- enabled: !active,
563
- ...!active ? {} : { disabledReason: "Workflow mode is active." }
564
- }, {
565
- id: "deactivate",
566
- enabled: active,
567
- ...active ? {} : { disabledReason: "Workflow mode is inactive." }
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
- const selectMode = async (enabled) => {
572
- const modes = (host.context.selection.state?.["minor-mode"] ?? []).filter((mode) => mode !== WORKFLOW_MODE_ID);
573
- await host.changeSelection({
574
- axis: "state",
575
- key: "minor-mode",
576
- values: enabled ? [...modes, WORKFLOW_MODE_ID] : modes
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
- modeOwner?.publish();
952
+ return detail;
579
953
  };
580
- modeOwner = (0, _agimon_ai_doompi_minor_mode.defineMinorMode)({
581
- descriptor: {
582
- source: SOURCE,
583
- id: WORKFLOW_MODE_ID,
584
- label: "Workflow",
585
- description: "Workflow discovery, launch, inspection, control, and recovery tools.",
586
- order: 20,
587
- actions: [{
588
- id: "activate",
589
- label: "Activate",
590
- description: "Enable workflow tools for this session.",
591
- contexts: ["headless"],
592
- parameters: []
593
- }, {
594
- id: "deactivate",
595
- label: "Deactivate",
596
- description: "Disable workflow tools without stopping active runs.",
597
- contexts: ["headless"],
598
- parameters: []
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
- state: modeState,
602
- async handleAction(_runtime, actionId, _argumentsValue, execution) {
603
- execution.signal.throwIfAborted();
604
- if (actionId === "activate") {
605
- await selectMode(true);
606
- return { message: "Workflow mode activated." };
607
- }
608
- if (actionId === "deactivate") {
609
- await selectMode(false);
610
- return { message: "Workflow mode deactivated." };
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
- }).createOwner(void 0);
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
- services: [(0, _agimon_ai_doompi_minor_mode.serverMinorModes)([modeOwner]), (context) => {
617
- const provider = require_workflowTerminal.registerRunProvider(context);
618
- runProvider = provider;
619
- return () => {
620
- provider.dispose();
621
- if (runProvider === provider) runProvider = void 0;
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
- hooks: [{
625
- event: "tool_execution_end",
626
- handle: async (_event, executionContext) => {
627
- await publishLifecycle(executionContext);
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
- when: {
631
- state: { "minor-mode": WORKFLOW_MODE_ID },
632
- attribution: {
633
- kind: "minor",
634
- mode: WORKFLOW_MODE_ID
635
- }
636
- },
637
- event: "session_shutdown",
638
- handle: disposeControl
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/:name{.+}`,
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
- /** One run's identity for caches and leases, stable across stage moves. */
1051
- function runIdentityOf(workspace, runKey) {
1052
- return `${workspace}/${runKey}`;
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 identity = runIdentityOf(workspace, runKey);
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
- const capabilities = terminal.capabilities(current);
1094
- const lines = capabilities.readable ? await terminal.screen(identity, current, SCREEN_LINES) : [];
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
- terminal.forget(/* @__PURE__ */ new Set());
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 capabilities = terminal.capabilities(record);
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), record, body.token, body.data);
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 { token, columns, rows } = await jsonBody(context);
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), record, token, columns, rows);
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)),