@hilbras/omninode 2.0.0-alpha.4 → 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 CHANGED
@@ -18,9 +18,9 @@ existing intelligence; it does not try to become another model.
18
18
 
19
19
  ## Status
20
20
 
21
- **v2.0.0-alpha.4 — v2 Phase 4: Agent Adapter Hardening**
22
- (after Phase 1: architecture & API stabilization, Phase 2: execution reliability,
23
- Phase 3: agent protocol v2 — the reliability & interoperability line) (the v2
21
+ **v2.0.0-alpha.5 — v2 Phase 5: Pipeline Lifecycle v2**
22
+ (the reliability & interoperability line: architecture → execution →
23
+ protocol → adapters → pipelines) (the v2
24
24
  roadmap is [docs/ROADMAP_V2.md](docs/ROADMAP_V2.md); the v1 plan is complete:
25
25
  the
26
26
  full loop from §28 runs in one command —
@@ -31,6 +31,13 @@ for the plan and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the design).
31
31
 
32
32
  ### What's new in v2 (so far)
33
33
 
34
+ - **Phase 5 — Pipeline Lifecycle**: first-class pipeline cancellation
35
+ (`omninode pipeline cancel <run-id>`: stop scheduling → cancel running
36
+ tasks → terminate agent processes → persist a `cancelled` run), `pipeline
37
+ validate` for pre-run checks (deps, cycles, agents, model references),
38
+ explicit run/step **attempts** in the run record, the
39
+ `IPipelineExecutor` contract, and recovery awareness — runs left `running`
40
+ without a `finishedAt` are flagged as interrupted in `pipeline runs`.
34
41
  - **Phase 4 — Agent Adapter Hardening**: process-tree cleanup (agents run in
35
42
  their own process group; timeout, cancel and Ctrl-C reap the whole tree, so
36
43
  no orphaned children), explicit environment policies
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.4";
9
+ var OMNINODE_VERSION = "2.0.0-alpha.5";
10
10
 
11
11
  // src/errors/index.ts
12
12
  var OmniNodeError = class extends Error {
@@ -1634,7 +1634,7 @@ function describeExit(code, signal) {
1634
1634
  return `Process exited with code ${code}.`;
1635
1635
  }
1636
1636
  function sanitizeChunk(chunk) {
1637
- return chunk.toString("utf8").replace(/\u0000/g, "");
1637
+ return [...chunk.toString("utf8")].filter((char) => char.charCodeAt(0) !== 0).join("");
1638
1638
  }
1639
1639
  function normalizeReport(payload, taskId, agentName, index) {
1640
1640
  if (payload === null || typeof payload !== "object") return void 0;
@@ -2068,6 +2068,11 @@ var TaskEngine = class {
2068
2068
  if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
2069
2069
  return current;
2070
2070
  }
2071
+ const currentState = await this.options.store.get(current.id);
2072
+ if (currentState?.status === "cancelled") {
2073
+ current = currentState;
2074
+ break;
2075
+ }
2071
2076
  const result = {
2072
2077
  ...output.summary !== void 0 ? { summary: output.summary } : {},
2073
2078
  ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
@@ -3193,9 +3198,23 @@ var PipelineEngine = class {
3193
3198
  }
3194
3199
  options;
3195
3200
  log;
3201
+ /** Runs executing in THIS process — cancellation can terminate them directly. */
3202
+ activeRuns = /* @__PURE__ */ new Map();
3196
3203
  get store() {
3197
3204
  return this.options.store;
3198
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
+ }
3199
3218
  /** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
3200
3219
  validate(def) {
3201
3220
  const ids = /* @__PURE__ */ new Set();
@@ -3295,18 +3314,44 @@ var PipelineEngine = class {
3295
3314
  );
3296
3315
  }
3297
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
+ }
3298
3333
  }
3299
3334
  const now = (/* @__PURE__ */ new Date()).toISOString();
3300
3335
  const run2 = {
3301
3336
  id: `run-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
3302
3337
  pipelineId: def.id,
3338
+ attempt: 1,
3303
3339
  objective,
3304
3340
  status: "running",
3305
3341
  startedAt: now,
3306
3342
  stepRuns: []
3307
3343
  };
3308
3344
  await this.options.store.save(run2);
3309
- this.log.info(`Running pipeline ${def.id} as ${run2.id}.`);
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}).`);
3310
3355
  await this.options.audit?.record({
3311
3356
  at: (/* @__PURE__ */ new Date()).toISOString(),
3312
3357
  action: "pipeline.run.started",
@@ -3318,7 +3363,22 @@ var PipelineEngine = class {
3318
3363
  const pending = new Set(def.steps.map((s) => s.id));
3319
3364
  const contextParts = [];
3320
3365
  const allReports = [];
3366
+ let cancelled = false;
3321
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
+ }
3322
3382
  const ready = def.steps.filter(
3323
3383
  (step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
3324
3384
  );
@@ -3335,7 +3395,8 @@ var PipelineEngine = class {
3335
3395
  ...role ? { role } : {},
3336
3396
  combinedContext: contextParts.join("\n\n"),
3337
3397
  reports: [...allReports],
3338
- pipelineRunId: run2.id
3398
+ pipelineRunId: run2.id,
3399
+ runId: run2.id
3339
3400
  };
3340
3401
  return this.executeStep(step, ctx);
3341
3402
  })
@@ -3352,6 +3413,8 @@ ${contribution}`);
3352
3413
  }
3353
3414
  const stepRun = {
3354
3415
  stepId: step.id,
3416
+ attempt: outcome.attempt ?? 1,
3417
+ ...outcome.executionIds && outcome.executionIds.length > 0 ? { executionIds: outcome.executionIds } : {},
3355
3418
  status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
3356
3419
  ...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
3357
3420
  ...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
@@ -3361,12 +3424,12 @@ ${contribution}`);
3361
3424
  options.onStep?.(stepRun);
3362
3425
  });
