@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/dist/index.js CHANGED
@@ -140,6 +140,11 @@ var agentConfigSchema = z.object({
140
140
  input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
141
141
  timeout_ms: z.number().int().positive().optional(),
142
142
  inherit_env: z.boolean().optional(),
143
+ env_policy: z.enum(["inherit", "allowlist", "denylist", "explicit"]).optional(),
144
+ env_allowlist: z.array(z.string()).optional(),
145
+ env_denylist: z.array(z.string()).optional(),
146
+ max_output_bytes: z.number().int().positive().optional(),
147
+ allow_external_cwd: z.boolean().optional(),
143
148
  metadata: z.record(z.unknown()).optional()
144
149
  }).strict();
145
150
  var roleConfigSchema = z.object({
@@ -314,6 +319,11 @@ function toAgentConfig(raw) {
314
319
  inputMode: raw.input_mode,
315
320
  timeoutMs: raw.timeout_ms,
316
321
  inheritEnv: raw.inherit_env,
322
+ envPolicy: raw.env_policy,
323
+ envAllowlist: raw.env_allowlist,
324
+ envDenylist: raw.env_denylist,
325
+ maxOutputBytes: raw.max_output_bytes,
326
+ allowExternalCwd: raw.allow_external_cwd,
317
327
  metadata: raw.metadata
318
328
  };
319
329
  }
@@ -1010,20 +1020,94 @@ function requireCompletion(session) {
1010
1020
  }
1011
1021
  }
1012
1022
 
