@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 +10 -3
- package/dist/cli/index.js +142 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +58 -4
- package/dist/index.js +112 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
22
|
-
(
|
|
23
|
-
|
|
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.
|
|
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").
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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) {
|