@hilbras/omninode 2.0.0-alpha.1 → 2.0.0-alpha.2
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 +1 -1
- package/dist/cli/index.js +140 -49
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +46 -7
- package/dist/index.js +140 -48
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ existing intelligence; it does not try to become another model.
|
|
|
18
18
|
|
|
19
19
|
## Status
|
|
20
20
|
|
|
21
|
-
**v2.0.0-alpha.
|
|
21
|
+
**v2.0.0-alpha.2 — v2 Phase 2: Execution Reliability** (after Phase 1: architecture & API stabilization) (the v2
|
|
22
22
|
roadmap is [docs/ROADMAP_V2.md](docs/ROADMAP_V2.md); the v1 plan is complete:
|
|
23
23
|
the
|
|
24
24
|
full loop from §28 runs in one command —
|
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.2";
|
|
10
10
|
|
|
11
11
|
// src/errors/index.ts
|
|
12
12
|
var OmniNodeError = class extends Error {
|
|
@@ -1037,7 +1037,19 @@ var ProcessAgent = class {
|
|
|
1037
1037
|
new AgentError(
|
|
1038
1038
|
"AGENT_TIMEOUT",
|
|
1039
1039
|
`Agent "${this.config.name}" timed out after ${timeoutMs}ms and was killed.`,
|
|
1040
|
-
{
|
|
1040
|
+
{
|
|
1041
|
+
details: {
|
|
1042
|
+
operation: "agent.run",
|
|
1043
|
+
agent: this.config.name,
|
|
1044
|
+
durationMs: timeoutMs,
|
|
1045
|
+
// The process was started and may have performed side
|
|
1046
|
+
// effects before being killed — the outcome is unknown,
|
|
1047
|
+
// not failed (roadmap §6.3).
|
|
1048
|
+
dispatched: true,
|
|
1049
|
+
stdout: stdout.slice(0, 4e3),
|
|
1050
|
+
stderr: stderr.slice(0, 2e3)
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1041
1053
|
)
|
|
1042
1054
|
);
|
|
1043
1055
|
return;
|
|
@@ -1397,12 +1409,18 @@ import { randomBytes } from "crypto";
|
|
|
1397
1409
|
var ALLOWED_TRANSITIONS = {
|
|
1398
1410
|
created: ["queued", "running", "cancelled"],
|
|
1399
1411
|
queued: ["running", "cancelled"],
|
|
1400
|
-
running: ["completed", "failed", "cancelled"],
|
|
1412
|
+
running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
|
|
1401
1413
|
waiting: ["running", "cancelled"],
|
|
1402
1414
|
completed: [],
|
|
1403
|
-
failed:
|
|
1404
|
-
|
|
1415
|
+
// failed → running is the only re-entry: bounded automatic retry (§6.4).
|
|
1416
|
+
failed: ["running"],
|
|
1417
|
+
timed_out: [],
|
|
1418
|
+
cancelled: [],
|
|
1419
|
+
unknown: [],
|
|
1420
|
+
partially_completed: []
|
|
1405
1421
|
};
|
|
1422
|
+
var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
|
|
1423
|
+
var EXECUTION_HISTORY_LIMIT = 10;
|
|
1406
1424
|
var TaskEngine = class {
|
|
1407
1425
|
constructor(options) {
|
|
1408
1426
|
this.options = options;
|
|
@@ -1434,6 +1452,7 @@ var TaskEngine = class {
|
|
|
1434
1452
|
id: generateTaskId(),
|
|
1435
1453
|
objective: input.objective.trim(),
|
|
1436
1454
|
status: "created",
|
|
1455
|
+
attempt: 0,
|
|
1437
1456
|
createdAt: now,
|
|
1438
1457
|
updatedAt: now,
|
|
1439
1458
|
...input.project !== void 0 ? { project: input.project } : {},
|
|
@@ -1450,8 +1469,10 @@ var TaskEngine = class {
|
|
|
1450
1469
|
const task = await this.mustGet(id);
|
|
1451
1470
|
return this.transition(task, "queued");
|
|
1452
1471
|
}
|
|
1453
|
-
/** Runs the task through its assigned agent and records the result.
|
|
1454
|
-
|
|
1472
|
+
/** Runs the task through its assigned agent and records the result.
|
|
1473
|
+
* `maxAttempts` enables bounded automatic retries of *retryable* errors
|
|
1474
|
+
* with linear backoff; unknown/timed_out outcomes are never auto-retried. */
|
|
1475
|
+
async run(id, options = {}) {
|
|
1455
1476
|
const task = await this.mustGet(id);
|
|
1456
1477
|
if (task.status !== "created" && task.status !== "queued") {
|
|
1457
1478
|
throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
|
|
@@ -1464,49 +1485,84 @@ var TaskEngine = class {
|
|
|
1464
1485
|
throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
|
|
1465
1486
|
}
|
|
1466
1487
|
const role = task.role ? this.options.roles.get(task.role) : void 0;
|
|
1467
|
-
const running = await this.transition(task, "running");
|
|
1468
1488
|
this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
|
|
1469
|
-
let background =
|
|
1489
|
+
let background = task.context?.background;
|
|
1470
1490
|
if (this.options.memory) {
|
|
1471
1491
|
const memoryContext = await this.options.memory.gatherContext({
|
|
1472
|
-
objective:
|
|
1473
|
-
...
|
|
1474
|
-
...
|
|
1492
|
+
objective: task.objective,
|
|
1493
|
+
...task.role ? { role: task.role } : {},
|
|
1494
|
+
...task.project ? { project: task.project } : {}
|
|
1475
1495
|
});
|
|
1476
1496
|
if (memoryContext.length > 0) {
|
|
1477
1497
|
background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
|
|
1478
1498
|
}
|
|
1479
1499
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
});
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1500
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? 1);
|
|
1501
|
+
const backoffMs = options.retryBackoffMs ?? 0;
|
|
1502
|
+
let current = task;
|
|
1503
|
+
for (let i = 0; i < maxAttempts; i += 1) {
|
|
1504
|
+
const attempt = current.attempt + 1;
|
|
1505
|
+
const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
1506
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1507
|
+
current = await this.transition(current, "running", { attempt });
|
|
1508
|
+
let output;
|
|
1509
|
+
let thrown;
|
|
1510
|
+
try {
|
|
1511
|
+
output = await agent.run({
|
|
1512
|
+
taskId: current.id,
|
|
1513
|
+
objective: current.objective,
|
|
1514
|
+
...role ? { role } : {},
|
|
1515
|
+
...background !== void 0 ? { context: background } : {}
|
|
1516
|
+
});
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
thrown = error;
|
|
1519
|
+
}
|
|
1520
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1521
|
+
if (thrown !== void 0 || output === void 0) {
|
|
1522
|
+
const outcome = classifyThrown(thrown);
|
|
1523
|
+
const message = thrown instanceof Error ? thrown.message : String(thrown);
|
|
1524
|
+
const retryable = outcome === "failed" && RETRYABLE_ERROR_CODES.includes(errorCode(thrown)) && i < maxAttempts - 1;
|
|
1525
|
+
current = await this.transition(current, outcome, {
|
|
1526
|
+
attempt,
|
|
1527
|
+
lastError: message,
|
|
1528
|
+
execution: {
|
|
1529
|
+
executionId,
|
|
1530
|
+
attempt,
|
|
1531
|
+
startedAt,
|
|
1532
|
+
finishedAt,
|
|
1533
|
+
outcome,
|
|
1534
|
+
error: message
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
|
|
1538
|
+
if (retryable) {
|
|
1539
|
+
if (backoffMs > 0) await sleep(backoffMs * attempt);
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
|
|
1543
|
+
return current;
|
|
1544
|
+
}
|
|
1545
|
+
const result = {
|
|
1546
|
+
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
1547
|
+
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
1548
|
+
// Keep the verbatim output (bounded) for report extraction and audit.
|
|
1549
|
+
...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
|
|
1550
|
+
...output.error !== void 0 ? { error: output.error } : {},
|
|
1551
|
+
finishedAt
|
|
1552
|
+
};
|
|
1553
|
+
const finalStatus = output.status === "completed" ? "completed" : "failed";
|
|
1554
|
+
current = await this.transition(current, finalStatus, {
|
|
1555
|
+
attempt,
|
|
1556
|
+
result,
|
|
1557
|
+
execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
|
|
1493
1558
|
});
|
|
1559
|
+
this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
|
|
1560
|
+
break;
|
|
1494
1561
|
}
|
|
1495
|
-
const result = {
|
|
1496
|
-
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
1497
|
-
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
1498
|
-
// Keep the verbatim output (bounded) for report extraction and audit.
|
|
1499
|
-
...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
|
|
1500
|
-
...output.error !== void 0 ? { error: output.error } : {},
|
|
1501
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1502
|
-
};
|
|
1503
|
-
const finalStatus = output.status === "completed" ? "completed" : "failed";
|
|
1504
|
-
const finished = await this.transition(running, finalStatus, { result });
|
|
1505
|
-
this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
|
|
1506
1562
|
if (this.options.memory) {
|
|
1507
|
-
await this.options.memory.recordTaskOutcome(
|
|
1563
|
+
await this.options.memory.recordTaskOutcome(current);
|
|
1508
1564
|
}
|
|
1509
|
-
return
|
|
1565
|
+
return current;
|
|
1510
1566
|
}
|
|
1511
1567
|
async cancel(id) {
|
|
1512
1568
|
const task = await this.mustGet(id);
|
|
@@ -1519,13 +1575,14 @@ var TaskEngine = class {
|
|
|
1519
1575
|
}
|
|
1520
1576
|
return this.transition(task, "cancelled");
|
|
1521
1577
|
}
|
|
1522
|
-
/** Error recovery
|
|
1578
|
+
/** Error recovery: reset a failed/cancelled/unknown/timed_out task so it can run again. */
|
|
1523
1579
|
async retry(id) {
|
|
1524
1580
|
const task = await this.mustGet(id);
|
|
1525
|
-
|
|
1581
|
+
const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
|
|
1582
|
+
if (!retryableStates.includes(task.status)) {
|
|
1526
1583
|
throw new TaskError(
|
|
1527
1584
|
"TASK_INVALID",
|
|
1528
|
-
`Only failed or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
|
|
1585
|
+
`Only failed, timed out, unknown or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
|
|
1529
1586
|
);
|
|
1530
1587
|
}
|
|
1531
1588
|
const { result: _discarded, ...rest } = task;
|
|
@@ -1557,10 +1614,14 @@ var TaskEngine = class {
|
|
|
1557
1614
|
if (!allowed.includes(to)) {
|
|
1558
1615
|
throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
|
|
1559
1616
|
}
|
|
1617
|
+
const executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
|
|
1560
1618
|
const updated = {
|
|
1561
1619
|
...task,
|
|
1562
1620
|
status: to,
|
|
1563
1621
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1622
|
+
...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
|
|
1623
|
+
...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
|
|
1624
|
+
...executions !== void 0 ? { executions } : {},
|
|
1564
1625
|
...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
|
|
1565
1626
|
};
|
|
1566
1627
|
await this.options.store.save(updated);
|
|
@@ -1581,6 +1642,19 @@ var TaskEngine = class {
|
|
|
1581
1642
|
function generateTaskId() {
|
|
1582
1643
|
return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
1583
1644
|
}
|
|
1645
|
+
function errorCode(error) {
|
|
1646
|
+
return error?.code ?? "";
|
|
1647
|
+
}
|
|
1648
|
+
function classifyThrown(error) {
|
|
1649
|
+
if (error instanceof AgentError && error.code === "AGENT_TIMEOUT") {
|
|
1650
|
+
const dispatched = error.details?.dispatched;
|
|
1651
|
+
return dispatched === false ? "timed_out" : "unknown";
|
|
1652
|
+
}
|
|
1653
|
+
return "failed";
|
|
1654
|
+
}
|
|
1655
|
+
function sleep(ms) {
|
|
1656
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1657
|
+
}
|
|
1584
1658
|
|
|
1585
1659
|
// src/audit/index.ts
|
|
1586
1660
|
import { appendFile, mkdir as mkdir2 } from "fs/promises";
|
|
@@ -1980,7 +2054,7 @@ function registerTaskCommands(program) {
|
|
|
1980
2054
|
const cancelled = await engine.cancel(taskId);
|
|
1981
2055
|
console.log(`Task ${cancelled.id} is now "${cancelled.status}".`);
|
|
1982
2056
|
});
|
|
1983
|
-
task.command("retry <task-id>").description("Reset a failed
|
|
2057
|
+
task.command("retry <task-id>").description("Reset a failed, timed out, unknown or cancelled task to created; combine with --run to execute it again.").option("--run", "Run the task again immediately after resetting it.").action(async (taskId, options) => {
|
|
1984
2058
|
const config = loadConfig();
|
|
1985
2059
|
const engine = createTaskEngine(config);
|
|
1986
2060
|
const reset = await engine.retry(taskId);
|
|
@@ -2003,6 +2077,15 @@ function printOutcome(task, options = {}) {
|
|
|
2003
2077
|
if (task.context?.files) console.log(` files: ${task.context.files.join(", ")}`);
|
|
2004
2078
|
console.log(` objective: ${task.objective}`);
|
|
2005
2079
|
}
|
|
2080
|
+
if (options.verbose && task.attempt > 0) {
|
|
2081
|
+
console.log(` attempt: ${task.attempt}`);
|
|
2082
|
+
}
|
|
2083
|
+
if (task.lastError) console.log(` error: ${task.lastError}`);
|
|
2084
|
+
if (options.verbose && task.executions && task.executions.length > 0) {
|
|
2085
|
+
console.log(
|
|
2086
|
+
` executions: ${task.executions.map((e) => `#${e.attempt}:${e.outcome}`).join(", ")}`
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2006
2089
|
const result = task.result;
|
|
2007
2090
|
if (!result) return;
|
|
2008
2091
|
if (result.summary) console.log(` summary: ${result.summary}`);
|
|
@@ -2743,7 +2826,7 @@ ${contribution}`);
|
|
|
2743
2826
|
}
|
|
2744
2827
|
const stepRun = {
|
|
2745
2828
|
stepId: step.id,
|
|
2746
|
-
status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
|
|
2829
|
+
status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
|
|
2747
2830
|
...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
|
|
2748
2831
|
...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
|
|
2749
2832
|
...outcome.error !== void 0 ? { error: outcome.error } : {}
|
|
@@ -2754,8 +2837,10 @@ ${contribution}`);
|
|
|
2754
2837
|
run2.reports = [...allReports];
|
|
2755
2838
|
await this.options.store.save(run2);
|
|
2756
2839
|
}
|
|
2757
|
-
const
|
|
2758
|
-
const
|
|
2840
|
+
const allOutcomes = [...outcomes.values()];
|
|
2841
|
+
const failed = allOutcomes.some((o) => o.status === "failed");
|
|
2842
|
+
const partial = !failed && allOutcomes.some((o) => o.status === "partial");
|
|
2843
|
+
const finalStatus = failed ? "failed" : partial ? "partial" : "completed";
|
|
2759
2844
|
const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
|
|
2760
2845
|
let resultSummary;
|
|
2761
2846
|
for (let i = def.steps.length - 1; i >= 0; i -= 1) {
|
|
@@ -2861,6 +2946,7 @@ ${contribution}`);
|
|
|
2861
2946
|
)
|
|
2862
2947
|
);
|
|
2863
2948
|
const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
|
|
2949
|
+
const completedTasks = results.filter((r) => r.status === "completed");
|
|
2864
2950
|
const failedTasks = results.filter((r) => r.status !== "completed");
|
|
2865
2951
|
const lines = results.map((result, index) => {
|
|
2866
2952
|
const agentName = agentNames[index] ?? "unknown";
|
|
@@ -2870,12 +2956,15 @@ ${contribution}`);
|
|
|
2870
2956
|
const reports = results.flatMap((r) => r.result?.reports ?? []);
|
|
2871
2957
|
const summary = lines.join("\n");
|
|
2872
2958
|
if (failedTasks.length > 0) {
|
|
2873
|
-
const firstError = failedTasks[0]?.result?.error ?? "unknown
|
|
2959
|
+
const firstError = failedTasks[0]?.result?.error ?? failedTasks[0]?.lastError ?? "unknown outcome";
|
|
2960
|
+
const states = failedTasks.map((r) => r.status).join(", ");
|
|
2874
2961
|
return {
|
|
2875
|
-
|
|
2962
|
+
// Some agents succeeded → the step produced usable but incomplete
|
|
2963
|
+
// intelligence: partial, not failed (roadmap §9.3).
|
|
2964
|
+
status: completedTasks.length > 0 ? "partial" : "failed",
|
|
2876
2965
|
summary,
|
|
2877
2966
|
taskIds: created.map((t) => t.id),
|
|
2878
|
-
error: `${failedTasks.length}/${results.length} task(s)
|
|
2967
|
+
error: `${failedTasks.length}/${results.length} task(s) incomplete (${states}) \u2014 ${firstError}`,
|
|
2879
2968
|
reports
|
|
2880
2969
|
};
|
|
2881
2970
|
}
|
|
@@ -2891,7 +2980,9 @@ ${contribution}`);
|
|
|
2891
2980
|
const result = await this.options.tasks.run(task.id);
|
|
2892
2981
|
if (result.status !== "completed") {
|
|
2893
2982
|
return {
|
|
2894
|
-
|
|
2983
|
+
// Unknown/timed_out outcomes are not failures — the work may have
|
|
2984
|
+
// happened remotely; the step is incomplete, not broken.
|
|
2985
|
+
status: result.status === "unknown" || result.status === "timed_out" ? "partial" : "failed",
|
|
2895
2986
|
summary: result.result?.summary ?? "",
|
|
2896
2987
|
taskIds: [task.id],
|
|
2897
2988
|
error: result.result?.error ?? `task ${task.id} did not complete`,
|