3363
3426
  run2.reports = [...allReports];
3364
- await this.options.store.save(run2);
3427
+ await this.persist(run2);
3365
3428
  }
3366
3429
  const allOutcomes = [...outcomes.values()];
3367
- const failed = allOutcomes.some((o) => o.status === "failed");
3368
- const partial = !failed && allOutcomes.some((o) => o.status === "partial");
3369
- 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";
3370
3433
  const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
3371
3434
  let resultSummary;
3372
3435
  for (let i = def.steps.length - 1; i >= 0; i -= 1) {
@@ -3394,16 +3457,19 @@ ${contribution}`);
3394
3457
  run2.reports = collected;
3395
3458
  }
3396
3459
  }
3460
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
3461
+ this.activeRuns.delete(run2.id);
3397
3462
  const finished = {
3398
3463
  ...run2,
3399
3464
  status: finalStatus,
3400
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
3465
+ finishedAt,
3466
+ attempts: [...run2.attempts ?? [], { attempt: run2.attempt, startedAt: run2.startedAt ?? finishedAt, finishedAt, status: finalStatus }],
3401
3467
  ...run2.reports !== void 0 && run2.reports.length > 0 ? { reports: run2.reports } : {},
3402
3468
  ...combinedReportId !== void 0 ? { combinedReportId } : {},
3403
3469
  ...planId !== void 0 ? { planId } : {},
3404
3470
  ...resultSummary !== void 0 ? { resultSummary } : {}
3405
3471
  };
3406
- await this.options.store.save(finished);
3472
+ await this.persist(finished);
3407
3473
  this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
3408
3474
  await this.options.audit?.record({
3409
3475
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3413,6 +3479,23 @@ ${contribution}`);
3413
3479
  });
3414
3480
  return finished;
3415
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
+ }
3416
3499
  async get(runId) {
3417
3500
  return this.options.store.get(runId);
3418
3501
  }