1013
- // src/agents/process/index.ts
1014
- var DEFAULT_TIMEOUT_MS2 = 6e5;
1015
- var KILL_GRACE_MS = 5e3;
1023
+ // src/agents/cwd.ts
1024
+ import { statSync } from "fs";
1025
+ import { isAbsolute, relative, resolve } from "path";
1026
+ function validateWorkingDirectory(cwd, options = {}) {
1027
+ if (!cwd) return void 0;
1028
+ const normalized = isAbsolute(cwd) ? resolve(cwd) : resolve(process.cwd(), cwd);
1029
+ let stats;
1030
+ try {
1031
+ stats = statSync(normalized);
1032
+ } catch (error) {
1033
+ throw new AgentError(
1034
+ "AGENT_NOT_FOUND",
1035
+ `Working directory "${normalized}" does not exist or is not accessible.`,
1036
+ { cause: error, details: { cwd: normalized } }
1037
+ );
1038
+ }
1039
+ if (!stats.isDirectory()) {
1040
+ throw new AgentError("AGENT_NOT_FOUND", `Working directory "${normalized}" is not a directory.`, {
1041
+ details: { cwd: normalized }
1042
+ });
1043
+ }
1044
+ try {
1045
+ statSync(resolve(normalized, "."));
1046
+ } catch (error) {
1047
+ throw new AgentError(
1048
+ "AGENT_NOT_FOUND",
1049
+ `Working directory "${normalized}" is not accessible.`,
1050
+ { cause: error }
1051
+ );
1052
+ }
1053
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
1054
+ const rel = relative(projectRoot, normalized);
1055
+ const outsideProject = rel.startsWith("..") || isAbsolute(rel);
1056
+ if (outsideProject && options.allowExternal !== true) {
1057
+ throw new AgentError(
1058
+ "AGENT_NOT_FOUND",
1059
+ `Working directory "${normalized}" is outside the project root "${projectRoot}". Set allow_external_cwd: true on the agent to permit this.`,
1060
+ { details: { cwd: normalized, projectRoot } }
1061
+ );
1062
+ }
1063
+ return { cwd: normalized, outsideProject };
1064
+ }
1065
+
1066
+ // src/agents/env.ts
1067
+ var BASELINE_KEYS = ["PATH", "HOME"];
1068
+ function resolveEnvPolicy(config) {
1069
+ if (config.envPolicy !== void 0) return config.envPolicy;
1070
+ if (config.inheritEnv === false) return "explicit";
1071
+ return "inherit";
1072
+ }
1016
1073
  function buildChildEnv(config) {
1017
1074
  const configured = config.env ?? {};
1018
- if (config.inheritEnv !== false) {
1075
+ const policy = resolveEnvPolicy(config);
1076
+ if (policy === "inherit") {
1019
1077
  return { ...process.env, ...configured };
1020
1078
  }
1021
- return {
1022
- PATH: process.env.PATH ?? "",
1023
- HOME: process.env.HOME ?? "",
1024
- ...configured
1025
- };
1079
+ const base = {};
1080
+ for (const key of BASELINE_KEYS) {
1081
+ const value = process.env[key];
1082
+ if (value !== void 0) base[key] = value;
1083
+ }
1084
+ switch (policy) {
1085
+ case "explicit":
1086
+ return { ...base, ...configured };
1087
+ case "allowlist": {
1088
+ const allowed = { ...base };
1089
+ for (const key of config.envAllowlist ?? []) {
1090
+ const value = process.env[key];
1091
+ if (value !== void 0) allowed[key] = value;
1092
+ }
1093
+ return { ...allowed, ...configured };
1094
+ }
1095
+ case "denylist": {
1096
+ const denied = new Set((config.envDenylist ?? []).map((k) => k.toUpperCase()));
1097
+ const filtered = {};
1098
+ for (const [key, value] of Object.entries(process.env)) {
1099
+ if (denied.has(key.toUpperCase())) continue;
1100
+ filtered[key] = value;
1101
+ }
1102
+ return { ...filtered, ...configured };
1103
+ }
1104
+ }
1026
1105
  }
1106
+
1107
+ // src/agents/process/index.ts
1108
+ var DEFAULT_TIMEOUT_MS2 = 6e5;
1109
+ var KILL_GRACE_MS = 5e3;
1110
+ var DEFAULT_MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
1027
1111
  function composeTaskText(input) {
1028
1112
  const sections = [`# Objective
1029
1113
  ${input.objective}`];
@@ -1085,40 +1169,73 @@ var ProcessAgent = class {
1085
1169
  }
1086
1170
  const mode = this.config.inputMode ?? "stdin";
1087
1171
  const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
1088
- return new Promise((resolve, reject) => {
1172
+ return new Promise((resolve2, reject) => {
1173
+ let cwd;
1174
+ try {
1175
+ cwd = validateWorkingDirectory(this.config.cwd, {
1176
+ allowExternal: this.config.allowExternalCwd
1177
+ })?.cwd;
1178
+ } catch (error) {
1179
+ reject(error);
1180
+ return;
1181
+ }
1089
1182
  const child = spawn(
1090
1183
  command,
1091
1184
  [...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
1092
1185
  {
1093
- cwd: this.config.cwd,
1186
+ ...cwd !== void 0 ? { cwd } : {},
1094
1187
  env: buildChildEnv(this.config),
1095
- stdio: ["pipe", "pipe", "pipe"]
1188
+ stdio: ["pipe", "pipe", "pipe"],
1189
+ // Own process group so the whole tree can be terminated together
1190
+ // (roadmap §8: no orphaned children).
1191
+ ...process.platform === "win32" ? {} : { detached: true }
1096
1192
  }
1097
1193
  );
1098
1194
  this.running.set(input.taskId, child);
1195
+ GLOBAL_RUNNING.add(child);
1196
+ installSignalForwarding();
1099
1197
  let stdout = "";
1100
1198
  let stderr = "";
1101
1199
  let timedOut = false;
1200
+ let outputLimitExceeded = false;
1102
1201
  let settled = false;
1202
+ const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
1103
1203
  const timer = setTimeout(() => {
1104
1204
  timedOut = true;
1105
- this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
1106
- child.kill("SIGTERM");
1107
- setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
1205
+ this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process tree.`);
1206
+ killTree(child, "SIGTERM");
1207
+ setTimeout(() => killTree(child, "SIGKILL"), KILL_GRACE_MS).unref();
1108
1208
  }, timeoutMs);
1109
1209
  const settle = (finish) => {
1110
1210
  if (settled) return;
1111
1211
  settled = true;
1112
1212
  clearTimeout(timer);
1113
1213
  this.running.delete(input.taskId);
1214
+ GLOBAL_RUNNING.delete(child);
1114
1215
  finish();
1115
1216
  };
1116
- child.stdout?.on("data", (chunk) => {
1117
- stdout += chunk.toString();
1118
- });
1119
- child.stderr?.on("data", (chunk) => {
1120
- stderr += chunk.toString();
1121
- });
1217
+ const collect = (into, chunk) => {
1218
+ if (outputLimitExceeded) return;
1219
+ const text = sanitizeChunk(chunk);
1220
+ if (into === "stdout") {
1221
+ if (stdout.length + text.length > maxOutputBytes) {
1222
+ stdout = (stdout + text).slice(0, maxOutputBytes);
1223
+ outputLimitExceeded = true;
1224
+ this.log.warn(`Agent "${this.config.name}" exceeded the ${maxOutputBytes} byte output limit \u2014 killing it.`);
1225
+ killTree(child, "SIGKILL");
1226
+ return;
1227
+ }
1228
+ stdout += text;
1229
+ } else {
1230
+ if (stderr.length + text.length > maxOutputBytes) {
1231
+ stderr = (stderr + text).slice(0, maxOutputBytes);
1232
+ return;
1233
+ }
1234
+ stderr += text;
1235
+ }
1236
+ };
1237
+ child.stdout?.on("data", (chunk) => collect("stdout", chunk));
1238
+ child.stderr?.on("data", (chunk) => collect("stderr", chunk));
1122
1239
  child.stdin?.on("error", () => {
1123
1240
  });
1124
1241
  if (mode !== "arg") {
@@ -1172,7 +1289,7 @@ var ProcessAgent = class {
1172
1289
  const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
1173
1290
  if (mode === "protocol") {
1174
1291
  try {
1175
- resolve(
1292
+ resolve2(
1176
1293
  interpretProtocolRun({
1177
1294
  input,
1178
1295
  agentName: this.config.name,
@@ -1187,14 +1304,14 @@ var ProcessAgent = class {
1187
1304
  return;
1188
1305
  }
1189
1306
  const trimmed = stdout.trim();
1190
- resolve({
1307
+ resolve2({
1191
1308
  taskId: input.taskId,
1192
- status: code === 0 ? "completed" : "failed",
1309
+ status: code === 0 && !outputLimitExceeded ? "completed" : "failed",
1193
1310
  summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
1194
1311
  rawOutput: trimmed.length > 0 ? trimmed : void 0,
1195
1312
  exitCode: code ?? void 0,
1196
1313
  logs,
1197
- error: code === 0 ? void 0 : `Process exited with code ${code ?? "unknown"}${signal ? ` (signal: ${signal})` : ""}`
1314
+ ...outputLimitExceeded ? { error: `Agent exceeded the ${maxOutputBytes} byte output limit and was terminated.` } : code === 0 ? {} : { error: describeExit(code, signal) }
1198
1315
  });
1199
1316
  });
1200
1317
  });
@@ -1203,7 +1320,7 @@ var ProcessAgent = class {
1203
1320
  /** Ask the agent to stop current work (SIGTERM). */
1204
1321
  async cancel(taskId) {
1205
1322
  const child = this.running.get(taskId);
1206
- if (child) child.kill("SIGTERM");
1323
+ if (child) killTree(child, "SIGTERM");
1207
1324
  }
1208
1325
  runningTaskIds() {
1209
1326
  return [...this.running.keys()];
@@ -1278,6 +1395,43 @@ function interpretProtocolRun({
1278
1395
  ...exitCode !== 0 ? { error: `Process exited with code ${exitCode ?? "unknown"}` } : {}
1279
1396
  };
1280
1397
  }
1398
+ var GLOBAL_RUNNING = /* @__PURE__ */ new Set();
1399
+ var signalForwardingInstalled = false;
1400
+ function installSignalForwarding() {
1401
+ if (signalForwardingInstalled) return;
1402
+ signalForwardingInstalled = true;
1403
+ for (const signal of ["SIGINT", "SIGTERM"]) {
1404
+ process.on(signal, () => {
1405
+ for (const child of GLOBAL_RUNNING) {
1406
+ killTree(child, "SIGTERM");
1407
+ }
1408
+ });
1409
+ }
1410
+ }
1411
+ function killTree(child, signal) {
1412
+ if (child.pid === void 0) return;
1413
+ if (process.platform !== "win32") {
1414
+ try {
1415
+ process.kill(-child.pid, signal);
1416
+ return;
1417
+ } catch {
1418
+ }
1419
+ }
1420
+ try {
1421
+ child.kill(signal);
1422
+ } catch {
1423
+ }
1424
+ }
1425
+ function describeExit(code, signal) {
1426
+ if (signal !== null) {
1427
+ return signal === "SIGKILL" ? "Process was killed (SIGKILL) \u2014 usually an external kill or OOM." : `Process terminated by signal ${signal}.`;
1428
+ }
1429
+ if (code === null) return "Process terminated without an exit code.";
1430
+ return `Process exited with code ${code}.`;
1431
+ }
1432
+ function sanitizeChunk(chunk) {
1433
+ return [...chunk.toString("utf8")].filter((char) => char.charCodeAt(0) !== 0).join("");
1434
+ }
1281
1435
  function normalizeReport(payload, taskId, agentName, index) {
1282
1436
  if (payload === null || typeof payload !== "object") return void 0;
1283
1437
  const raw = payload;
@@ -1297,6 +1451,40 @@ function firstLine(text, maxLength = 200) {
1297
1451
  return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
1298
1452
  }
1299
1453
 
1454
+ // src/agents/adapters.ts
1455
+ var AgentAdapterRegistry = class {
1456
+ adapters = /* @__PURE__ */ new Map();
1457
+ register(adapter) {
1458
+ this.adapters.set(adapter.integration, adapter);
1459
+ }
1460
+ get(integration) {
1461
+ return this.adapters.get(integration);
1462
+ }
1463
+ list() {
1464
+ return [...this.adapters.values()];
1465
+ }
1466
+ create(config) {
1467
+ const adapter = this.get(config.integration);
1468
+ if (!adapter) {
1469
+ throw new AgentError(
1470
+ "AGENT_NOT_FOUND",
1471
+ `No agent adapter registered for integration "${config.integration}". Registered: ${[...this.adapters.keys()].join(", ") || "(none)"}.`
1472
+ );
1473
+ }
1474
+ return adapter.create(config);
1475
+ }
1476
+ };
1477
+ var ProcessAgentAdapter = class {
1478
+ constructor(factory) {
1479
+ this.factory = factory;
1480
+ }
1481
+ factory;
1482
+ integration = "process";
1483
+ create(config) {
1484
+ return this.factory(config);
1485
+ }
1486
+ };
1487
+
1300
1488
  // src/agents/registry.ts
1301
1489
  var AgentRegistry = class {
1302
1490
  agents = /* @__PURE__ */ new Map();
@@ -1312,14 +1500,15 @@ var AgentRegistry = class {
1312
1500
  };
1313
1501
 
1314
1502
  // src/agents/factory.ts
1315
- function createAgent(config) {
1316
- if (config.integration === "process") {
1317
- return new ProcessAgent(config);
1318
- }
1503
+ var defaultAgentAdapters = new AgentAdapterRegistry();
1504
+ defaultAgentAdapters.register(new ProcessAgentAdapter((config) => new ProcessAgent(config)));
1505
+ function createAgent(config, adapters = defaultAgentAdapters) {
1506
+ const registered = adapters.get(config.integration);
1507
+ if (registered) return registered.create(config);
1319
1508
  if (config.integration === "native") {
1320
1509
  throw new OmniNodeError(
1321
1510
  "NOT_IMPLEMENTED",
1322
- `Native agent adapters are planned for a later phase. Use input over stdin/stdout (type "cli") for "${config.name}" for now.`
1511
+ `No native adapter is registered for "${config.name}". Register an IAgentAdapter (createAgent(config, adapters)) or use input over stdin/stdout (type "cli").`
1323
1512
  );
1324
1513
  }
1325
1514
  throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
@@ -1588,6 +1777,11 @@ var TaskEngine = class {
1588
1777
  if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
1589
1778
  return current;
1590
1779
  }
1780
+ const currentState = await this.options.store.get(current.id);
1781
+ if (currentState?.status === "cancelled") {
1782
+ current = currentState;
1783
+ break;
1784
+ }
1591
1785
  const result = {
1592
1786
  ...output.summary !== void 0 ? { summary: output.summary } : {},
1593
1787
  ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
@@ -1699,7 +1893,7 @@ function classifyThrown(error) {
1699
1893
  return "failed";
1700
1894
  }
1701
1895
  function sleep(ms) {
1702
- return new Promise((resolve) => setTimeout(resolve, ms));
1896
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1703
1897
  }
1704
1898
 
1705
1899
  // src/audit/index.ts
@@ -2250,9 +2444,23 @@ var PipelineEngine = class {
2250
2444
  }
2251
2445
  options;
2252
2446
  log;
2447
+ /** Runs executing in THIS process — cancellation can terminate them directly. */
2448
+ activeRuns = /* @__PURE__ */ new Map();
2253
2449
  get store() {
2254
2450
  return this.options.store;
2255
2451
  }
2452
+ /**
2453
+ * Saves the run while preserving fields that may be set by another process
2454
+ * (e.g. a cancellation request) — an in-memory snapshot must never clobber
2455
+ * externally requested cancellation (§9.2).
2456
+ */
2457
+ async persist(run) {
2458
+ const current = await this.options.store.get(run.id);
2459
+ await this.options.store.save({
2460
+ ...run,
2461
+ ...current?.cancellationRequested === true ? { cancellationRequested: true, cancellationRequestedAt: current.cancellationRequestedAt } : {}
2462
+ });
2463
+ }
2256
2464
  /** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
2257
2465
  validate(def) {
2258
2466
  const ids = /* @__PURE__ */ new Set();
@@ -2352,18 +2560,44 @@ var PipelineEngine = class {
2352
2560
  );
2353
2561
  }
2354
2562
  }
2563
+ if (step.model) {
2564
+ const separator = step.model.indexOf(":");
2565
+ if (separator <= 0 || separator === step.model.length - 1) {
2566
+ throw new PipelineError(
2567
+ "PIPELINE_INVALID",
2568
+ `Step "${step.id}" has an invalid model reference "${step.model}" \u2014 use "provider:model-id".`
2569
+ );
2570
+ }
2571
+ const providerName = step.model.slice(0, separator);
2572
+ if (!this.options.providers.some((p) => p.name === providerName)) {
2573
+ throw new PipelineError(
2574
+ "PIPELINE_INVALID",
2575
+ `Step "${step.id}" uses model "${step.model}" but provider "${providerName}" is not configured.`
2576
+ );
2577
+ }
2578
+ }
2355
2579
  }
2356
2580
  const now = (/* @__PURE__ */ new Date()).toISOString();
2357
2581
  const run = {
2358
2582
  id: `run-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
2359
2583
  pipelineId: def.id,
2584
+ attempt: 1,
2360
2585
  objective,
2361
2586
  status: "running",
2362
2587
  startedAt: now,
2363
2588
  stepRuns: []
2364
2589
  };
2365
2590
  await this.options.store.save(run);
2366
- this.log.info(`Running pipeline ${def.id} as ${run.id}.`);
2591
+ const liveTaskIds = /* @__PURE__ */ new Set();
2592
+ this.activeRuns.set(run.id, {
2593
+ taskIds: liveTaskIds,
2594
+ cancelTasks: async () => {
2595
+ for (const taskId of liveTaskIds) {
2596
+ await this.options.tasks.cancel(taskId).catch(() => void 0);
2597
+ }
2598
+ }
2599
+ });
2600
+ this.log.info(`Running pipeline ${def.id} as ${run.id} (attempt ${run.attempt}).`);
2367
2601
  await this.options.audit?.record({
2368
2602
  at: (/* @__PURE__ */ new Date()).toISOString(),
2369
2603
  action: "pipeline.run.started",
@@ -2375,7 +2609,22 @@ var PipelineEngine = class {
2375
2609
  const pending = new Set(def.steps.map((s) => s.id));
2376
2610
  const contextParts = [];
2377
2611
  const allReports = [];
2612
+ let cancelled = false;
2378
2613
  while (pending.size > 0) {
2614
+ const persisted = await this.options.store.get(run.id);
2615
+ if (persisted?.cancellationRequested === true) {
2616
+ cancelled = true;
2617
+ run.cancellationRequested = true;
2618
+ run.cancellationRequestedAt = persisted.cancellationRequestedAt;
2619
+ await this.activeRuns.get(run.id)?.cancelTasks();
2620
+ for (const step of def.steps.filter((s) => pending.has(s.id))) {
2621
+ const stepRun = { stepId: step.id, status: "cancelled" };
2622
+ run.stepRuns.push(stepRun);
2623
+ options.onStep?.(stepRun);
2624
+ }
2625
+ pending.clear();
2626
+ break;
2627
+ }
2379
2628
  const ready = def.steps.filter(
2380
2629
  (step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
2381
2630
  );
@@ -2392,7 +2641,8 @@ var PipelineEngine = class {
2392
2641
  ...role ? { role } : {},
2393
2642
  combinedContext: contextParts.join("\n\n"),
2394
2643
  reports: [...allReports],
2395
- pipelineRunId: run.id
2644
+ pipelineRunId: run.id,
2645
+ runId: run.id
2396
2646
  };
2397
2647
  return this.executeStep(step, ctx);
2398
2648
  })
@@ -2409,6 +2659,8 @@ ${contribution}`);
2409
2659
  }
2410
2660
  const stepRun = {
2411
2661
  stepId: step.id,
2662
+ attempt: outcome.attempt ?? 1,
2663
+ ...outcome.executionIds && outcome.executionIds.length > 0 ? { executionIds: outcome.executionIds } : {},
2412
2664
  status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
2413
2665
  ...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
2414
2666
  ...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
@@ -2418,12 +2670,12 @@ ${contribution}`);
2418
2670
  options.onStep?.(stepRun);
2419
2671
  });
2420
2672
  run.reports = [...allReports];
2421
- await this.options.store.save(run);
2673
+ await this.persist(run);
2422
2674
  }
2423
2675
  const allOutcomes = [...outcomes.values()];
2424
- const failed = allOutcomes.some((o) => o.status === "failed");
2425
- const partial = !failed && allOutcomes.some((o) => o.status === "partial");
2426
- const finalStatus = failed ? "failed" : partial ? "partial" : "completed";
2676
+ const failed = !cancelled && allOutcomes.some((o) => o.status === "failed");
2677
+ const partial = !cancelled && !failed && allOutcomes.some((o) => o.status === "partial");
2678
+ const finalStatus = cancelled ? "cancelled" : failed ? "failed" : partial ? "partial" : "completed";
2427
2679
  const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
2428
2680
  let resultSummary;
2429
2681
  for (let i = def.steps.length - 1; i >= 0; i -= 1) {
@@ -2451,16 +2703,19 @@ ${contribution}`);
2451
2703
  run.reports = collected;
2452
2704
  }
2453
2705
  }
2706
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2707
+ this.activeRuns.delete(run.id);
2454
2708
  const finished = {
2455
2709
  ...run,
2456
2710
  status: finalStatus,
2457
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
2711
+ finishedAt,
2712
+ attempts: [...run.attempts ?? [], { attempt: run.attempt, startedAt: run.startedAt ?? finishedAt, finishedAt, status: finalStatus }],
2458
2713
  ...run.reports !== void 0 && run.reports.length > 0 ? { reports: run.reports } : {},
2459
2714
  ...combinedReportId !== void 0 ? { combinedReportId } : {},
2460
2715
  ...planId !== void 0 ? { planId } : {},
2461
2716
  ...resultSummary !== void 0 ? { resultSummary } : {}
2462
2717
  };
2463
- await this.options.store.save(finished);
2718
+ await this.persist(finished);
2464
2719
  this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
2465
2720
  await this.options.audit?.record({
2466
2721
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2470,6 +2725,23 @@ ${contribution}`);
2470
2725
  });
2471
2726
  return finished;
2472
2727
  }
2728
+ /** First-class cancellation (§9.2): requests cancellation and terminates
2729
+ * tasks that are running in this process immediately. */
2730
+ async cancel(runId) {
2731
+ const run = await this.options.store.get(runId);
2732
+ if (!run) {
2733
+ throw new PipelineError("PIPELINE_NOT_FOUND", `Pipeline run "${runId}" does not exist.`);
2734
+ }
2735
+ await this.options.store.save({
2736
+ ...run,
2737
+ cancellationRequested: true,
2738
+ cancellationRequestedAt: (/* @__PURE__ */ new Date()).toISOString()
2739
+ });
2740
+ const active = this.activeRuns.get(runId);
2741
+ if (active) {
2742
+ await active.cancelTasks();
2743
+ }
2744
+ }
2473
2745
  async get(runId) {
2474
2746
  return this.options.store.get(runId);
2475
2747
  }
@@ -2493,9 +2765,16 @@ ${contribution}`);
2493
2765
  this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
2494
2766
  }
2495
2767
  last = await this.executeStepOnce(step, ctx);
2496
- if (last.status !== "failed") return last;
2768
+ if (last.status !== "failed") return { ...last, attempt: attempt + 1 };
2497
2769
  }
2498
- return last;
2770
+ return { ...last, attempt: retries + 1 };
2771
+ }
2772
+ /** Registers tasks created for a step as live, so cancellation can terminate them (§9.2). */
2773
+ trackLiveTasks(runId, taskIds) {
2774
+ if (!runId) return;
2775
+ const active = this.activeRuns.get(runId);
2776
+ if (!active) return;
2777
+ for (const id of taskIds) active.taskIds.add(id);
2499
2778
  }
2500
2779
  async executeStepOnce(step, ctx) {
2501
2780
  switch (step.kind) {
@@ -2528,7 +2807,9 @@ ${contribution}`);
2528
2807
  })
2529
2808
  )
