@hilbras/omninode 2.0.0-alpha.3 → 2.0.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -9
- package/dist/cli/index.js +369 -47
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +221 -11
- package/dist/index.js +341 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { realpathSync } from "fs";
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/version.ts
|
|
9
|
-
var OMNINODE_VERSION = "2.0.0-alpha.
|
|
9
|
+
var OMNINODE_VERSION = "2.0.0-alpha.5";
|
|
10
10
|
|
|
11
11
|
// src/errors/index.ts
|
|
12
12
|
var OmniNodeError = class extends Error {
|
|
@@ -83,6 +83,11 @@ var agentConfigSchema = z.object({
|
|
|
83
83
|
input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
|
|
84
84
|
timeout_ms: z.number().int().positive().optional(),
|
|
85
85
|
inherit_env: z.boolean().optional(),
|
|
86
|
+
env_policy: z.enum(["inherit", "allowlist", "denylist", "explicit"]).optional(),
|
|
87
|
+
env_allowlist: z.array(z.string()).optional(),
|
|
88
|
+
env_denylist: z.array(z.string()).optional(),
|
|
89
|
+
max_output_bytes: z.number().int().positive().optional(),
|
|
90
|
+
allow_external_cwd: z.boolean().optional(),
|
|
86
91
|
metadata: z.record(z.unknown()).optional()
|
|
87
92
|
}).strict();
|
|
88
93
|
var roleConfigSchema = z.object({
|
|
@@ -257,6 +262,11 @@ function toAgentConfig(raw) {
|
|
|
257
262
|
inputMode: raw.input_mode,
|
|
258
263
|
timeoutMs: raw.timeout_ms,
|
|
259
264
|
inheritEnv: raw.inherit_env,
|
|
265
|
+
envPolicy: raw.env_policy,
|
|
266
|
+
envAllowlist: raw.env_allowlist,
|
|
267
|
+
envDenylist: raw.env_denylist,
|
|
268
|
+
maxOutputBytes: raw.max_output_bytes,
|
|
269
|
+
allowExternalCwd: raw.allow_external_cwd,
|
|
260
270
|
metadata: raw.metadata
|
|
261
271
|
};
|
|
262
272
|
}
|
|
@@ -1214,20 +1224,94 @@ var ProtocolSession = class {
|
|
|
1214
1224
|
sawV2 = false;
|
|
1215
1225
|
};
|
|
1216
1226
|
|
|
1217
|
-
// src/agents/
|
|
1218
|
-
|
|
1219
|
-
|
|
1227
|
+
// src/agents/cwd.ts
|
|
1228
|
+
import { statSync } from "fs";
|
|
1229
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
1230
|
+
function validateWorkingDirectory(cwd, options = {}) {
|
|
1231
|
+
if (!cwd) return void 0;
|
|
1232
|
+
const normalized = isAbsolute(cwd) ? resolve(cwd) : resolve(process.cwd(), cwd);
|
|
1233
|
+
let stats;
|
|
1234
|
+
try {
|
|
1235
|
+
stats = statSync(normalized);
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
throw new AgentError(
|
|
1238
|
+
"AGENT_NOT_FOUND",
|
|
1239
|
+
`Working directory "${normalized}" does not exist or is not accessible.`,
|
|
1240
|
+
{ cause: error, details: { cwd: normalized } }
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
if (!stats.isDirectory()) {
|
|
1244
|
+
throw new AgentError("AGENT_NOT_FOUND", `Working directory "${normalized}" is not a directory.`, {
|
|
1245
|
+
details: { cwd: normalized }
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
try {
|
|
1249
|
+
statSync(resolve(normalized, "."));
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
throw new AgentError(
|
|
1252
|
+
"AGENT_NOT_FOUND",
|
|
1253
|
+
`Working directory "${normalized}" is not accessible.`,
|
|
1254
|
+
{ cause: error }
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
1258
|
+
const rel = relative(projectRoot, normalized);
|
|
1259
|
+
const outsideProject = rel.startsWith("..") || isAbsolute(rel);
|
|
1260
|
+
if (outsideProject && options.allowExternal !== true) {
|
|
1261
|
+
throw new AgentError(
|
|
1262
|
+
"AGENT_NOT_FOUND",
|
|
1263
|
+
`Working directory "${normalized}" is outside the project root "${projectRoot}". Set allow_external_cwd: true on the agent to permit this.`,
|
|
1264
|
+
{ details: { cwd: normalized, projectRoot } }
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
return { cwd: normalized, outsideProject };
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// src/agents/env.ts
|
|
1271
|
+
var BASELINE_KEYS = ["PATH", "HOME"];
|
|
1272
|
+
function resolveEnvPolicy(config) {
|
|
1273
|
+
if (config.envPolicy !== void 0) return config.envPolicy;
|
|
1274
|
+
if (config.inheritEnv === false) return "explicit";
|
|
1275
|
+
return "inherit";
|
|
1276
|
+
}
|
|
1220
1277
|
function buildChildEnv(config) {
|
|
1221
1278
|
const configured = config.env ?? {};
|
|
1222
|
-
|
|
1279
|
+
const policy = resolveEnvPolicy(config);
|
|
1280
|
+
if (policy === "inherit") {
|
|
1223
1281
|
return { ...process.env, ...configured };
|
|
1224
1282
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
}
|
|
1283
|
+
const base = {};
|
|
1284
|
+
for (const key of BASELINE_KEYS) {
|
|
1285
|
+
const value = process.env[key];
|
|
1286
|
+
if (value !== void 0) base[key] = value;
|
|
1287
|
+
}
|
|
1288
|
+
switch (policy) {
|
|
1289
|
+
case "explicit":
|
|
1290
|
+
return { ...base, ...configured };
|
|
1291
|
+
case "allowlist": {
|
|
1292
|
+
const allowed = { ...base };
|
|
1293
|
+
for (const key of config.envAllowlist ?? []) {
|
|
1294
|
+
const value = process.env[key];
|
|
1295
|
+
if (value !== void 0) allowed[key] = value;
|
|
1296
|
+
}
|
|
1297
|
+
return { ...allowed, ...configured };
|
|
1298
|
+
}
|
|
1299
|
+
case "denylist": {
|
|
1300
|
+
const denied = new Set((config.envDenylist ?? []).map((k) => k.toUpperCase()));
|
|
1301
|
+
const filtered = {};
|
|
1302
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
1303
|
+
if (denied.has(key.toUpperCase())) continue;
|
|
1304
|
+
filtered[key] = value;
|
|
1305
|
+
}
|
|
1306
|
+
return { ...filtered, ...configured };
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1230
1309
|
}
|
|
1310
|
+
|
|
1311
|
+
// src/agents/process/index.ts
|
|
1312
|
+
var DEFAULT_TIMEOUT_MS2 = 6e5;
|
|
1313
|
+
var KILL_GRACE_MS = 5e3;
|
|
1314
|
+
var DEFAULT_MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
|
|
1231
1315
|
function composeTaskText(input) {
|
|
1232
1316
|
const sections = [`# Objective
|
|
1233
1317
|
${input.objective}`];
|
|
@@ -1289,40 +1373,73 @@ var ProcessAgent = class {
|
|
|
1289
1373
|
}
|
|
1290
1374
|
const mode = this.config.inputMode ?? "stdin";
|
|
1291
1375
|
const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
1292
|
-
return new Promise((
|
|
1376
|
+
return new Promise((resolve2, reject) => {
|
|
1377
|
+
let cwd;
|
|
1378
|
+
try {
|
|
1379
|
+
cwd = validateWorkingDirectory(this.config.cwd, {
|
|
1380
|
+
allowExternal: this.config.allowExternalCwd
|
|
1381
|
+
})?.cwd;
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
reject(error);
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1293
1386
|
const child = spawn(
|
|
1294
1387
|
command,
|
|
1295
1388
|
[...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
|
|
1296
1389
|
{
|
|
1297
|
-
cwd:
|
|
1390
|
+
...cwd !== void 0 ? { cwd } : {},
|
|
1298
1391
|
env: buildChildEnv(this.config),
|
|
1299
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1392
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1393
|
+
// Own process group so the whole tree can be terminated together
|
|
1394
|
+
// (roadmap §8: no orphaned children).
|
|
1395
|
+
...process.platform === "win32" ? {} : { detached: true }
|
|
1300
1396
|
}
|
|
1301
1397
|
);
|
|
1302
1398
|
this.running.set(input.taskId, child);
|
|
1399
|
+
GLOBAL_RUNNING.add(child);
|
|
1400
|
+
installSignalForwarding();
|
|
1303
1401
|
let stdout = "";
|
|
1304
1402
|
let stderr = "";
|
|
1305
1403
|
let timedOut = false;
|
|
1404
|
+
let outputLimitExceeded = false;
|
|
1306
1405
|
let settled = false;
|
|
1406
|
+
const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
1307
1407
|
const timer = setTimeout(() => {
|
|
1308
1408
|
timedOut = true;
|
|
1309
|
-
this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
|
|
1310
|
-
child
|
|
1311
|
-
setTimeout(() => child
|
|
1409
|
+
this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process tree.`);
|
|
1410
|
+
killTree(child, "SIGTERM");
|
|
1411
|
+
setTimeout(() => killTree(child, "SIGKILL"), KILL_GRACE_MS).unref();
|
|
1312
1412
|
}, timeoutMs);
|
|
1313
1413
|
const settle = (finish) => {
|
|
1314
1414
|
if (settled) return;
|
|
1315
1415
|
settled = true;
|
|
1316
1416
|
clearTimeout(timer);
|
|
1317
1417
|
this.running.delete(input.taskId);
|
|
1418
|
+
GLOBAL_RUNNING.delete(child);
|
|
1318
1419
|
finish();
|
|
1319
1420
|
};
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1421
|
+
const collect = (into, chunk) => {
|
|
1422
|
+
if (outputLimitExceeded) return;
|
|
1423
|
+
const text = sanitizeChunk(chunk);
|
|
1424
|
+
if (into === "stdout") {
|
|
1425
|
+
if (stdout.length + text.length > maxOutputBytes) {
|
|
1426
|
+
stdout = (stdout + text).slice(0, maxOutputBytes);
|
|
1427
|
+
outputLimitExceeded = true;
|
|
1428
|
+
this.log.warn(`Agent "${this.config.name}" exceeded the ${maxOutputBytes} byte output limit \u2014 killing it.`);
|
|
1429
|
+
killTree(child, "SIGKILL");
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
stdout += text;
|
|
1433
|
+
} else {
|
|
1434
|
+
if (stderr.length + text.length > maxOutputBytes) {
|
|
1435
|
+
stderr = (stderr + text).slice(0, maxOutputBytes);
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
stderr += text;
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
child.stdout?.on("data", (chunk) => collect("stdout", chunk));
|
|
1442
|
+
child.stderr?.on("data", (chunk) => collect("stderr", chunk));
|
|
1326
1443
|
child.stdin?.on("error", () => {
|
|
1327
1444
|
});
|
|
1328
1445
|
if (mode !== "arg") {
|
|
@@ -1376,7 +1493,7 @@ var ProcessAgent = class {
|
|
|
1376
1493
|
const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
|
|
1377
1494
|
if (mode === "protocol") {
|
|
1378
1495
|
try {
|
|
1379
|
-
|
|
1496
|
+
resolve2(
|
|
1380
1497
|
interpretProtocolRun({
|
|
1381
1498
|
input,
|
|
1382
1499
|
agentName: this.config.name,
|
|
@@ -1391,14 +1508,14 @@ var ProcessAgent = class {
|
|
|
1391
1508
|
return;
|
|
1392
1509
|
}
|
|
1393
1510
|
const trimmed = stdout.trim();
|
|
1394
|
-
|
|
1511
|
+
resolve2({
|
|
1395
1512
|
taskId: input.taskId,
|
|
1396
|
-
status: code === 0 ? "completed" : "failed",
|
|
1513
|
+
status: code === 0 && !outputLimitExceeded ? "completed" : "failed",
|
|
1397
1514
|
summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
|
|
1398
1515
|
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
1399
1516
|
exitCode: code ?? void 0,
|
|
1400
1517
|
logs,
|
|
1401
|
-
error:
|
|
1518
|
+
...outputLimitExceeded ? { error: `Agent exceeded the ${maxOutputBytes} byte output limit and was terminated.` } : code === 0 ? {} : { error: describeExit(code, signal) }
|
|
1402
1519
|
});
|
|
1403
1520
|
});
|
|
1404
1521
|
});
|
|
@@ -1407,7 +1524,7 @@ var ProcessAgent = class {
|
|
|
1407
1524
|
/** Ask the agent to stop current work (SIGTERM). */
|
|
1408
1525
|
async cancel(taskId) {
|
|
1409
1526
|
const child = this.running.get(taskId);
|
|
1410
|
-
if (child) child
|
|
1527
|
+
if (child) killTree(child, "SIGTERM");
|
|
1411
1528
|
}
|
|
1412
1529
|
runningTaskIds() {
|
|
1413
1530
|
return [...this.running.keys()];
|
|
@@ -1482,6 +1599,43 @@ function interpretProtocolRun({
|
|
|
1482
1599
|
...exitCode !== 0 ? { error: `Process exited with code ${exitCode ?? "unknown"}` } : {}
|
|
1483
1600
|
};
|
|
1484
1601
|
}
|
|
1602
|
+
var GLOBAL_RUNNING = /* @__PURE__ */ new Set();
|
|
1603
|
+
var signalForwardingInstalled = false;
|
|
1604
|
+
function installSignalForwarding() {
|
|
1605
|
+
if (signalForwardingInstalled) return;
|
|
1606
|
+
signalForwardingInstalled = true;
|
|
1607
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
1608
|
+
process.on(signal, () => {
|
|
1609
|
+
for (const child of GLOBAL_RUNNING) {
|
|
1610
|
+
killTree(child, "SIGTERM");
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
function killTree(child, signal) {
|
|
1616
|
+
if (child.pid === void 0) return;
|
|
1617
|
+
if (process.platform !== "win32") {
|
|
1618
|
+
try {
|
|
1619
|
+
process.kill(-child.pid, signal);
|
|
1620
|
+
return;
|
|
1621
|
+
} catch {
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
try {
|
|
1625
|
+
child.kill(signal);
|
|
1626
|
+
} catch {
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
function describeExit(code, signal) {
|
|
1630
|
+
if (signal !== null) {
|
|
1631
|
+
return signal === "SIGKILL" ? "Process was killed (SIGKILL) \u2014 usually an external kill or OOM." : `Process terminated by signal ${signal}.`;
|
|
1632
|
+
}
|
|
1633
|
+
if (code === null) return "Process terminated without an exit code.";
|
|
1634
|
+
return `Process exited with code ${code}.`;
|
|
1635
|
+
}
|
|
1636
|
+
function sanitizeChunk(chunk) {
|
|
1637
|
+
return [...chunk.toString("utf8")].filter((char) => char.charCodeAt(0) !== 0).join("");
|
|
1638
|
+
}
|
|
1485
1639
|
function normalizeReport(payload, taskId, agentName, index) {
|
|
1486
1640
|
if (payload === null || typeof payload !== "object") return void 0;
|
|
1487
1641
|
const raw = payload;
|
|
@@ -1501,6 +1655,40 @@ function firstLine(text, maxLength = 200) {
|
|
|
1501
1655
|
return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
|
|
1502
1656
|
}
|
|
1503
1657
|
|
|
1658
|
+
// src/agents/adapters.ts
|
|
1659
|
+
var AgentAdapterRegistry = class {
|
|
1660
|
+
adapters = /* @__PURE__ */ new Map();
|
|
1661
|
+
register(adapter) {
|
|
1662
|
+
this.adapters.set(adapter.integration, adapter);
|
|
1663
|
+
}
|
|
1664
|
+
get(integration) {
|
|
1665
|
+
return this.adapters.get(integration);
|
|
1666
|
+
}
|
|
1667
|
+
list() {
|
|
1668
|
+
return [...this.adapters.values()];
|
|
1669
|
+
}
|
|
1670
|
+
create(config) {
|
|
1671
|
+
const adapter = this.get(config.integration);
|
|
1672
|
+
if (!adapter) {
|
|
1673
|
+
throw new AgentError(
|
|
1674
|
+
"AGENT_NOT_FOUND",
|
|
1675
|
+
`No agent adapter registered for integration "${config.integration}". Registered: ${[...this.adapters.keys()].join(", ") || "(none)"}.`
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
return adapter.create(config);
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
var ProcessAgentAdapter = class {
|
|
1682
|
+
constructor(factory) {
|
|
1683
|
+
this.factory = factory;
|
|
1684
|
+
}
|
|
1685
|
+
factory;
|
|
1686
|
+
integration = "process";
|
|
1687
|
+
create(config) {
|
|
1688
|
+
return this.factory(config);
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1504
1692
|
// src/agents/registry.ts
|
|
1505
1693
|
var AgentRegistry = class {
|
|
1506
1694
|
agents = /* @__PURE__ */ new Map();
|
|
@@ -1516,14 +1704,15 @@ var AgentRegistry = class {
|
|
|
1516
1704
|
};
|
|
1517
1705
|
|
|
1518
1706
|
// src/agents/factory.ts
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1707
|
+
var defaultAgentAdapters = new AgentAdapterRegistry();
|
|
1708
|
+
defaultAgentAdapters.register(new ProcessAgentAdapter((config) => new ProcessAgent(config)));
|
|
1709
|
+
function createAgent(config, adapters = defaultAgentAdapters) {
|
|
1710
|
+
const registered = adapters.get(config.integration);
|
|
1711
|
+
if (registered) return registered.create(config);
|
|
1523
1712
|
if (config.integration === "native") {
|
|
1524
1713
|
throw new OmniNodeError(
|
|
1525
1714
|
"NOT_IMPLEMENTED",
|
|
1526
|
-
`
|
|
1715
|
+
`No native adapter is registered for "${config.name}". Register an IAgentAdapter (createAgent(config, adapters)) or use input over stdin/stdout (type "cli").`
|
|
1527
1716
|
);
|
|
1528
1717
|
}
|
|
1529
1718
|
throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
|
|
@@ -1566,7 +1755,7 @@ function registerAgentCommands(program) {
|
|
|
1566
1755
|
"--input-mode <mode>",
|
|
1567
1756
|
"How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
|
|
1568
1757
|
"stdin"
|
|
1569
|
-
).option(
|
|
1758
|
+
).option("--env-policy <policy>", "Environment policy: inherit, allowlist, denylist or explicit.").option("--allow-env <name>", "Variable to forward under the allowlist policy (repeatable).", collectArgs).option("--deny-env <name>", "Variable to remove under the denylist policy (repeatable).", collectArgs).option("--max-output-bytes <n>", "Per-stream output budget in bytes (default 5 MiB).", (value) => Number(value)).option("--allow-external-cwd", "Allow a working directory outside the project root.").option(
|
|
1570
1759
|
"--timeout-ms <ms>",
|
|
1571
1760
|
"Per-task timeout in milliseconds.",
|
|
1572
1761
|
(value) => Number(value)
|
|
@@ -1581,7 +1770,12 @@ async function addAgent(options) {
|
|
|
1581
1770
|
...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
|
|
1582
1771
|
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
1583
1772
|
input_mode: options.inputMode,
|
|
1584
|
-
...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {}
|
|
1773
|
+
...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {},
|
|
1774
|
+
...options.envPolicy !== void 0 ? { env_policy: options.envPolicy } : {},
|
|
1775
|
+
...options.allowEnv !== void 0 && options.allowEnv.length > 0 ? { env_allowlist: options.allowEnv } : {},
|
|
1776
|
+
...options.denyEnv !== void 0 && options.denyEnv.length > 0 ? { env_denylist: options.denyEnv } : {},
|
|
1777
|
+
...options.maxOutputBytes !== void 0 ? { max_output_bytes: options.maxOutputBytes } : {},
|
|
1778
|
+
...options.allowExternalCwd !== void 0 ? { allow_external_cwd: options.allowExternalCwd } : {}
|
|
1585
1779
|
});
|
|
1586
1780
|
if (!candidate.success) {
|
|
1587
1781
|
const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
@@ -1874,6 +2068,11 @@ var TaskEngine = class {
|
|
|
1874
2068
|
if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
|
|
1875
2069
|
return current;
|
|
1876
2070
|
}
|
|
2071
|
+
const currentState = await this.options.store.get(current.id);
|
|
2072
|
+
if (currentState?.status === "cancelled") {
|
|
2073
|
+
current = currentState;
|
|
2074
|
+
break;
|
|
2075
|
+
}
|
|
1877
2076
|
const result = {
|
|
1878
2077
|
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
1879
2078
|
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
@@ -1985,7 +2184,7 @@ function classifyThrown(error) {
|
|
|
1985
2184
|
return "failed";
|
|
1986
2185
|
}
|
|
1987
2186
|
function sleep(ms) {
|
|
1988
|
-
return new Promise((
|
|
2187
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1989
2188
|
}
|
|
1990
2189
|
|
|
1991
2190
|
// src/audit/index.ts
|
|
@@ -2999,9 +3198,23 @@ var PipelineEngine = class {
|
|
|
2999
3198
|
}
|
|
3000
3199
|
options;
|
|
3001
3200
|
log;
|
|
3201
|
+
/** Runs executing in THIS process — cancellation can terminate them directly. */
|
|
3202
|
+
activeRuns = /* @__PURE__ */ new Map();
|
|
3002
3203
|
get store() {
|
|
3003
3204
|
return this.options.store;
|
|
3004
3205
|
}
|
|
3206
|
+
/**
|
|
3207
|
+
* Saves the run while preserving fields that may be set by another process
|
|
3208
|
+
* (e.g. a cancellation request) — an in-memory snapshot must never clobber
|
|
3209
|
+
* externally requested cancellation (§9.2).
|
|
3210
|
+
*/
|
|
3211
|
+
async persist(run2) {
|
|
3212
|
+
const current = await this.options.store.get(run2.id);
|
|
3213
|
+
await this.options.store.save({
|
|
3214
|
+
...run2,
|
|
3215
|
+
...current?.cancellationRequested === true ? { cancellationRequested: true, cancellationRequestedAt: current.cancellationRequestedAt } : {}
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3005
3218
|
/** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
|
|
3006
3219
|
validate(def) {
|
|
3007
3220
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -3101,18 +3314,44 @@ var PipelineEngine = class {
|
|
|
3101
3314
|
);
|
|
3102
3315
|
}
|
|
3103
3316
|
}
|
|
3317
|
+
if (step.model) {
|
|
3318
|
+
const separator = step.model.indexOf(":");
|
|
3319
|
+
if (separator <= 0 || separator === step.model.length - 1) {
|
|
3320
|
+
throw new PipelineError(
|
|
3321
|
+
"PIPELINE_INVALID",
|
|
3322
|
+
`Step "${step.id}" has an invalid model reference "${step.model}" \u2014 use "provider:model-id".`
|
|
3323
|
+
);
|
|
3324
|
+
}
|
|
3325
|
+
const providerName = step.model.slice(0, separator);
|
|
3326
|
+
if (!this.options.providers.some((p) => p.name === providerName)) {
|
|
3327
|
+
throw new PipelineError(
|
|
3328
|
+
"PIPELINE_INVALID",
|
|
3329
|
+
`Step "${step.id}" uses model "${step.model}" but provider "${providerName}" is not configured.`
|
|
3330
|
+
);
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3104
3333
|
}
|
|
3105
3334
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3106
3335
|
const run2 = {
|
|
3107
3336
|
id: `run-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
|
|
3108
3337
|
pipelineId: def.id,
|
|
3338
|
+
attempt: 1,
|
|
3109
3339
|
objective,
|
|
3110
3340
|
status: "running",
|
|
3111
3341
|
startedAt: now,
|
|
3112
3342
|
stepRuns: []
|
|
3113
3343
|
};
|
|
3114
3344
|
await this.options.store.save(run2);
|
|
3115
|
-
|
|
3345
|
+
const liveTaskIds = /* @__PURE__ */ new Set();
|
|
3346
|
+
this.activeRuns.set(run2.id, {
|
|
3347
|
+
taskIds: liveTaskIds,
|
|
3348
|
+
cancelTasks: async () => {
|
|
3349
|
+
for (const taskId of liveTaskIds) {
|
|
3350
|
+
await this.options.tasks.cancel(taskId).catch(() => void 0);
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
});
|
|
3354
|
+
this.log.info(`Running pipeline ${def.id} as ${run2.id} (attempt ${run2.attempt}).`);
|
|
3116
3355
|
await this.options.audit?.record({
|
|
3117
3356
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3118
3357
|
action: "pipeline.run.started",
|
|
@@ -3124,7 +3363,22 @@ var PipelineEngine = class {
|
|
|
3124
3363
|
const pending = new Set(def.steps.map((s) => s.id));
|
|
3125
3364
|
const contextParts = [];
|
|
3126
3365
|
const allReports = [];
|
|
3366
|
+
let cancelled = false;
|
|
3127
3367
|
while (pending.size > 0) {
|
|
3368
|
+
const persisted = await this.options.store.get(run2.id);
|
|
3369
|
+
if (persisted?.cancellationRequested === true) {
|
|
3370
|
+
cancelled = true;
|
|
3371
|
+
run2.cancellationRequested = true;
|
|
3372
|
+
run2.cancellationRequestedAt = persisted.cancellationRequestedAt;
|
|
3373
|
+
await this.activeRuns.get(run2.id)?.cancelTasks();
|
|
3374
|
+
for (const step of def.steps.filter((s) => pending.has(s.id))) {
|
|
3375
|
+
const stepRun = { stepId: step.id, status: "cancelled" };
|
|
3376
|
+
run2.stepRuns.push(stepRun);
|
|
3377
|
+
options.onStep?.(stepRun);
|
|
3378
|
+
}
|
|
3379
|
+
pending.clear();
|
|
3380
|
+
break;
|
|
3381
|
+
}
|
|
3128
3382
|
const ready = def.steps.filter(
|
|
3129
3383
|
(step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
|
|
3130
3384
|
);
|
|
@@ -3141,7 +3395,8 @@ var PipelineEngine = class {
|
|
|
3141
3395
|
...role ? { role } : {},
|
|
3142
3396
|
combinedContext: contextParts.join("\n\n"),
|
|
3143
3397
|
reports: [...allReports],
|
|
3144
|
-
pipelineRunId: run2.id
|
|
3398
|
+
pipelineRunId: run2.id,
|
|
3399
|
+
runId: run2.id
|
|
3145
3400
|
};
|
|
3146
3401
|
return this.executeStep(step, ctx);
|
|
3147
3402
|
})
|
|
@@ -3158,6 +3413,8 @@ ${contribution}`);
|
|
|
3158
3413
|
}
|
|
3159
3414
|
const stepRun = {
|
|
3160
3415
|
stepId: step.id,
|
|
3416
|
+
attempt: outcome.attempt ?? 1,
|
|
3417
|
+
...outcome.executionIds && outcome.executionIds.length > 0 ? { executionIds: outcome.executionIds } : {},
|
|
3161
3418
|
status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
|
|
3162
3419
|
...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
|
|
3163
3420
|
...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
|
|
@@ -3167,12 +3424,12 @@ ${contribution}`);
|
|
|
3167
3424
|
options.onStep?.(stepRun);
|
|
3168
3425
|
});
|
|
3169
3426
|
run2.reports = [...allReports];
|
|
3170
|
-
await this.
|
|
3427
|
+
await this.persist(run2);
|
|
3171
3428
|
}
|
|
3172
3429
|
const allOutcomes = [...outcomes.values()];
|
|
3173
|
-
const failed = allOutcomes.some((o) => o.status === "failed");
|
|
3174
|
-
const partial = !failed && allOutcomes.some((o) => o.status === "partial");
|
|
3175
|
-
const finalStatus = failed ? "failed" : partial ? "partial" : "completed";
|
|
3430
|
+
const failed = !cancelled && allOutcomes.some((o) => o.status === "failed");
|
|
3431
|
+
const partial = !cancelled && !failed && allOutcomes.some((o) => o.status === "partial");
|
|
3432
|
+
const finalStatus = cancelled ? "cancelled" : failed ? "failed" : partial ? "partial" : "completed";
|
|
3176
3433
|
const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
|
|
3177
3434
|
let resultSummary;
|
|
3178
3435
|
for (let i = def.steps.length - 1; i >= 0; i -= 1) {
|
|
@@ -3200,16 +3457,19 @@ ${contribution}`);
|
|
|
3200
3457
|
run2.reports = collected;
|
|
3201
3458
|
}
|
|
3202
3459
|
}
|
|
3460
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3461
|
+
this.activeRuns.delete(run2.id);
|
|
3203
3462
|
const finished = {
|
|
3204
3463
|
...run2,
|
|
3205
3464
|
status: finalStatus,
|
|
3206
|
-
finishedAt
|
|
3465
|
+
finishedAt,
|
|
3466
|
+
attempts: [...run2.attempts ?? [], { attempt: run2.attempt, startedAt: run2.startedAt ?? finishedAt, finishedAt, status: finalStatus }],
|
|
3207
3467
|
...run2.reports !== void 0 && run2.reports.length > 0 ? { reports: run2.reports } : {},
|
|
3208
3468
|
...combinedReportId !== void 0 ? { combinedReportId } : {},
|
|
3209
3469
|
...planId !== void 0 ? { planId } : {},
|
|
3210
3470
|
...resultSummary !== void 0 ? { resultSummary } : {}
|
|
3211
3471
|
};
|
|
3212
|
-
await this.
|
|
3472
|
+
await this.persist(finished);
|
|
3213
3473
|
this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
|
|
3214
3474
|
await this.options.audit?.record({
|
|
3215
3475
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3219,6 +3479,23 @@ ${contribution}`);
|
|
|
3219
3479
|
});
|
|
3220
3480
|
return finished;
|
|
3221
3481
|
}
|
|
3482
|
+
/** First-class cancellation (§9.2): requests cancellation and terminates
|
|
3483
|
+
* tasks that are running in this process immediately. */
|
|
3484
|
+
async cancel(runId) {
|
|
3485
|
+
const run2 = await this.options.store.get(runId);
|
|
3486
|
+
if (!run2) {
|
|
3487
|
+
throw new PipelineError("PIPELINE_NOT_FOUND", `Pipeline run "${runId}" does not exist.`);
|
|
3488
|
+
}
|
|
3489
|
+
await this.options.store.save({
|
|
3490
|
+
...run2,
|
|
3491
|
+
cancellationRequested: true,
|
|
3492
|
+
cancellationRequestedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3493
|
+
});
|
|
3494
|
+
const active = this.activeRuns.get(runId);
|
|
3495
|
+
if (active) {
|
|
3496
|
+
await active.cancelTasks();
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3222
3499
|
async get(runId) {
|
|
3223
3500
|
return this.options.store.get(runId);
|
|
3224
3501
|
}
|
|
@@ -3242,9 +3519,16 @@ ${contribution}`);
|
|
|
3242
3519
|
this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
|
|
3243
3520
|
}
|
|
3244
3521
|
last = await this.executeStepOnce(step, ctx);
|
|
3245
|
-
if (last.status !== "failed") return last;
|
|
3522
|
+
if (last.status !== "failed") return { ...last, attempt: attempt + 1 };
|
|
3246
3523
|
}
|
|
3247
|
-
return last;
|
|
3524
|
+
return { ...last, attempt: retries + 1 };
|
|
3525
|
+
}
|
|
3526
|
+
/** Registers tasks created for a step as live, so cancellation can terminate them (§9.2). */
|
|
3527
|
+
trackLiveTasks(runId, taskIds) {
|
|
3528
|
+
if (!runId) return;
|
|
3529
|
+
const active = this.activeRuns.get(runId);
|
|
3530
|
+
if (!active) return;
|
|
3531
|
+
for (const id of taskIds) active.taskIds.add(id);
|
|
3248
3532
|
}
|
|
3249
3533
|
async executeStepOnce(step, ctx) {
|
|
3250
3534
|
switch (step.kind) {
|
|
@@ -3277,7 +3561,9 @@ ${contribution}`);
|
|
|
3277
3561
|
})
|
|
3278
3562
|
)
|
|
3279
3563
|
);
|
|
3564
|
+
this.trackLiveTasks(ctx.runId, created.map((t) => t.id));
|
|
3280
3565
|
const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
|
|
3566
|
+
const executionIds = results.map((r) => r.executions?.at(-1)?.executionId).filter((id) => id !== void 0);
|
|
3281
3567
|
const completedTasks = results.filter((r) => r.status === "completed");
|
|
3282
3568
|
const failedTasks = results.filter((r) => r.status !== "completed");
|
|
3283
3569
|
const lines = results.map((result, index) => {
|
|
@@ -3300,7 +3586,13 @@ ${contribution}`);
|
|
|
3300
3586
|
reports
|
|
3301
3587
|
};
|
|
3302
3588
|
}
|
|
3303
|
-
return {
|
|
3589
|
+
return {
|
|
3590
|
+
status: "completed",
|
|
3591
|
+
summary,
|
|
3592
|
+
taskIds: created.map((t) => t.id),
|
|
3593
|
+
...executionIds.length > 0 ? { executionIds } : {},
|
|
3594
|
+
reports
|
|
3595
|
+
};
|
|
3304
3596
|
}
|
|
3305
3597
|
async runSingle(step, ctx, agentName) {
|
|
3306
3598
|
const task = await this.options.tasks.create({
|
|
@@ -3309,6 +3601,7 @@ ${contribution}`);
|
|
|
3309
3601
|
...ctx.role ? { role: ctx.role.id } : {},
|
|
3310
3602
|
...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
|
|
3311
3603
|
});
|
|
3604
|
+
this.trackLiveTasks(ctx.runId, [task.id]);
|
|
3312
3605
|
const result = await this.options.tasks.run(task.id);
|
|
3313
3606
|
if (result.status !== "completed") {
|
|
3314
3607
|
return {
|
|
@@ -3559,6 +3852,24 @@ function registerPipelineCommands(program) {
|
|
|
3559
3852
|
}
|
|
3560
3853
|
await runPipelineAction(def.id, objective ?? previous.objective);
|
|
3561
3854
|
});
|
|
3855
|
+
pipeline.command("cancel <run-id>").description("Request cancellation of a pipeline run (stops scheduling and cancels running tasks).").action(async (runId) => {
|
|
3856
|
+
const config = loadConfig();
|
|
3857
|
+
const engine = buildPipelineEngine(config);
|
|
3858
|
+
await engine.cancel(runId);
|
|
3859
|
+
console.log(`Cancellation requested for run ${runId}.`);
|
|
3860
|
+
console.log("A run executing in another process stops at its next check point.");
|
|
3861
|
+
});
|
|
3862
|
+
pipeline.command("validate <id>").description("Validate a pipeline definition before running it (\xA79.5).").action(async (id) => {
|
|
3863
|
+
const config = loadConfig();
|
|
3864
|
+
const def = findPipeline(config, id);
|
|
3865
|
+
const engine = buildPipelineEngine(config);
|
|
3866
|
+
engine.validate(def);
|
|
3867
|
+
console.log(`Pipeline "${def.id}" is valid (${def.steps.length} step(s)).`);
|
|
3868
|
+
for (const step of def.steps) {
|
|
3869
|
+
const deps = step.dependsOn?.join(", ") ?? "(previous step)";
|
|
3870
|
+
console.log(` ${step.id} kind=${step.kind} depends_on=${deps}`);
|
|
3871
|
+
}
|
|
3872
|
+
});
|
|
3562
3873
|
pipeline.command("runs <id>").description("List recorded runs of a pipeline.").action(async (id) => {
|
|
3563
3874
|
const config = loadConfig();
|
|
3564
3875
|
const engine = buildPipelineEngine(config);
|
|
@@ -3568,7 +3879,8 @@ function registerPipelineCommands(program) {
|
|
|
3568
3879
|
return;
|
|
3569
3880
|
}
|
|
3570
3881
|
for (const run2 of runs) {
|
|
3571
|
-
|
|
3882
|
+
const note = isInterrupted(run2) ? " (interrupted \u2014 process gone before finish)" : "";
|
|
3883
|
+
console.log(`${run2.id} ${run2.status.padEnd(10)} ${run2.startedAt ?? "?"}${note}`);
|
|
3572
3884
|
}
|
|
3573
3885
|
});
|
|
3574
3886
|
pipeline.command("status <run-id>").description("Show the recorded state of a pipeline run.").action(async (runId) => {
|
|
@@ -3586,6 +3898,13 @@ function registerPipelineCommands(program) {
|
|
|
3586
3898
|
console.log(` status: ${run2.status}`);
|
|
3587
3899
|
console.log(` started: ${run2.startedAt ?? "?"}`);
|
|
3588
3900
|
if (run2.finishedAt) console.log(` finished: ${run2.finishedAt}`);
|
|
3901
|
+
console.log(` attempt: ${run2.attempt}`);
|
|
3902
|
+
if (run2.cancellationRequested) {
|
|
3903
|
+
console.log(` cancellation requested at ${run2.cancellationRequestedAt ?? "?"}`);
|
|
3904
|
+
}
|
|
3905
|
+
if (isInterrupted(run2)) {
|
|
3906
|
+
console.log(" note: this run looks interrupted (no finishedAt) \u2014 retry with `pipeline retry`");
|
|
3907
|
+
}
|
|
3589
3908
|
if (run2.resultSummary) console.log(` result: ${run2.resultSummary}`);
|
|
3590
3909
|
for (const stepRun of run2.stepRuns) {
|
|
3591
3910
|
console.log(` [${stepRun.stepId}] ${stepRun.status}`);
|
|
@@ -3627,6 +3946,9 @@ async function runPipelineAction(id, objective) {
|
|
|
3627
3946
|
function registerRunAlias(program) {
|
|
3628
3947
|
program.command("run <id> [objective]").description("Alias of `omninode pipeline run` \u2014 execute a configured pipeline end to end.").action(async (id, objective) => runPipelineAction(id, objective));
|
|
3629
3948
|
}
|
|
3949
|
+
function isInterrupted(run2) {
|
|
3950
|
+
return run2.status === "running" && run2.finishedAt === void 0;
|
|
3951
|
+
}
|
|
3630
3952
|
function findPipeline(config, id) {
|
|
3631
3953
|
const def = config.project.pipelines.find((p) => p.id === id);
|
|
3632
3954
|
if (!def) {
|