@hilbras/omninode 0.8.0 → 0.10.0
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 +48 -4
- package/dist/cli/index.js +567 -46
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +154 -5
- package/dist/index.js +430 -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 = "0.
|
|
9
|
+
var OMNINODE_VERSION = "0.10.0";
|
|
10
10
|
|
|
11
11
|
// src/errors/index.ts
|
|
12
12
|
var OmniNodeError = class extends Error {
|
|
@@ -49,6 +49,11 @@ var ProtocolError = class extends OmniNodeError {
|
|
|
49
49
|
super("PROTOCOL_VIOLATION", message, options);
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
|
+
var PlannerError = class extends OmniNodeError {
|
|
53
|
+
constructor(code, message, options = {}) {
|
|
54
|
+
super(code, message, options);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
52
57
|
var MemoryError = class extends OmniNodeError {
|
|
53
58
|
constructor(message, options = {}) {
|
|
54
59
|
super("MEMORY_UNAVAILABLE", message, options);
|
|
@@ -111,12 +116,18 @@ var pipelineConfigSchema = z.object({
|
|
|
111
116
|
role: z.string().optional(),
|
|
112
117
|
steps: z.array(pipelineStepConfigSchema).min(1)
|
|
113
118
|
}).strict();
|
|
119
|
+
var plannerConfigSchema = z.object({
|
|
120
|
+
kind: z.enum(["model", "heuristic"]).default("model"),
|
|
121
|
+
model: z.string().optional(),
|
|
122
|
+
instruction: z.string().optional()
|
|
123
|
+
}).strict();
|
|
114
124
|
var projectConfigSchema = z.object({
|
|
115
125
|
name: z.string().min(1),
|
|
116
126
|
providers: z.array(providerConfigSchema).default([]),
|
|
117
127
|
agents: z.array(agentConfigSchema).default([]),
|
|
118
128
|
roles: z.array(roleConfigSchema).default([]),
|
|
119
129
|
pipelines: z.array(pipelineConfigSchema).default([]),
|
|
130
|
+
planner: plannerConfigSchema.optional(),
|
|
120
131
|
memory: z.object({
|
|
121
132
|
provider: z.string().min(1).default("local"),
|
|
122
133
|
base_url: z.string().url().optional(),
|
|
@@ -191,6 +202,13 @@ function toAppConfig(yaml) {
|
|
|
191
202
|
agents: yaml.project.agents.map(toAgentConfig),
|
|
192
203
|
roles: yaml.project.roles.map(toRoleDefinition),
|
|
193
204
|
pipelines: yaml.project.pipelines.map(toPipelineDefinition),
|
|
205
|
+
...yaml.project.planner !== void 0 ? {
|
|
206
|
+
planner: {
|
|
207
|
+
kind: yaml.project.planner.kind,
|
|
208
|
+
model: yaml.project.planner.model,
|
|
209
|
+
instruction: yaml.project.planner.instruction
|
|
210
|
+
}
|
|
211
|
+
} : {},
|
|
194
212
|
...yaml.project.memory !== void 0 ? {
|
|
195
213
|
memory: {
|
|
196
214
|
provider: yaml.project.memory.provider,
|
|
@@ -1457,6 +1475,25 @@ var TaskEngine = class {
|
|
|
1457
1475
|
}
|
|
1458
1476
|
return this.transition(task, "cancelled");
|
|
1459
1477
|
}
|
|
1478
|
+
/** Error recovery (§27 Phase 9): reset a failed/cancelled task so it can run again. */
|
|
1479
|
+
async retry(id) {
|
|
1480
|
+
const task = await this.mustGet(id);
|
|
1481
|
+
if (task.status !== "failed" && task.status !== "cancelled") {
|
|
1482
|
+
throw new TaskError(
|
|
1483
|
+
"TASK_INVALID",
|
|
1484
|
+
`Only failed or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
const { result: _discarded, ...rest } = task;
|
|
1488
|
+
const reset = {
|
|
1489
|
+
...rest,
|
|
1490
|
+
status: "created",
|
|
1491
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1492
|
+
};
|
|
1493
|
+
await this.options.store.save(reset);
|
|
1494
|
+
this.log.info(`Task ${id} reset for retry.`);
|
|
1495
|
+
return reset;
|
|
1496
|
+
}
|
|
1460
1497
|
async get(id) {
|
|
1461
1498
|
return this.options.store.get(id);
|
|
1462
1499
|
}
|
|
@@ -1876,6 +1913,17 @@ function registerTaskCommands(program) {
|
|
|
1876
1913
|
const cancelled = await engine.cancel(taskId);
|
|
1877
1914
|
console.log(`Task ${cancelled.id} is now "${cancelled.status}".`);
|
|
1878
1915
|
});
|
|
1916
|
+
task.command("retry <task-id>").description("Reset a failed/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) => {
|
|
1917
|
+
const config = loadConfig();
|
|
1918
|
+
const engine = createTaskEngine(config);
|
|
1919
|
+
const reset = await engine.retry(taskId);
|
|
1920
|
+
console.log(`Task ${reset.id} reset to "${reset.status}".`);
|
|
1921
|
+
if (!options.run) return;
|
|
1922
|
+
console.log(`Running task ${reset.id}...`);
|
|
1923
|
+
const finished = await engine.run(reset.id);
|
|
1924
|
+
printOutcome(finished);
|
|
1925
|
+
if (finished.status !== "completed") process.exitCode = 1;
|
|
1926
|
+
});
|
|
1879
1927
|
}
|
|
1880
1928
|
function printOutcome(task, options = {}) {
|
|
1881
1929
|
console.log(` id: ${task.id}`);
|
|
@@ -2282,7 +2330,231 @@ function registerReportCommands(program) {
|
|
|
2282
2330
|
}
|
|
2283
2331
|
|
|
2284
2332
|
// src/pipelines/engine.ts
|
|
2333
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
2334
|
+
|
|
2335
|
+
// src/planner/heuristic.ts
|
|
2285
2336
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
2337
|
+
var SEVERITY_RANK2 = {
|
|
2338
|
+
info: 0,
|
|
2339
|
+
low: 1,
|
|
2340
|
+
medium: 2,
|
|
2341
|
+
high: 3,
|
|
2342
|
+
critical: 4
|
|
2343
|
+
};
|
|
2344
|
+
var HeuristicPlanner = class {
|
|
2345
|
+
name = "heuristic";
|
|
2346
|
+
async plan(request) {
|
|
2347
|
+
const steps = [];
|
|
2348
|
+
const findings = request.reports.flatMap(
|
|
2349
|
+
(report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
|
|
2350
|
+
);
|
|
2351
|
+
const recommendations = dedupe(request.reports.flatMap((r) => r.recommendations));
|
|
2352
|
+
for (const [index, text] of recommendations.entries()) {
|
|
2353
|
+
steps.push({
|
|
2354
|
+
id: `step-${steps.length + 1}`,
|
|
2355
|
+
title: truncate5(text, 120),
|
|
2356
|
+
order: steps.length + 1,
|
|
2357
|
+
...index > 0 ? { dependsOn: [`step-${steps.length}`] } : {}
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
const significant = findings.filter(({ finding }) => finding.severity ? SEVERITY_RANK2[finding.severity] >= SEVERITY_RANK2.high : false).sort((a, b) => SEVERITY_RANK2[b.finding.severity ?? "info"] - SEVERITY_RANK2[a.finding.severity ?? "info"]);
|
|
2361
|
+
for (const { finding } of significant) {
|
|
2362
|
+
steps.push(remediationStep(finding, steps.length + 1));
|
|
2363
|
+
}
|
|
2364
|
+
if (steps.length === 0) {
|
|
2365
|
+
steps.push({
|
|
2366
|
+
id: "step-1",
|
|
2367
|
+
title: "Gather additional information",
|
|
2368
|
+
description: "The research reports contained no actionable recommendations or significant findings. Investigate the objective further before planning implementation work.",
|
|
2369
|
+
order: 1
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
const risks = significant.filter(({ finding }) => finding.severity === "critical").map(({ finding }) => `Critical finding unaddressed until its step completes: ${finding.title}`);
|
|
2373
|
+
return {
|
|
2374
|
+
id: `plan-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
|
|
2375
|
+
objective: request.objective,
|
|
2376
|
+
summary: `Derived from ${request.reports.length} report(s): ${recommendations.length} recommendation step(s), ${significant.length} remediation step(s).`,
|
|
2377
|
+
steps,
|
|
2378
|
+
...risks.length > 0 ? { risks } : {},
|
|
2379
|
+
generatedBy: "heuristic",
|
|
2380
|
+
...request.taskId !== void 0 ? { taskId: request.taskId } : {},
|
|
2381
|
+
...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
|
|
2382
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
};
|
|
2386
|
+
function remediationStep(finding, order) {
|
|
2387
|
+
return {
|
|
2388
|
+
id: `step-${order}`,
|
|
2389
|
+
title: `Fix: ${truncate5(finding.title, 100)}`,
|
|
2390
|
+
description: finding.detail,
|
|
2391
|
+
order,
|
|
2392
|
+
dependsOn: order > 1 ? [`step-${order - 1}`] : [],
|
|
2393
|
+
sourceFindings: [finding.id]
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
function dedupe(texts) {
|
|
2397
|
+
return [...new Set(texts.map((t) => t.trim()).filter((t) => t.length > 0))];
|
|
2398
|
+
}
|
|
2399
|
+
function truncate5(text, max) {
|
|
2400
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
// src/planner/model.ts
|
|
2404
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
2405
|
+
import { z as z2 } from "zod";
|
|
2406
|
+
|
|
2407
|
+
// src/planner/context.ts
|
|
2408
|
+
function buildPlannerContext(request) {
|
|
2409
|
+
const parts = [`# Objective
|
|
2410
|
+
${request.objective}`];
|
|
2411
|
+
if (request.role) {
|
|
2412
|
+
const roleLines = [`# Role
|
|
2413
|
+
${request.role.name} (${request.role.id})`];
|
|
2414
|
+
if (request.role.responsibilities.length > 0) {
|
|
2415
|
+
roleLines.push("Responsibilities:", ...request.role.responsibilities.map((r) => `- ${r}`));
|
|
2416
|
+
}
|
|
2417
|
+
if (request.role.expectedOutputs.length > 0) {
|
|
2418
|
+
roleLines.push("Expected outputs:", ...request.role.expectedOutputs.map((o) => `- ${o}`));
|
|
2419
|
+
}
|
|
2420
|
+
parts.push(roleLines.join("\n"));
|
|
2421
|
+
}
|
|
2422
|
+
if (request.reports.length > 0) {
|
|
2423
|
+
const agents = [...new Set(request.reports.map((r) => r.agent))].join(", ");
|
|
2424
|
+
const findingLines = request.reports.flatMap(
|
|
2425
|
+
(report) => report.findings.map(
|
|
2426
|
+
(finding) => `- [${finding.severity ?? "info"}] ${finding.title} (source: ${report.agent}, finding: ${finding.id})` + (finding.detail ? ` \u2014 ${finding.detail}` : "")
|
|
2427
|
+
)
|
|
2428
|
+
);
|
|
2429
|
+
parts.push(
|
|
2430
|
+
`# Findings (${request.reports.length} report(s) by ${agents})
|
|
2431
|
+
` + (findingLines.length > 0 ? findingLines.join("\n") : "(no findings reported)")
|
|
2432
|
+
);
|
|
2433
|
+
const recommendationLines = request.reports.flatMap(
|
|
2434
|
+
(report) => report.recommendations.map((text) => `- ${text} (source: ${report.agent})`)
|
|
2435
|
+
);
|
|
2436
|
+
if (recommendationLines.length > 0) {
|
|
2437
|
+
parts.push(`# Recommendations
|
|
2438
|
+
${recommendationLines.join("\n")}`);
|
|
2439
|
+
}
|
|
2440
|
+
const summaryLines = request.reports.map((report) => `- [${report.agent}] ${report.summary}`);
|
|
2441
|
+
parts.push(`# Report summaries
|
|
2442
|
+
${summaryLines.join("\n")}`);
|
|
2443
|
+
}
|
|
2444
|
+
if (request.memory && request.memory.length > 0) {
|
|
2445
|
+
parts.push(
|
|
2446
|
+
`# Relevant memory
|
|
2447
|
+
` + request.memory.map((entry2) => `- [${entry2.scope}] ${entry2.content}`).join("\n")
|
|
2448
|
+
);
|
|
2449
|
+
}
|
|
2450
|
+
if (request.context && request.context.length > 0) {
|
|
2451
|
+
parts.push(`# Research context
|
|
2452
|
+
${request.context}`);
|
|
2453
|
+
}
|
|
2454
|
+
return parts.join("\n\n");
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// src/planner/model.ts
|
|
2458
|
+
var planStepJsonSchema = z2.object({
|
|
2459
|
+
id: z2.string().optional(),
|
|
2460
|
+
title: z2.string().min(1),
|
|
2461
|
+
description: z2.string().optional(),
|
|
2462
|
+
order: z2.number().int().optional(),
|
|
2463
|
+
targets: z2.array(z2.string()).optional(),
|
|
2464
|
+
acceptance_criteria: z2.array(z2.string()).optional(),
|
|
2465
|
+
depends_on: z2.array(z2.string()).optional(),
|
|
2466
|
+
source_findings: z2.array(z2.string()).optional()
|
|
2467
|
+
}).strict();
|
|
2468
|
+
var planJsonSchema = z2.object({
|
|
2469
|
+
summary: z2.string().min(1),
|
|
2470
|
+
steps: z2.array(planStepJsonSchema).min(1),
|
|
2471
|
+
risks: z2.array(z2.string()).optional(),
|
|
2472
|
+
notes: z2.array(z2.string()).optional()
|
|
2473
|
+
}).strict();
|
|
2474
|
+
var ModelPlanner = class {
|
|
2475
|
+
constructor(options) {
|
|
2476
|
+
this.options = options;
|
|
2477
|
+
this.log = (options.log ?? logger).child({ component: "planner", model: options.model });
|
|
2478
|
+
}
|
|
2479
|
+
options;
|
|
2480
|
+
name = "model";
|
|
2481
|
+
log;
|
|
2482
|
+
async plan(request) {
|
|
2483
|
+
const context = buildPlannerContext(request);
|
|
2484
|
+
const messages = [
|
|
2485
|
+
{
|
|
2486
|
+
role: "system",
|
|
2487
|
+
content: 'You are the OmniNode planner. Given the objective and the combined research context, produce a final analysis and a prioritized, actionable implementation plan. Respond with JSON ONLY (no prose, no code fences) matching exactly this shape: {"summary": string, "steps": [{"id": string, "title": string, "description": string, "order": number, "targets": string[], "acceptance_criteria": string[], "source_findings": string[]}], "risks": string[], "notes": string[]}. ' + (this.options.instruction ?? "")
|
|
2488
|
+
},
|
|
2489
|
+
{ role: "user", content: context }
|
|
2490
|
+
];
|
|
2491
|
+
let responseText;
|
|
2492
|
+
try {
|
|
2493
|
+
responseText = await this.options.chat(this.options.model, messages);
|
|
2494
|
+
} catch (error) {
|
|
2495
|
+
return this.degrade(request, `planner model call failed: ${describe(error)}`);
|
|
2496
|
+
}
|
|
2497
|
+
const parsed = parsePlanJson(responseText);
|
|
2498
|
+
if (!parsed) {
|
|
2499
|
+
return this.degrade(request, "planner model output was not valid plan JSON");
|
|
2500
|
+
}
|
|
2501
|
+
const plan = {
|
|
2502
|
+
id: `plan-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
|
|
2503
|
+
objective: request.objective,
|
|
2504
|
+
summary: parsed.summary,
|
|
2505
|
+
steps: parsed.steps,
|
|
2506
|
+
...parsed.risks !== void 0 ? { risks: parsed.risks } : {},
|
|
2507
|
+
...parsed.notes !== void 0 ? { notes: parsed.notes } : {},
|
|
2508
|
+
generatedBy: `model:${this.options.model}`,
|
|
2509
|
+
model: this.options.model,
|
|
2510
|
+
...request.taskId !== void 0 ? { taskId: request.taskId } : {},
|
|
2511
|
+
...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
|
|
2512
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2513
|
+
};
|
|
2514
|
+
return plan;
|
|
2515
|
+
}
|
|
2516
|
+
async degrade(request, reason) {
|
|
2517
|
+
if (this.options.fallback) {
|
|
2518
|
+
this.log.warn(`${reason} \u2014 using the heuristic fallback planner.`);
|
|
2519
|
+
return this.options.fallback.plan(request);
|
|
2520
|
+
}
|
|
2521
|
+
throw new PlannerError("PLANNER_FAILED", `Planner failed: ${reason}.`);
|
|
2522
|
+
}
|
|
2523
|
+
};
|
|
2524
|
+
function parsePlanJson(text) {
|
|
2525
|
+
const unfenced = text.replace(/```(?:json)?/gi, "");
|
|
2526
|
+
const start = unfenced.indexOf("{");
|
|
2527
|
+
const end = unfenced.lastIndexOf("}");
|
|
2528
|
+
if (start < 0 || end <= start) return void 0;
|
|
2529
|
+
try {
|
|
2530
|
+
const json = JSON.parse(unfenced.slice(start, end + 1));
|
|
2531
|
+
const parsed = planJsonSchema.safeParse(json);
|
|
2532
|
+
if (!parsed.success) return void 0;
|
|
2533
|
+
const steps = parsed.data.steps.map((step, index) => ({
|
|
2534
|
+
id: step.id ?? `step-${index + 1}`,
|
|
2535
|
+
title: step.title,
|
|
2536
|
+
description: step.description,
|
|
2537
|
+
order: step.order ?? index + 1,
|
|
2538
|
+
targets: step.targets,
|
|
2539
|
+
acceptanceCriteria: step.acceptance_criteria,
|
|
2540
|
+
dependsOn: step.depends_on,
|
|
2541
|
+
sourceFindings: step.source_findings
|
|
2542
|
+
}));
|
|
2543
|
+
return {
|
|
2544
|
+
summary: parsed.data.summary,
|
|
2545
|
+
steps: steps.sort((a, b) => a.order - b.order),
|
|
2546
|
+
risks: parsed.data.risks,
|
|
2547
|
+
notes: parsed.data.notes
|
|
2548
|
+
};
|
|
2549
|
+
} catch {
|
|
2550
|
+
return void 0;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
function describe(error) {
|
|
2554
|
+
return error instanceof Error ? error.message : String(error);
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
// src/pipelines/engine.ts
|
|
2286
2558
|
var PipelineEngine = class {
|
|
2287
2559
|
constructor(options) {
|
|
2288
2560
|
this.options = options;
|
|
@@ -2395,8 +2667,9 @@ var PipelineEngine = class {
|
|
|
2395
2667
|
}
|
|
2396
2668
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2397
2669
|
const run2 = {
|
|
2398
|
-
id: `run-${Date.now().toString(36)}-${
|
|
2670
|
+
id: `run-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
|
|
2399
2671
|
pipelineId: def.id,
|
|
2672
|
+
objective,
|
|
2400
2673
|
status: "running",
|
|
2401
2674
|
startedAt: now,
|
|
2402
2675
|
stepRuns: []
|
|
@@ -2423,7 +2696,9 @@ var PipelineEngine = class {
|
|
|
2423
2696
|
const ctx = {
|
|
2424
2697
|
objective,
|
|
2425
2698
|
...role ? { role } : {},
|
|
2426
|
-
combinedContext: contextParts.join("\n\n")
|
|
2699
|
+
combinedContext: contextParts.join("\n\n"),
|
|
2700
|
+
reports: [...allReports],
|
|
2701
|
+
pipelineRunId: run2.id
|
|
2427
2702
|
};
|
|
2428
2703
|
return this.executeStep(step, ctx);
|
|
2429
2704
|
})
|
|
@@ -2442,6 +2717,7 @@ ${contribution}`);
|
|
|
2442
2717
|
stepId: step.id,
|
|
2443
2718
|
status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
|
|
2444
2719
|
...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
|
|
2720
|
+
...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
|
|
2445
2721
|
...outcome.error !== void 0 ? { error: outcome.error } : {}
|
|
2446
2722
|
};
|
|
2447
2723
|
run2.stepRuns.push(stepRun);
|
|
@@ -2452,9 +2728,20 @@ ${contribution}`);
|
|
|
2452
2728
|
}
|
|
2453
2729
|
const failed = [...outcomes.values()].some((o) => o.status === "failed");
|
|
2454
2730
|
const finalStatus = failed ? "failed" : "completed";
|
|
2731
|
+
const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
|
|
2732
|
+
let resultSummary;
|
|
2733
|
+
for (let i = def.steps.length - 1; i >= 0; i -= 1) {
|
|
2734
|
+
const step = def.steps[i];
|
|
2735
|
+
const outcome = outcomes.get(step.id);
|
|
2736
|
+
if (outcome?.status !== "completed" || outcome.taskIds.length === 0) continue;
|
|
2737
|
+
const stepTasks = (await Promise.all(outcome.taskIds.map((id) => this.options.tasks.get(id)))).filter((task) => task !== void 0);
|
|
2738
|
+
const summaries = stepTasks.map((task) => task.result?.summary).filter((summary) => summary !== void 0);
|
|
2739
|
+
if (summaries.length > 0) resultSummary = summaries.join("\n");
|
|
2740
|
+
break;
|
|
2741
|
+
}
|
|
2455
2742
|
let combinedReportId;
|
|
2456
2743
|
if (this.options.reports) {
|
|
2457
|
-
const taskIds =
|
|
2744
|
+
const taskIds = def.steps.filter((step) => step.kind === "research" || step.kind === "analyze").flatMap((step) => outcomes.get(step.id)?.taskIds ?? []);
|
|
2458
2745
|
const tasks = (await Promise.all(taskIds.map((id) => this.options.tasks.get(id)))).filter((task) => task !== void 0);
|
|
2459
2746
|
const collected = await this.options.reports.collectFromTasks(tasks);
|
|
2460
2747
|
await this.options.reports.saveReports(collected);
|
|
@@ -2473,7 +2760,9 @@ ${contribution}`);
|
|
|
2473
2760
|
status: finalStatus,
|
|
2474
2761
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2475
2762
|
...run2.reports !== void 0 && run2.reports.length > 0 ? { reports: run2.reports } : {},
|
|
2476
|
-
...combinedReportId !== void 0 ? { combinedReportId } : {}
|
|
2763
|
+
...combinedReportId !== void 0 ? { combinedReportId } : {},
|
|
2764
|
+
...planId !== void 0 ? { planId } : {},
|
|
2765
|
+
...resultSummary !== void 0 ? { resultSummary } : {}
|
|
2477
2766
|
};
|
|
2478
2767
|
await this.options.store.save(finished);
|
|
2479
2768
|
this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
|
|
@@ -2583,33 +2872,34 @@ ${contribution}`);
|
|
|
2583
2872
|
};
|
|
2584
2873
|
}
|
|
2585
2874
|
async runPlan(step, ctx) {
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
{
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
try {
|
|
2605
|
-
const planText = await this.getChat()(step.model, messages);
|
|
2875
|
+
const request = {
|
|
2876
|
+
objective: ctx.objective,
|
|
2877
|
+
reports: ctx.reports,
|
|
2878
|
+
...ctx.combinedContext.length > 0 ? { context: ctx.combinedContext } : {},
|
|
2879
|
+
...ctx.role ? { role: ctx.role } : {},
|
|
2880
|
+
...ctx.pipelineRunId !== void 0 ? { pipelineRunId: ctx.pipelineRunId } : {}
|
|
2881
|
+
};
|
|
2882
|
+
let planner;
|
|
2883
|
+
if (step.model) {
|
|
2884
|
+
planner = new ModelPlanner({
|
|
2885
|
+
model: step.model,
|
|
2886
|
+
chat: this.getChat(),
|
|
2887
|
+
fallback: new HeuristicPlanner(),
|
|
2888
|
+
log: this.log
|
|
2889
|
+
});
|
|
2890
|
+
} else if (this.options.planner) {
|
|
2891
|
+
planner = this.options.planner;
|
|
2892
|
+
} else {
|
|
2606
2893
|
return {
|
|
2607
2894
|
status: "completed",
|
|
2608
|
-
summary:
|
|
2609
|
-
contextContribution: planText,
|
|
2895
|
+
summary: "(plan: no model or planner configured \u2014 skipped)",
|
|
2610
2896
|
taskIds: [],
|
|
2611
2897
|
reports: []
|
|
2612
2898
|
};
|
|
2899
|
+
}
|
|
2900
|
+
let plan;
|
|
2901
|
+
try {
|
|
2902
|
+
plan = await planner.plan(request);
|
|
2613
2903
|
} catch (error) {
|
|
2614
2904
|
return {
|
|
2615
2905
|
status: "failed",
|
|
@@ -2619,6 +2909,15 @@ Produce the final plan.`
|
|
|
2619
2909
|
reports: []
|
|
2620
2910
|
};
|
|
2621
2911
|
}
|
|
2912
|
+
await this.options.plans?.save(plan);
|
|
2913
|
+
return {
|
|
2914
|
+
status: "completed",
|
|
2915
|
+
summary: plan.summary,
|
|
2916
|
+
contextContribution: formatPlan(plan),
|
|
2917
|
+
taskIds: [],
|
|
2918
|
+
planId: plan.id,
|
|
2919
|
+
reports: []
|
|
2920
|
+
};
|
|
2622
2921
|
}
|
|
2623
2922
|
getChat() {
|
|
2624
2923
|
if (this.options.chat) return this.options.chat;
|
|
@@ -2628,6 +2927,21 @@ Produce the final plan.`
|
|
|
2628
2927
|
);
|
|
2629
2928
|
}
|
|
2630
2929
|
};
|
|
2930
|
+
function formatPlan(plan) {
|
|
2931
|
+
const lines = [
|
|
2932
|
+
`# Implementation Plan (by ${plan.generatedBy})`,
|
|
2933
|
+
plan.summary,
|
|
2934
|
+
"Steps:",
|
|
2935
|
+
...plan.steps.map((step) => {
|
|
2936
|
+
const targets = step.targets && step.targets.length > 0 ? ` [targets: ${step.targets.join(", ")}]` : "";
|
|
2937
|
+
return `${step.order}. ${step.title}${targets}`;
|
|
2938
|
+
})
|
|
2939
|
+
];
|
|
2940
|
+
if (plan.risks && plan.risks.length > 0) {
|
|
2941
|
+
lines.push("Risks:", ...plan.risks.map((risk) => `- ${risk}`));
|
|
2942
|
+
}
|
|
2943
|
+
return lines.join("\n");
|
|
2944
|
+
}
|
|
2631
2945
|
function createDefaultChatFn(providers) {
|
|
2632
2946
|
return async (modelRef, messages) => {
|
|
2633
2947
|
const separator = modelRef.indexOf(":");
|
|
@@ -2708,10 +3022,88 @@ var FilePipelineRunStore = class {
|
|
|
2708
3022
|
}
|
|
2709
3023
|
};
|
|
2710
3024
|
|
|
3025
|
+
// src/planner/store.ts
|
|
3026
|
+
import { mkdir as mkdir5, readFile as readFile5, rename as rename5, writeFile as writeFile5 } from "fs/promises";
|
|
3027
|
+
import { dirname as dirname5 } from "path";
|
|
3028
|
+
var FilePlanStore = class {
|
|
3029
|
+
filePath;
|
|
3030
|
+
queue = Promise.resolve();
|
|
3031
|
+
constructor(directory = `${process.cwd()}/.omninode`) {
|
|
3032
|
+
this.filePath = `${directory}/plans.json`;
|
|
3033
|
+
}
|
|
3034
|
+
async save(plan) {
|
|
3035
|
+
return this.synchronized(async () => {
|
|
3036
|
+
const plans = await this.load();
|
|
3037
|
+
const index = plans.findIndex((p) => p.id === plan.id);
|
|
3038
|
+
if (index >= 0) plans[index] = plan;
|
|
3039
|
+
else plans.push(plan);
|
|
3040
|
+
await this.write(plans);
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
async get(id) {
|
|
3044
|
+
return this.synchronized(async () => {
|
|
3045
|
+
const plans = await this.load();
|
|
3046
|
+
return plans.find((p) => p.id === id);
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
async list(filter = {}) {
|
|
3050
|
+
return this.synchronized(async () => {
|
|
3051
|
+
const plans = await this.load();
|
|
3052
|
+
return plans.filter((plan) => filter.pipelineRunId ? plan.pipelineRunId === filter.pipelineRunId : true).filter((plan) => filter.taskId ? plan.taskId === filter.taskId : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
synchronized(operation) {
|
|
3056
|
+
const result = this.queue.then(operation, operation);
|
|
3057
|
+
this.queue = result.then(
|
|
3058
|
+
() => void 0,
|
|
3059
|
+
() => void 0
|
|
3060
|
+
);
|
|
3061
|
+
return result;
|
|
3062
|
+
}
|
|
3063
|
+
async load() {
|
|
3064
|
+
try {
|
|
3065
|
+
const raw = await readFile5(this.filePath, "utf8");
|
|
3066
|
+
const parsed = JSON.parse(raw);
|
|
3067
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3068
|
+
} catch {
|
|
3069
|
+
return [];
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
async write(plans) {
|
|
3073
|
+
const tmp = `${this.filePath}.tmp`;
|
|
3074
|
+
await mkdir5(dirname5(this.filePath), { recursive: true });
|
|
3075
|
+
await writeFile5(tmp, `${JSON.stringify(plans, null, 2)}
|
|
3076
|
+
`, "utf8");
|
|
3077
|
+
await rename5(tmp, this.filePath);
|
|
3078
|
+
}
|
|
3079
|
+
};
|
|
3080
|
+
|
|
3081
|
+
// src/planner/factory.ts
|
|
3082
|
+
function buildPlanner(plannerConfig, chat, log = logger) {
|
|
3083
|
+
if (!plannerConfig) return void 0;
|
|
3084
|
+
if (plannerConfig.kind === "heuristic") {
|
|
3085
|
+
return new HeuristicPlanner();
|
|
3086
|
+
}
|
|
3087
|
+
if (!plannerConfig.model) {
|
|
3088
|
+
throw new ConfigError(
|
|
3089
|
+
"CONFIG_INVALID",
|
|
3090
|
+
'Planner kind "model" needs a model reference ("provider:model-id") in omninode.yaml.'
|
|
3091
|
+
);
|
|
3092
|
+
}
|
|
3093
|
+
return new ModelPlanner({
|
|
3094
|
+
model: plannerConfig.model,
|
|
3095
|
+
chat,
|
|
3096
|
+
...plannerConfig.instruction !== void 0 ? { instruction: plannerConfig.instruction } : {},
|
|
3097
|
+
fallback: new HeuristicPlanner(),
|
|
3098
|
+
log
|
|
3099
|
+
});
|
|
3100
|
+
}
|
|
3101
|
+
|
|
2711
3102
|
// src/pipelines/engine-factory.ts
|
|
2712
3103
|
function buildPipelineEngine(config, log = logger) {
|
|
2713
3104
|
const agents = buildAgentRegistry(config, log);
|
|
2714
3105
|
const roles = buildRoleRegistry(config);
|
|
3106
|
+
const chat = createDefaultChatFn(config.project.providers);
|
|
2715
3107
|
const tasks = new TaskEngine({
|
|
2716
3108
|
agents,
|
|
2717
3109
|
roles,
|
|
@@ -2725,7 +3117,9 @@ function buildPipelineEngine(config, log = logger) {
|
|
|
2725
3117
|
roles,
|
|
2726
3118
|
providers: config.project.providers,
|
|
2727
3119
|
store: new FilePipelineRunStore(),
|
|
2728
|
-
chat
|
|
3120
|
+
chat,
|
|
3121
|
+
...config.project.planner !== void 0 ? { planner: buildPlanner(config.project.planner, chat, log) } : {},
|
|
3122
|
+
plans: new FilePlanStore(),
|
|
2729
3123
|
reports: new ReportService(new FileReportStore(), log),
|
|
2730
3124
|
log
|
|
2731
3125
|
});
|
|
@@ -2748,28 +3142,27 @@ function registerPipelineCommands(program) {
|
|
|
2748
3142
|
});
|
|
2749
3143
|
pipeline.command("run <id> [objective]").description(
|
|
2750
3144
|
"Run a pipeline. The objective argument overrides the definition's default objective."
|
|
2751
|
-
).action(async (id, objective) =>
|
|
3145
|
+
).action(async (id, objective) => runPipelineAction(id, objective));
|
|
3146
|
+
pipeline.command("retry <run-id> [objective]").description(
|
|
3147
|
+
"Error recovery: re-run a recorded pipeline run as a new run, reusing its objective."
|
|
3148
|
+
).action(async (runId, objective) => {
|
|
2752
3149
|
const config = loadConfig();
|
|
2753
|
-
const def = findPipeline(config, id);
|
|
2754
3150
|
const engine = buildPipelineEngine(config);
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
const suffix = stepRun.error ? ` \u2014 ${stepRun.error}` : "";
|
|
2762
|
-
console.log(` [${stepRun.stepId}] ${stepRun.status}${suffix}`);
|
|
2763
|
-
}
|
|
2764
|
-
});
|
|
2765
|
-
if (finished.reports && finished.reports.length > 0) {
|
|
2766
|
-
console.log(` reports: ${finished.reports.length} structured report(s)`);
|
|
3151
|
+
const previous = await engine.get(runId);
|
|
3152
|
+
if (!previous) {
|
|
3153
|
+
throw new PipelineError(
|
|
3154
|
+
"PIPELINE_NOT_FOUND",
|
|
3155
|
+
`Pipeline run "${runId}" does not exist in this project's run store.`
|
|
3156
|
+
);
|
|
2767
3157
|
}
|
|
2768
|
-
|
|
2769
|
-
|
|
3158
|
+
const def = config.project.pipelines.find((p) => p.id === previous.pipelineId);
|
|
3159
|
+
if (!def) {
|
|
3160
|
+
throw new PipelineError(
|
|
3161
|
+
"PIPELINE_NOT_FOUND",
|
|
3162
|
+
`Pipeline "${previous.pipelineId}" (run ${runId}) is no longer configured in omninode.yaml.`
|
|
3163
|
+
);
|
|
2770
3164
|
}
|
|
2771
|
-
|
|
2772
|
-
if (finished.status !== "completed") process.exitCode = 1;
|
|
3165
|
+
await runPipelineAction(def.id, objective ?? previous.objective);
|
|
2773
3166
|
});
|
|
2774
3167
|
pipeline.command("runs <id>").description("List recorded runs of a pipeline.").action(async (id) => {
|
|
2775
3168
|
const config = loadConfig();
|
|
@@ -2798,6 +3191,7 @@ function registerPipelineCommands(program) {
|
|
|
2798
3191
|
console.log(` status: ${run2.status}`);
|
|
2799
3192
|
console.log(` started: ${run2.startedAt ?? "?"}`);
|
|
2800
3193
|
if (run2.finishedAt) console.log(` finished: ${run2.finishedAt}`);
|
|
3194
|
+
if (run2.resultSummary) console.log(` result: ${run2.resultSummary}`);
|
|
2801
3195
|
for (const stepRun of run2.stepRuns) {
|
|
2802
3196
|
console.log(` [${stepRun.stepId}] ${stepRun.status}`);
|
|
2803
3197
|
}
|
|
@@ -2806,6 +3200,38 @@ function registerPipelineCommands(program) {
|
|
|
2806
3200
|
}
|
|
2807
3201
|
});
|
|
2808
3202
|
}
|
|
3203
|
+
async function runPipelineAction(id, objective) {
|
|
3204
|
+
const config = loadConfig();
|
|
3205
|
+
const def = findPipeline(config, id);
|
|
3206
|
+
const engine = buildPipelineEngine(config);
|
|
3207
|
+
console.log(
|
|
3208
|
+
`Running pipeline "${def.id}" (${def.steps.length} step(s))${objective ? ` \u2014 "${objective}"` : ""}...`
|
|
3209
|
+
);
|
|
3210
|
+
const finished = await engine.run(def, {
|
|
3211
|
+
...objective !== void 0 ? { objective } : {},
|
|
3212
|
+
onStep: (stepRun) => {
|
|
3213
|
+
const suffix = stepRun.error ? ` \u2014 ${stepRun.error}` : "";
|
|
3214
|
+
console.log(` [${stepRun.stepId}] ${stepRun.status}${suffix}`);
|
|
3215
|
+
}
|
|
3216
|
+
});
|
|
3217
|
+
if (finished.reports && finished.reports.length > 0) {
|
|
3218
|
+
console.log(` reports: ${finished.reports.length} structured report(s)`);
|
|
3219
|
+
}
|
|
3220
|
+
if (finished.combinedReportId) {
|
|
3221
|
+
console.log(` combined report: ${finished.combinedReportId}`);
|
|
3222
|
+
}
|
|
3223
|
+
if (finished.planId) {
|
|
3224
|
+
console.log(` plan: ${finished.planId}`);
|
|
3225
|
+
}
|
|
3226
|
+
if (finished.resultSummary) {
|
|
3227
|
+
console.log(` result: ${finished.resultSummary}`);
|
|
3228
|
+
}
|
|
3229
|
+
console.log(`Pipeline ${finished.status}. Run id: ${finished.id}`);
|
|
3230
|
+
if (finished.status !== "completed") process.exitCode = 1;
|
|
3231
|
+
}
|
|
3232
|
+
function registerRunAlias(program) {
|
|
3233
|
+
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));
|
|
3234
|
+
}
|
|
2809
3235
|
function findPipeline(config, id) {
|
|
2810
3236
|
const def = config.project.pipelines.find((p) => p.id === id);
|
|
2811
3237
|
if (!def) {
|
|
@@ -2871,6 +3297,98 @@ function registerMemoryCommands(program) {
|
|
|
2871
3297
|
});
|
|
2872
3298
|
}
|
|
2873
3299
|
|
|
3300
|
+
// src/cli/commands/plan.ts
|
|
3301
|
+
function registerPlanCommands(program) {
|
|
3302
|
+
const plan = program.command("plan").description("Inspect implementation plans produced by the planner layer (\xA718).");
|
|
3303
|
+
plan.command("list").description("List stored plans.").option("--run <pipelineRunId>", "Only plans for this pipeline run.").action(async (options) => {
|
|
3304
|
+
const store = new FilePlanStore();
|
|
3305
|
+
const plans = await store.list({
|
|
3306
|
+
...options.run !== void 0 ? { pipelineRunId: options.run } : {}
|
|
3307
|
+
});
|
|
3308
|
+
if (plans.length === 0) {
|
|
3309
|
+
console.log("No plans stored yet. Run a pipeline with a plan step to generate one.");
|
|
3310
|
+
return;
|
|
3311
|
+
}
|
|
3312
|
+
for (const p of plans) {
|
|
3313
|
+
console.log(`${p.id} ${p.steps.length} step(s) by ${p.generatedBy} ${truncate(p.summary, 50)}`);
|
|
3314
|
+
}
|
|
3315
|
+
});
|
|
3316
|
+
plan.command("show <id>").description("Show a stored implementation plan in full.").action(async (id) => {
|
|
3317
|
+
const store = new FilePlanStore();
|
|
3318
|
+
const found = await store.get(id);
|
|
3319
|
+
if (!found) {
|
|
3320
|
+
throw new OmniNodeError("PLAN_INVALID", `Plan "${id}" was not found in the plan store.`, {
|
|
3321
|
+
details: { id }
|
|
3322
|
+
});
|
|
3323
|
+
}
|
|
3324
|
+
console.log(`Plan ${found.id}`);
|
|
3325
|
+
console.log(` objective: ${found.objective}`);
|
|
3326
|
+
console.log(` by: ${found.generatedBy}`);
|
|
3327
|
+
console.log(` summary: ${found.summary}`);
|
|
3328
|
+
console.log(" steps:");
|
|
3329
|
+
for (const step of [...found.steps].sort((a, b) => a.order - b.order)) {
|
|
3330
|
+
const targets = step.targets && step.targets.length > 0 ? ` [${step.targets.join(", ")}]` : "";
|
|
3331
|
+
console.log(` ${step.order}. ${step.title}${targets}`);
|
|
3332
|
+
if (step.description) console.log(` ${truncate(step.description, 100)}`);
|
|
3333
|
+
for (const criterion of step.acceptanceCriteria ?? []) {
|
|
3334
|
+
console.log(` accept: ${criterion}`);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
if (found.risks && found.risks.length > 0) {
|
|
3338
|
+
console.log(" risks:");
|
|
3339
|
+
for (const risk of found.risks) {
|
|
3340
|
+
console.log(` - ${risk}`);
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
});
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
// src/cli/commands/status.ts
|
|
3347
|
+
function countBy(items, key) {
|
|
3348
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3349
|
+
for (const item of items) {
|
|
3350
|
+
const k = key(item);
|
|
3351
|
+
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
3352
|
+
}
|
|
3353
|
+
if (counts.size === 0) return "0";
|
|
3354
|
+
return `${items.length} (${[...counts.entries()].map(([k, v]) => `${k} ${v}`).join(", ")})`;
|
|
3355
|
+
}
|
|
3356
|
+
function registerStatusCommand(program) {
|
|
3357
|
+
program.command("status").description("Show the project's OmniNode state overview (config, tasks, runs, reports, plans, memory).").action(async () => {
|
|
3358
|
+
const config = loadConfig();
|
|
3359
|
+
console.log(`OmniNode v${OMNINODE_VERSION} \u2014 project "${config.project.name}"`);
|
|
3360
|
+
console.log(
|
|
3361
|
+
`providers: ${config.project.providers.length > 0 ? config.project.providers.map((p) => p.name).join(", ") : "none"}`
|
|
3362
|
+
);
|
|
3363
|
+
console.log(
|
|
3364
|
+
`agents: ${config.project.agents.length > 0 ? config.project.agents.map((a) => a.name).join(", ") : "none"}`
|
|
3365
|
+
);
|
|
3366
|
+
console.log(
|
|
3367
|
+
`roles: ${config.project.roles.length > 0 ? config.project.roles.map((r) => r.id).join(", ") : "none"}`
|
|
3368
|
+
);
|
|
3369
|
+
console.log(
|
|
3370
|
+
`pipelines: ${config.project.pipelines.length > 0 ? config.project.pipelines.map((p) => p.id).join(", ") : "none"}`
|
|
3371
|
+
);
|
|
3372
|
+
const tasks = await new FileTaskStore().list();
|
|
3373
|
+
console.log(`tasks: ${countBy(tasks, (t) => t.status)}`);
|
|
3374
|
+
const runs = await new FilePipelineRunStore().list();
|
|
3375
|
+
console.log(`runs: ${countBy(runs, (r) => r.status)}`);
|
|
3376
|
+
const reportStore = new FileReportStore();
|
|
3377
|
+
const reports = await reportStore.listReports();
|
|
3378
|
+
const combined = await reportStore.listCombined();
|
|
3379
|
+
console.log(`reports: ${reports.length} raw, ${combined.length} combined`);
|
|
3380
|
+
const plans = await new FilePlanStore().list();
|
|
3381
|
+
console.log(`plans: ${plans.length}`);
|
|
3382
|
+
if (config.project.memory !== void 0) {
|
|
3383
|
+
const memory = new MemoryService(createMemoryProvider(config.project.memory));
|
|
3384
|
+
const entries = await memory.providerRef.query({ limit: 1e4 });
|
|
3385
|
+
console.log(`memory: ${memory.providerName}, ${entries.length} entr(ies)`);
|
|
3386
|
+
} else {
|
|
3387
|
+
console.log("memory: not configured (local provider available \u2014 add a `memory:` section)");
|
|
3388
|
+
}
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
|
|
2874
3392
|
// src/cli/index.ts
|
|
2875
3393
|
function createProgram() {
|
|
2876
3394
|
const program = new Command();
|
|
@@ -2887,6 +3405,9 @@ function createProgram() {
|
|
|
2887
3405
|
registerModelsCommand(program);
|
|
2888
3406
|
registerPipelineCommands(program);
|
|
2889
3407
|
registerMemoryCommands(program);
|
|
3408
|
+
registerPlanCommands(program);
|
|
3409
|
+
registerStatusCommand(program);
|
|
3410
|
+
registerRunAlias(program);
|
|
2890
3411
|
return program;
|
|
2891
3412
|
}
|
|
2892
3413
|
async function run(argv) {
|