2530
2809
  );
2810
+ this.trackLiveTasks(ctx.runId, created.map((t) => t.id));
2531
2811
  const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
2812
+ const executionIds = results.map((r) => r.executions?.at(-1)?.executionId).filter((id) => id !== void 0);
2532
2813
  const completedTasks = results.filter((r) => r.status === "completed");
2533
2814
  const failedTasks = results.filter((r) => r.status !== "completed");
2534
2815
  const lines = results.map((result, index) => {
@@ -2551,7 +2832,13 @@ ${contribution}`);
2551
2832
  reports
2552
2833
  };
2553
2834
  }
2554
- return { status: "completed", summary, taskIds: created.map((t) => t.id), reports };
2835
+ return {
2836
+ status: "completed",
2837
+ summary,
2838
+ taskIds: created.map((t) => t.id),
2839
+ ...executionIds.length > 0 ? { executionIds } : {},
2840
+ reports
2841
+ };
2555
2842
  }
2556
2843
  async runSingle(step, ctx, agentName) {
2557
2844
  const task = await this.options.tasks.create({
@@ -2560,6 +2847,7 @@ ${contribution}`);
2560
2847
  ...ctx.role ? { role: ctx.role.id } : {},
2561
2848
  ...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
2562
2849
  });
2850
+ this.trackLiveTasks(ctx.runId, [task.id]);
2563
2851
  const result = await this.options.tasks.run(task.id);