@@ -3436,9 +3519,16 @@ ${contribution}`);
3436
3519
  this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
3437
3520
  }
3438
3521
  last = await this.executeStepOnce(step, ctx);
3439
- if (last.status !== "failed") return last;
3522
+ if (last.status !== "failed") return { ...last, attempt: attempt + 1 };
3440
3523
  }
3441
- 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);
3442
3532
  }
3443
3533
  async executeStepOnce(step, ctx) {
3444
3534
  switch (step.kind) {
@@ -3471,7 +3561,9 @@ ${contribution}`);
3471
3561
  })
3472
3562
  )
3473
3563
  );
3564
+ this.trackLiveTasks(ctx.runId, created.map((t) => t.id));
3474
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);
3475
3567
  const completedTasks = results.filter((r) => r.status === "completed");
3476
3568
  const failedTasks = results.filter((r) => r.status !== "completed");
3477
3569
  const lines = results.map((result, index) => {
@@ -3494,7 +3586,13 @@ ${contribution}`);
3494
3586
  reports
3495
3587
  };
3496
3588
  }
3497
- return { status: "completed", summary, taskIds: created.map((t) => t.id), reports };
3589
+ return {
3590
+ status: "completed",
3591
+ summary,
3592
+ taskIds: created.map((t) => t.id),
3593
+ ...executionIds.length > 0 ? { executionIds } : {},
3594
+ reports
3595
+ };
3498
3596
  }
3499
3597
  async runSingle(step, ctx, agentName) {
3500
3598
  const task = await this.options.tasks.create({
@@ -3503,6 +3601,7 @@ ${contribution}`);
3503
3601
  ...ctx.role ? { role: ctx.role.id } : {},
3504
3602
  ...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
3505
3603
  });
3604
+ this.trackLiveTasks(ctx.runId, [task.id]);
3506
3605
  const result = await this.options.tasks.run(task.id);
3507
3606
  if (result.status !== "completed") {
3508
3607
  return {
@@ -3753,6 +3852,24 @@ function registerPipelineCommands(program) {
3753
3852
  }
3754
3853
  await runPipelineAction(def.id, objective ?? previous.objective);
3755
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
+ });
3756
3873
  pipeline.command("runs <id>").description("List recorded runs of a pipeline.").action(async (id) => {
3757
3874
  const config = loadConfig();
3758
3875
  const engine = buildPipelineEngine(config);
@@ -3762,7 +3879,8 @@ function registerPipelineCommands(program) {
3762
3879
  return;
3763
3880
  }
3764
3881
  for (const run2 of runs) {
3765
- console.log(`${run2.id} ${run2.status.padEnd(10)} ${run2.startedAt ?? "?"}`);
3882
+ const note = isInterrupted(run2) ? " (interrupted \u2014 process gone before finish)" : "";
3883
+ console.log(`${run2.id} ${run2.status.padEnd(10)} ${run2.startedAt ?? "?"}${note}`);
3766
3884
  }
3767
3885
  });
3768
3886
  pipeline.command("status <run-id>").description("Show the recorded state of a pipeline run.").action(async (runId) => {
@@ -3780,6 +3898,13 @@ function registerPipelineCommands(program) {
3780
3898
  console.log(` status: ${run2.status}`);
3781
3899
  console.log(` started: ${run2.startedAt ?? "?"}`);
3782
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
+ }
3783
3908
  if (run2.resultSummary) console.log(` result: ${run2.resultSummary}`);
3784
3909
  for (const stepRun of run2.stepRuns) {
3785
3910
  console.log(` [${stepRun.stepId}] ${stepRun.status}`);
@@ -3821,6 +3946,9 @@ async function runPipelineAction(id, objective) {
3821
3946
  function registerRunAlias(program) {
3822
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));
3823
3948
  }
3949
+ function isInterrupted(run2) {
3950
+ return run2.status === "running" && run2.finishedAt === void 0;
3951
+ }
3824
3952
  function findPipeline(config, id) {
3825
3953
  const def = config.project.pipelines.find((p) => p.id === id);
3826
3954
  if (!def) {