@agimon-ai/doompi-workflow 0.0.1-alpha.113 → 0.0.1-alpha.115
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apiContracts.cjs +27 -1
- package/dist/apiContracts.cjs.map +1 -1
- package/dist/apiContracts.mjs +27 -1
- package/dist/apiContracts.mjs.map +1 -1
- package/dist/extensions/server.cjs +144 -31
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.mjs +144 -31
- package/dist/extensions/server.mjs.map +1 -1
- package/dist/extensions/web.mjs +180 -55
- package/dist/extensions/web.mjs.map +1 -1
- package/package.json +13 -13
- package/src/types/webWorkflowTerminal.ts +5 -0
- package/src/types/webWorkflows.ts +2 -1
|
@@ -8,12 +8,14 @@ import path, { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
|
8
8
|
import { NativeTerminalService, WORKFLOW_DECISION_FILE_ENV, WORKFLOW_DECISION_STEERING_ENV, WORKFLOW_DECISION_STEERING_HOST, WorkflowRegistryService, WorkflowTerminalService, connectNativeTerminal, createEmbeddedWorkflowFeature, diagnoseWorkflow, gateStepDecision, readStepDecision, readWorkflowCatalog, serveNativeTerminal, summarizeWorkflow, terminalSocketPath } from "@agimon-ai/workflow-mcp";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
11
12
|
import { defineMinorMode, serverMinorModes } from "@agimon-ai/doompi-minor-mode";
|
|
12
13
|
import { createDoomTelemetry } from "@agimon-ai/doompi-telemetry";
|
|
13
14
|
import { defineServerPlugin } from "@agimon-ai/doompi-core/serverFacet";
|
|
14
15
|
import { DOOM_NOTIFICATION_ENTRY_TYPE, createDoomNotificationEntryData } from "@agimon-ai/doompi-core/notification";
|
|
15
16
|
import { WORKFLOW_STEP_SESSION_PROVENANCE } from "@agimon-ai/doompi-core/hubChannel";
|
|
16
17
|
import { Readable } from "node:stream";
|
|
18
|
+
import { doomApiCallerFrom } from "@agimon-ai/doompi-core/packageApi";
|
|
17
19
|
import { Hono } from "hono";
|
|
18
20
|
import { streamSSE } from "hono/streaming";
|
|
19
21
|
import { apiResponse, defineApiRoutes } from "@agimon-ai/doompi-core/web";
|
|
@@ -216,14 +218,56 @@ function jobPhase(name) {
|
|
|
216
218
|
if (name === PHASE_JOB_POST) return PHASE_JOB_POST;
|
|
217
219
|
return "job";
|
|
218
220
|
}
|
|
219
|
-
/**
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
221
|
+
/** Names match the engine's progress log, including unnamed parallel children. */
|
|
222
|
+
function plannedSteps(steps = []) {
|
|
223
|
+
return steps.flatMap((step, index) => {
|
|
224
|
+
const name = step.name ?? `step ${index + 1}`;
|
|
225
|
+
return step.parallel === void 0 ? [{
|
|
226
|
+
name,
|
|
227
|
+
status: "pending"
|
|
228
|
+
}] : step.parallel.map((child, childIndex) => ({
|
|
229
|
+
name: child.name ?? `${name} ${childIndex + 1}`,
|
|
230
|
+
status: "pending",
|
|
231
|
+
group: name
|
|
232
|
+
}));
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/** The whole declared route, before any job or step has emitted progress. */
|
|
236
|
+
function planWorkflowJobs(workflow, order) {
|
|
237
|
+
const jobs = order.map((name) => {
|
|
238
|
+
const job = workflow.jobs[name];
|
|
239
|
+
return {
|
|
240
|
+
name,
|
|
241
|
+
phase: "job",
|
|
242
|
+
status: "pending",
|
|
243
|
+
steps: [
|
|
244
|
+
...plannedSteps(job.preJob?.steps),
|
|
245
|
+
...plannedSteps(job.steps),
|
|
246
|
+
...plannedSteps(job.postJob?.steps)
|
|
247
|
+
]
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
if (workflow.pre !== void 0) jobs.unshift({
|
|
251
|
+
name: PHASE_JOB_PRE,
|
|
252
|
+
phase: "pre",
|
|
253
|
+
status: "pending",
|
|
254
|
+
steps: plannedSteps(workflow.pre.steps)
|
|
255
|
+
});
|
|
256
|
+
if (workflow.post !== void 0) jobs.push({
|
|
257
|
+
name: PHASE_JOB_POST,
|
|
258
|
+
phase: "post",
|
|
259
|
+
status: "pending",
|
|
260
|
+
steps: plannedSteps(workflow.post.steps)
|
|
261
|
+
});
|
|
262
|
+
return jobs;
|
|
263
|
+
}
|
|
264
|
+
/** Overlay progress on the plan, retaining recorded jobs and steps absent from the current definition. */
|
|
265
|
+
function foldWorkflowProgress(events, plan = []) {
|
|
266
|
+
const jobs = plan.map((job) => ({
|
|
267
|
+
...job,
|
|
268
|
+
steps: job.steps.map((step) => ({ ...step }))
|
|
269
|
+
}));
|
|
270
|
+
const jobByName = new Map(jobs.map((job) => [job.name, job]));
|
|
227
271
|
for (const event of events) {
|
|
228
272
|
let job = jobByName.get(event.job);
|
|
229
273
|
if (job === void 0) {
|
|
@@ -251,6 +295,7 @@ function foldWorkflowProgress(events) {
|
|
|
251
295
|
continue;
|
|
252
296
|
}
|
|
253
297
|
if (event.step === void 0) continue;
|
|
298
|
+
if (job.status === "pending") job.status = event.status;
|
|
254
299
|
let step = job.steps.find((candidate) => candidate.name === event.step);
|
|
255
300
|
if (step === void 0) {
|
|
256
301
|
step = {
|
|
@@ -271,6 +316,9 @@ function foldWorkflowProgress(events) {
|
|
|
271
316
|
if (event.status === "running" && step.startedAt === void 0) step.startedAt = event.at;
|
|
272
317
|
if (STEP_TERMINAL_STATES.has(event.status)) step.endedAt = event.at;
|
|
273
318
|
}
|
|
319
|
+
for (const job of jobs) if (job.status === "skipped") {
|
|
320
|
+
for (const step of job.steps) if (step.status === "pending") step.status = "skipped";
|
|
321
|
+
}
|
|
274
322
|
return jobs;
|
|
275
323
|
}
|
|
276
324
|
const ACTIVE_PROGRESS_STATES = /* @__PURE__ */ new Set([
|
|
@@ -1361,9 +1409,15 @@ var apiRoutes_default = defineApiRoutes({
|
|
|
1361
1409
|
path: "/launch",
|
|
1362
1410
|
response: apiResponse()
|
|
1363
1411
|
},
|
|
1412
|
+
openRunDirectory: {
|
|
1413
|
+
method: "POST",
|
|
1414
|
+
path: `${RUN}/open-directory`,
|
|
1415
|
+
response: apiResponse()
|
|
1416
|
+
},
|
|
1364
1417
|
artifacts: {
|
|
1365
1418
|
method: "GET",
|
|
1366
1419
|
path: `${RUN}/artifacts`,
|
|
1420
|
+
query: ["directory"],
|
|
1367
1421
|
response: apiResponse()
|
|
1368
1422
|
},
|
|
1369
1423
|
/** One artifact: its metadata and text, or its raw bytes under `raw=1`. */
|
|
@@ -1674,18 +1728,49 @@ function createWorkflowHubApi(options = {}) {
|
|
|
1674
1728
|
}
|
|
1675
1729
|
return context.json({ deleted: true });
|
|
1676
1730
|
});
|
|
1731
|
+
app.post(apiRoutes_default.openRunDirectory.path, async (context) => {
|
|
1732
|
+
if (doomApiCallerFrom(context.req.raw.headers)?.locality !== "local") return context.json({ error: "Opening Finder is only available on the host." }, 403);
|
|
1733
|
+
const workspace = context.req.param("workspace");
|
|
1734
|
+
const runKey = context.req.param("runKey");
|
|
1735
|
+
const record = await runOf(workspace, runKey);
|
|
1736
|
+
if (record === void 0) return context.json(missing(workspace, runKey), 404);
|
|
1737
|
+
if (process.platform !== "darwin") return context.json({ error: "Finder is only available on macOS." }, 409);
|
|
1738
|
+
try {
|
|
1739
|
+
const runDir = path.resolve(registry.runDirectoryFor(record));
|
|
1740
|
+
if (!fs.statSync(runDir).isDirectory()) return context.json({ error: "The run directory is not a folder." }, 409);
|
|
1741
|
+
await new Promise((resolve, reject) => {
|
|
1742
|
+
execFile("/usr/bin/open", [
|
|
1743
|
+
"-a",
|
|
1744
|
+
"Finder",
|
|
1745
|
+
runDir
|
|
1746
|
+
], { timeout: 5e3 }, (error) => {
|
|
1747
|
+
if (error) reject(error);
|
|
1748
|
+
else resolve();
|
|
1749
|
+
});
|
|
1750
|
+
});
|
|
1751
|
+
return context.json({ opened: true });
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
return context.json({ error: error instanceof Error ? error.message : String(error) }, 409);
|
|
1754
|
+
}
|
|
1755
|
+
});
|
|
1677
1756
|
app.get(apiRoutes_default.artifacts.path, async (context) => {
|
|
1678
1757
|
const workspace = context.req.param("workspace");
|
|
1679
1758
|
const runKey = context.req.param("runKey");
|
|
1680
1759
|
const record = await runOf(workspace, runKey);
|
|
1681
1760
|
if (record === void 0) return context.json(missing(workspace, runKey), 404);
|
|
1682
1761
|
const runDir = registry.runDirectoryFor(record);
|
|
1683
|
-
const
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1762
|
+
const directory = context.req.query("directory") ?? "";
|
|
1763
|
+
if (resolveInside(runDir, directory) === void 0) return context.json({ error: "That folder is not inside this run." }, 400);
|
|
1764
|
+
try {
|
|
1765
|
+
const body = {
|
|
1766
|
+
runDir,
|
|
1767
|
+
description: record.runDirectory?.description ?? "",
|
|
1768
|
+
artifacts: readArtifacts(runDir, record, directory)
|
|
1769
|
+
};
|
|
1770
|
+
return context.json(body);
|
|
1771
|
+
} catch (error) {
|
|
1772
|
+
return context.json({ error: error instanceof Error ? error.message : String(error) }, 409);
|
|
1773
|
+
}
|
|
1689
1774
|
});
|
|
1690
1775
|
app.get(apiRoutes_default.artifact.path, async (context) => {
|
|
1691
1776
|
const workspace = context.req.param("workspace");
|
|
@@ -1714,7 +1799,20 @@ function resolveInside(runDir, requested) {
|
|
|
1714
1799
|
const resolved = path.resolve(runDir, requested);
|
|
1715
1800
|
const root = path.resolve(runDir);
|
|
1716
1801
|
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) return void 0;
|
|
1717
|
-
|
|
1802
|
+
try {
|
|
1803
|
+
const realRoot = fs.realpathSync(root);
|
|
1804
|
+
let ancestor = resolved;
|
|
1805
|
+
while (!fs.existsSync(ancestor)) {
|
|
1806
|
+
const parent = path.dirname(ancestor);
|
|
1807
|
+
if (parent === ancestor) return void 0;
|
|
1808
|
+
ancestor = parent;
|
|
1809
|
+
}
|
|
1810
|
+
const real = fs.realpathSync(ancestor);
|
|
1811
|
+
if (real !== realRoot && !real.startsWith(`${realRoot}${path.sep}`)) return void 0;
|
|
1812
|
+
return resolved;
|
|
1813
|
+
} catch {
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1718
1816
|
}
|
|
1719
1817
|
/** Size and modification time of one path, or undefined when it is not there. */
|
|
1720
1818
|
function statOf(target) {
|
|
@@ -1730,10 +1828,12 @@ function stateOf(stats) {
|
|
|
1730
1828
|
return stats.size === 0 ? "empty" : "written";
|
|
1731
1829
|
}
|
|
1732
1830
|
function viewOf(runDir, entry) {
|
|
1733
|
-
const
|
|
1831
|
+
const target = resolveInside(runDir, entry.path);
|
|
1832
|
+
const stats = target === void 0 ? void 0 : statOf(target);
|
|
1734
1833
|
return {
|
|
1735
1834
|
...entry,
|
|
1736
|
-
|
|
1835
|
+
kind: stats?.isDirectory() ? "directory" : entry.kind,
|
|
1836
|
+
state: target === void 0 ? "unreadable" : stateOf(stats),
|
|
1737
1837
|
...stats === void 0 ? {} : {
|
|
1738
1838
|
size: stats.size,
|
|
1739
1839
|
modifiedAt: stats.mtime.toISOString()
|
|
@@ -1749,7 +1849,7 @@ function viewOf(runDir, entry) {
|
|
|
1749
1849
|
* hidden: the engine's own context.md and progress log are often exactly what a
|
|
1750
1850
|
* reader is looking for.
|
|
1751
1851
|
*/
|
|
1752
|
-
function readArtifacts(runDir, record) {
|
|
1852
|
+
function readArtifacts(runDir, record, directory = "") {
|
|
1753
1853
|
const declared = (record.runDirectory?.entries ?? []).map((entry) => viewOf(runDir, {
|
|
1754
1854
|
path: entry.path,
|
|
1755
1855
|
kind: entry.kind,
|
|
@@ -1757,21 +1857,16 @@ function readArtifacts(runDir, record) {
|
|
|
1757
1857
|
producedBy: entry["produced-by"] ?? [],
|
|
1758
1858
|
declared: true
|
|
1759
1859
|
}));
|
|
1760
|
-
const
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
} catch {
|
|
1765
|
-
return declared;
|
|
1766
|
-
}
|
|
1767
|
-
const found = names.filter((entry) => !claimed.has(entry.name)).map((entry) => viewOf(runDir, {
|
|
1768
|
-
path: entry.name,
|
|
1860
|
+
const children = declared.filter((entry) => path.posix.dirname(entry.path) === (directory || "."));
|
|
1861
|
+
const claimed = new Set(children.map((entry) => entry.path));
|
|
1862
|
+
const found = fs.readdirSync(path.join(runDir, directory), { withFileTypes: true }).filter((entry) => !claimed.has(path.posix.join(directory, entry.name))).map((entry) => viewOf(runDir, {
|
|
1863
|
+
path: path.posix.join(directory, entry.name),
|
|
1769
1864
|
kind: entry.isDirectory() ? "directory" : "file",
|
|
1770
1865
|
description: "",
|
|
1771
1866
|
producedBy: [],
|
|
1772
1867
|
declared: false
|
|
1773
1868
|
})).sort((left, right) => left.path.localeCompare(right.path));
|
|
1774
|
-
return [...declared, ...found];
|
|
1869
|
+
return [...directory === "" ? declared : children, ...found];
|
|
1775
1870
|
}
|
|
1776
1871
|
/** Browser content type inferred from a filename without trusting client input. */
|
|
1777
1872
|
function artifactMimeType(requested) {
|
|
@@ -2072,6 +2167,7 @@ function createWorkflowSessionLifecycle(deps) {
|
|
|
2072
2167
|
}
|
|
2073
2168
|
//#endregion
|
|
2074
2169
|
//#region src/services/workflowWatcher/index.ts
|
|
2170
|
+
const { parser } = createEmbeddedWorkflowFeature();
|
|
2075
2171
|
/**
|
|
2076
2172
|
* Read the durable workflow registry once.
|
|
2077
2173
|
*
|
|
@@ -2119,6 +2215,18 @@ function readWorkflowRuns(options = {}) {
|
|
|
2119
2215
|
return [];
|
|
2120
2216
|
}
|
|
2121
2217
|
};
|
|
2218
|
+
const plans = /* @__PURE__ */ new Map();
|
|
2219
|
+
const planFor = (workflowPath) => {
|
|
2220
|
+
const cached = plans.get(workflowPath);
|
|
2221
|
+
if (cached !== void 0) return cached;
|
|
2222
|
+
let plan = [];
|
|
2223
|
+
try {
|
|
2224
|
+
const workflow = parser.parseWorkflowFile(workflowPath);
|
|
2225
|
+
plan = planWorkflowJobs(workflow, parser.resolveJobOrder(workflow.jobs, null));
|
|
2226
|
+
} catch {}
|
|
2227
|
+
plans.set(workflowPath, plan);
|
|
2228
|
+
return plan;
|
|
2229
|
+
};
|
|
2122
2230
|
const runs = [];
|
|
2123
2231
|
for (const workspace of listDir(workspacesDir)) for (const stage of WORKFLOW_STAGES) {
|
|
2124
2232
|
const stageDir = path.join(workspacesDir, workspace, stage);
|
|
@@ -2128,7 +2236,7 @@ function readWorkflowRuns(options = {}) {
|
|
|
2128
2236
|
const progressPath = path.join(runDir, PROGRESS_FILE_NAME);
|
|
2129
2237
|
const parsed = cachedParse(recordCache, recordPath, parseWorkflowRunRecord, void 0);
|
|
2130
2238
|
if (parsed === void 0) continue;
|
|
2131
|
-
const jobs = cachedParse(progressCache, progressPath,
|
|
2239
|
+
const jobs = foldWorkflowProgress(cachedParse(progressCache, progressPath, parseWorkflowProgress, []), planFor(parsed.view.workflowPath));
|
|
2132
2240
|
runs.push({
|
|
2133
2241
|
...parsed,
|
|
2134
2242
|
view: completeWorkflowRunView(parsed.view, jobs)
|
|
@@ -2734,8 +2842,13 @@ function createWorkflowServerRuntime(host, serverHost) {
|
|
|
2734
2842
|
},
|
|
2735
2843
|
name: LAUNCH_TOOL,
|
|
2736
2844
|
label: "Launch Workflow",
|
|
2737
|
-
description: "Start a workflow run. A successful launch is not a completed workflow.",
|
|
2738
|
-
promptGuidelines: [
|
|
2845
|
+
description: "Start a workflow run. Prefer in-process Pi execution with command: \"pi\" when supported; runner: \"codex\" is not an alias for it. A successful launch is not a completed workflow.",
|
|
2846
|
+
promptGuidelines: [
|
|
2847
|
+
"Before launching, inspect the workflow and its imports for commands and interactiveRun choices. Prefer a supported Pi template with inProcess: true, selected explicitly with command: \"pi\" (or a supported in-process \"pi-claude\" command when requested). Only use an external CLI when the user requests it or no in-process option exists.",
|
|
2848
|
+
"command selects the exact template. Without it, runner only matches an exact command name; otherwise each step uses its first command. runner: \"codex\" does not imply command: \"pi\", so pass command explicitly to avoid switching to \"claude\" on later steps.",
|
|
2849
|
+
"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.",
|
|
2850
|
+
"Do not relaunch a healthy running workflow merely because it has not completed."
|
|
2851
|
+
],
|
|
2739
2852
|
parameters: z.toJSONSchema(feature.runTool.getInputSchema()),
|
|
2740
2853
|
executionMode: "serial",
|
|
2741
2854
|
async execute(_toolCallId, parameters, signal, _onUpdate, context) {
|