2564
2852
  if (result.status !== "completed") {
2565
2853
  return {
@@ -3037,12 +3325,14 @@ var ProjectStores = class {
3037
3325
  };
3038
3326
 
3039
3327
  // src/version.ts
3040
- var OMNINODE_VERSION = "2.0.0-alpha.3";
3328
+ var OMNINODE_VERSION = "2.0.0-alpha.5";
3041
3329
  export {
3330
+ AgentAdapterRegistry,
3042
3331
  AgentError,
3043
3332
  AgentRegistry,
3044
3333
  ConfigError,
3045
3334
  ConsoleLogSink,
3335
+ DEFAULT_MAX_OUTPUT_BYTES,
3046
3336
  FileAuditLog,
3047
3337
  FilePipelineRunStore,
3048
3338
  FilePlanStore,
@@ -3067,6 +3357,7 @@ export {
3067
3357
  PipelineError,
3068
3358
  PlannerError,
3069
3359
  ProcessAgent,
3360
+ ProcessAgentAdapter,
3070
3361
  ProjectStores,
3071
3362
  ProtocolDecoder,
3072
3363
  ProtocolError,
@@ -3100,6 +3391,7 @@ export {
3100
3391
  decodeMessage,
3101
3392
  decodeStream,
3102
3393
  defaultProjectConfigYaml,
3394
+ describeExit,
3103
3395
  encodeMessage,
3104
3396
  envelope,
3105
3397
  expandEnvRefs,
@@ -3108,6 +3400,7 @@ export {
3108
3400
  isOmniNodeError,
3109
3401
  isSupportedProtocol,
3110
3402
  keywords,
3403
+ killTree,
3111
3404
  loadConfig,
3112
3405
  logger,
3113
3406
  newMessageId,
@@ -3122,8 +3415,11 @@ export {
3122
3415
  requestJson,
3123
3416
  requireCompletion,
3124
3417
  resolveApiKey,
3418
+ resolveEnvPolicy,
3125
3419
  roleConfigSchema,
3420
+ sanitizeChunk,
3126
3421
  similar,
3127
- validateEnvelope
3422
+ validateEnvelope,
3423
+ validateWorkingDirectory
3128
3424
  };
3129
3425
  //# sourceMappingURL=index.js.map