@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/index.js
CHANGED
|
@@ -39,6 +39,11 @@ var ProtocolError = class extends OmniNodeError {
|
|
|
39
39
|
super("PROTOCOL_VIOLATION", message, options);
|
|
40
40
|
}
|
|
41
41
|
};
|
|
42
|
+
var PlannerError = class extends OmniNodeError {
|
|
43
|
+
constructor(code, message, options = {}) {
|
|
44
|
+
super(code, message, options);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
42
47
|
var MemoryError = class extends OmniNodeError {
|
|
43
48
|
constructor(message, options = {}) {
|
|
44
49
|
super("MEMORY_UNAVAILABLE", message, options);
|
|
@@ -154,12 +159,18 @@ var pipelineConfigSchema = z.object({
|
|
|
154
159
|
role: z.string().optional(),
|
|
155
160
|
steps: z.array(pipelineStepConfigSchema).min(1)
|
|
156
161
|
}).strict();
|
|
162
|
+
var plannerConfigSchema = z.object({
|
|
163
|
+
kind: z.enum(["model", "heuristic"]).default("model"),
|
|
164
|
+
model: z.string().optional(),
|
|
165
|
+
instruction: z.string().optional()
|
|
166
|
+
}).strict();
|
|
157
167
|
var projectConfigSchema = z.object({
|
|
158
168
|
name: z.string().min(1),
|
|
159
169
|
providers: z.array(providerConfigSchema).default([]),
|
|
160
170
|
agents: z.array(agentConfigSchema).default([]),
|
|
161
171
|
roles: z.array(roleConfigSchema).default([]),
|
|
162
172
|
pipelines: z.array(pipelineConfigSchema).default([]),
|
|
173
|
+
planner: plannerConfigSchema.optional(),
|
|
163
174
|
memory: z.object({
|
|
164
175
|
provider: z.string().min(1).default("local"),
|
|
165
176
|
base_url: z.string().url().optional(),
|
|
@@ -234,6 +245,13 @@ function toAppConfig(yaml) {
|
|
|
234
245
|
agents: yaml.project.agents.map(toAgentConfig),
|
|
235
246
|
roles: yaml.project.roles.map(toRoleDefinition),
|
|
236
247
|
pipelines: yaml.project.pipelines.map(toPipelineDefinition),
|
|
248
|
+
...yaml.project.planner !== void 0 ? {
|
|
249
|
+
planner: {
|
|
250
|
+
kind: yaml.project.planner.kind,
|
|
251
|
+
model: yaml.project.planner.model,
|
|
252
|
+
instruction: yaml.project.planner.instruction
|
|
253
|
+
}
|
|
254
|
+
} : {},
|
|
237
255
|
...yaml.project.memory !== void 0 ? {
|
|
238
256
|
memory: {
|
|
239
257
|
provider: yaml.project.memory.provider,
|
|
@@ -1142,6 +1160,25 @@ var TaskEngine = class {
|
|
|
1142
1160
|
}
|
|
1143
1161
|
return this.transition(task, "cancelled");
|
|
1144
1162
|
}
|
|
1163
|
+
/** Error recovery (§27 Phase 9): reset a failed/cancelled task so it can run again. */
|
|
1164
|
+
async retry(id) {
|
|
1165
|
+
const task = await this.mustGet(id);
|
|
1166
|
+
if (task.status !== "failed" && task.status !== "cancelled") {
|
|
1167
|
+
throw new TaskError(
|
|
1168
|
+
"TASK_INVALID",
|
|
1169
|
+
`Only failed or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
const { result: _discarded, ...rest } = task;
|
|
1173
|
+
const reset = {
|
|
1174
|
+
...rest,
|
|
1175
|
+
status: "created",
|
|
1176
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1177
|
+
};
|
|
1178
|
+
await this.options.store.save(reset);
|
|
1179
|
+
this.log.info(`Task ${id} reset for retry.`);
|
|
1180
|
+
return reset;
|
|
1181
|
+
}
|
|
1145
1182
|
async get(id) {
|
|
1146
1183
|
return this.options.store.get(id);
|
|
1147
1184
|
}
|
|
@@ -1479,7 +1516,231 @@ function createTaskEngine(config, log = logger, directory) {
|
|
|
1479
1516
|
}
|
|
1480
1517
|
|
|
1481
1518
|
// src/pipelines/engine.ts
|
|
1519
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
1520
|
+
|
|
1521
|
+
// src/planner/heuristic.ts
|
|
1482
1522
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
1523
|
+
var SEVERITY_RANK = {
|
|
1524
|
+
info: 0,
|
|
1525
|
+
low: 1,
|
|
1526
|
+
medium: 2,
|
|
1527
|
+
high: 3,
|
|
1528
|
+
critical: 4
|
|
1529
|
+
};
|
|
1530
|
+
var HeuristicPlanner = class {
|
|
1531
|
+
name = "heuristic";
|
|
1532
|
+
async plan(request) {
|
|
1533
|
+
const steps = [];
|
|
1534
|
+
const findings = request.reports.flatMap(
|
|
1535
|
+
(report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
|
|
1536
|
+
);
|
|
1537
|
+
const recommendations = dedupe(request.reports.flatMap((r) => r.recommendations));
|
|
1538
|
+
for (const [index, text] of recommendations.entries()) {
|
|
1539
|
+
steps.push({
|
|
1540
|
+
id: `step-${steps.length + 1}`,
|
|
1541
|
+
title: truncate(text, 120),
|
|
1542
|
+
order: steps.length + 1,
|
|
1543
|
+
...index > 0 ? { dependsOn: [`step-${steps.length}`] } : {}
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
const significant = findings.filter(({ finding }) => finding.severity ? SEVERITY_RANK[finding.severity] >= SEVERITY_RANK.high : false).sort((a, b) => SEVERITY_RANK[b.finding.severity ?? "info"] - SEVERITY_RANK[a.finding.severity ?? "info"]);
|
|
1547
|
+
for (const { finding } of significant) {
|
|
1548
|
+
steps.push(remediationStep(finding, steps.length + 1));
|
|
1549
|
+
}
|
|
1550
|
+
if (steps.length === 0) {
|
|
1551
|
+
steps.push({
|
|
1552
|
+
id: "step-1",
|
|
1553
|
+
title: "Gather additional information",
|
|
1554
|
+
description: "The research reports contained no actionable recommendations or significant findings. Investigate the objective further before planning implementation work.",
|
|
1555
|
+
order: 1
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
const risks = significant.filter(({ finding }) => finding.severity === "critical").map(({ finding }) => `Critical finding unaddressed until its step completes: ${finding.title}`);
|
|
1559
|
+
return {
|
|
1560
|
+
id: `plan-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
|
|
1561
|
+
objective: request.objective,
|
|
1562
|
+
summary: `Derived from ${request.reports.length} report(s): ${recommendations.length} recommendation step(s), ${significant.length} remediation step(s).`,
|
|
1563
|
+
steps,
|
|
1564
|
+
...risks.length > 0 ? { risks } : {},
|
|
1565
|
+
generatedBy: "heuristic",
|
|
1566
|
+
...request.taskId !== void 0 ? { taskId: request.taskId } : {},
|
|
1567
|
+
...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
|
|
1568
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
function remediationStep(finding, order) {
|
|
1573
|
+
return {
|
|
1574
|
+
id: `step-${order}`,
|
|
1575
|
+
title: `Fix: ${truncate(finding.title, 100)}`,
|
|
1576
|
+
description: finding.detail,
|
|
1577
|
+
order,
|
|
1578
|
+
dependsOn: order > 1 ? [`step-${order - 1}`] : [],
|
|
1579
|
+
sourceFindings: [finding.id]
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
function dedupe(texts) {
|
|
1583
|
+
return [...new Set(texts.map((t) => t.trim()).filter((t) => t.length > 0))];
|
|
1584
|
+
}
|
|
1585
|
+
function truncate(text, max) {
|
|
1586
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// src/planner/model.ts
|
|
1590
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
1591
|
+
import { z as z2 } from "zod";
|
|
1592
|
+
|
|
1593
|
+
// src/planner/context.ts
|
|
1594
|
+
function buildPlannerContext(request) {
|
|
1595
|
+
const parts = [`# Objective
|
|
1596
|
+
${request.objective}`];
|
|
1597
|
+
if (request.role) {
|
|
1598
|
+
const roleLines = [`# Role
|
|
1599
|
+
${request.role.name} (${request.role.id})`];
|
|
1600
|
+
if (request.role.responsibilities.length > 0) {
|
|
1601
|
+
roleLines.push("Responsibilities:", ...request.role.responsibilities.map((r) => `- ${r}`));
|
|
1602
|
+
}
|
|
1603
|
+
if (request.role.expectedOutputs.length > 0) {
|
|
1604
|
+
roleLines.push("Expected outputs:", ...request.role.expectedOutputs.map((o) => `- ${o}`));
|
|
1605
|
+
}
|
|
1606
|
+
parts.push(roleLines.join("\n"));
|
|
1607
|
+
}
|
|
1608
|
+
if (request.reports.length > 0) {
|
|
1609
|
+
const agents = [...new Set(request.reports.map((r) => r.agent))].join(", ");
|
|
1610
|
+
const findingLines = request.reports.flatMap(
|
|
1611
|
+
(report) => report.findings.map(
|
|
1612
|
+
(finding) => `- [${finding.severity ?? "info"}] ${finding.title} (source: ${report.agent}, finding: ${finding.id})` + (finding.detail ? ` \u2014 ${finding.detail}` : "")
|
|
1613
|
+
)
|
|
1614
|
+
);
|
|
1615
|
+
parts.push(
|
|
1616
|
+
`# Findings (${request.reports.length} report(s) by ${agents})
|
|
1617
|
+
` + (findingLines.length > 0 ? findingLines.join("\n") : "(no findings reported)")
|
|
1618
|
+
);
|
|
1619
|
+
const recommendationLines = request.reports.flatMap(
|
|
1620
|
+
(report) => report.recommendations.map((text) => `- ${text} (source: ${report.agent})`)
|
|
1621
|
+
);
|
|
1622
|
+
if (recommendationLines.length > 0) {
|
|
1623
|
+
parts.push(`# Recommendations
|
|
1624
|
+
${recommendationLines.join("\n")}`);
|
|
1625
|
+
}
|
|
1626
|
+
const summaryLines = request.reports.map((report) => `- [${report.agent}] ${report.summary}`);
|
|
1627
|
+
parts.push(`# Report summaries
|
|
1628
|
+
${summaryLines.join("\n")}`);
|
|
1629
|
+
}
|
|
1630
|
+
if (request.memory && request.memory.length > 0) {
|
|
1631
|
+
parts.push(
|
|
1632
|
+
`# Relevant memory
|
|
1633
|
+
` + request.memory.map((entry) => `- [${entry.scope}] ${entry.content}`).join("\n")
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
if (request.context && request.context.length > 0) {
|
|
1637
|
+
parts.push(`# Research context
|
|
1638
|
+
${request.context}`);
|
|
1639
|
+
}
|
|
1640
|
+
return parts.join("\n\n");
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// src/planner/model.ts
|
|
1644
|
+
var planStepJsonSchema = z2.object({
|
|
1645
|
+
id: z2.string().optional(),
|
|
1646
|
+
title: z2.string().min(1),
|
|
1647
|
+
description: z2.string().optional(),
|
|
1648
|
+
order: z2.number().int().optional(),
|
|
1649
|
+
targets: z2.array(z2.string()).optional(),
|
|
1650
|
+
acceptance_criteria: z2.array(z2.string()).optional(),
|
|
1651
|
+
depends_on: z2.array(z2.string()).optional(),
|
|
1652
|
+
source_findings: z2.array(z2.string()).optional()
|
|
1653
|
+
}).strict();
|
|
1654
|
+
var planJsonSchema = z2.object({
|
|
1655
|
+
summary: z2.string().min(1),
|
|
1656
|
+
steps: z2.array(planStepJsonSchema).min(1),
|
|
1657
|
+
risks: z2.array(z2.string()).optional(),
|
|
1658
|
+
notes: z2.array(z2.string()).optional()
|
|
1659
|
+
}).strict();
|
|
1660
|
+
var ModelPlanner = class {
|
|
1661
|
+
constructor(options) {
|
|
1662
|
+
this.options = options;
|
|
1663
|
+
this.log = (options.log ?? logger).child({ component: "planner", model: options.model });
|
|
1664
|
+
}
|
|
1665
|
+
options;
|
|
1666
|
+
name = "model";
|
|
1667
|
+
log;
|
|
1668
|
+
async plan(request) {
|
|
1669
|
+
const context = buildPlannerContext(request);
|
|
1670
|
+
const messages = [
|
|
1671
|
+
{
|
|
1672
|
+
role: "system",
|
|
1673
|
+
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 ?? "")
|
|
1674
|
+
},
|
|
1675
|
+
{ role: "user", content: context }
|
|
1676
|
+
];
|
|
1677
|
+
let responseText;
|
|
1678
|
+
try {
|
|
1679
|
+
responseText = await this.options.chat(this.options.model, messages);
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
return this.degrade(request, `planner model call failed: ${describe(error)}`);
|
|
1682
|
+
}
|
|
1683
|
+
const parsed = parsePlanJson(responseText);
|
|
1684
|
+
if (!parsed) {
|
|
1685
|
+
return this.degrade(request, "planner model output was not valid plan JSON");
|
|
1686
|
+
}
|
|
1687
|
+
const plan = {
|
|
1688
|
+
id: `plan-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
|
|
1689
|
+
objective: request.objective,
|
|
1690
|
+
summary: parsed.summary,
|
|
1691
|
+
steps: parsed.steps,
|
|
1692
|
+
...parsed.risks !== void 0 ? { risks: parsed.risks } : {},
|
|
1693
|
+
...parsed.notes !== void 0 ? { notes: parsed.notes } : {},
|
|
1694
|
+
generatedBy: `model:${this.options.model}`,
|
|
1695
|
+
model: this.options.model,
|
|
1696
|
+
...request.taskId !== void 0 ? { taskId: request.taskId } : {},
|
|
1697
|
+
...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
|
|
1698
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1699
|
+
};
|
|
1700
|
+
return plan;
|
|
1701
|
+
}
|
|
1702
|
+
async degrade(request, reason) {
|
|
1703
|
+
if (this.options.fallback) {
|
|
1704
|
+
this.log.warn(`${reason} \u2014 using the heuristic fallback planner.`);
|
|
1705
|
+
return this.options.fallback.plan(request);
|
|
1706
|
+
}
|
|
1707
|
+
throw new PlannerError("PLANNER_FAILED", `Planner failed: ${reason}.`);
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
function parsePlanJson(text) {
|
|
1711
|
+
const unfenced = text.replace(/```(?:json)?/gi, "");
|
|
1712
|
+
const start = unfenced.indexOf("{");
|
|
1713
|
+
const end = unfenced.lastIndexOf("}");
|
|
1714
|
+
if (start < 0 || end <= start) return void 0;
|
|
1715
|
+
try {
|
|
1716
|
+
const json = JSON.parse(unfenced.slice(start, end + 1));
|
|
1717
|
+
const parsed = planJsonSchema.safeParse(json);
|
|
1718
|
+
if (!parsed.success) return void 0;
|
|
1719
|
+
const steps = parsed.data.steps.map((step, index) => ({
|
|
1720
|
+
id: step.id ?? `step-${index + 1}`,
|
|
1721
|
+
title: step.title,
|
|
1722
|
+
description: step.description,
|
|
1723
|
+
order: step.order ?? index + 1,
|
|
1724
|
+
targets: step.targets,
|
|
1725
|
+
acceptanceCriteria: step.acceptance_criteria,
|
|
1726
|
+
dependsOn: step.depends_on,
|
|
1727
|
+
sourceFindings: step.source_findings
|
|
1728
|
+
}));
|
|
1729
|
+
return {
|
|
1730
|
+
summary: parsed.data.summary,
|
|
1731
|
+
steps: steps.sort((a, b) => a.order - b.order),
|
|
1732
|
+
risks: parsed.data.risks,
|
|
1733
|
+
notes: parsed.data.notes
|
|
1734
|
+
};
|
|
1735
|
+
} catch {
|
|
1736
|
+
return void 0;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
function describe(error) {
|
|
1740
|
+
return error instanceof Error ? error.message : String(error);
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// src/pipelines/engine.ts
|
|
1483
1744
|
var PipelineEngine = class {
|
|
1484
1745
|
constructor(options) {
|
|
1485
1746
|
this.options = options;
|
|
@@ -1592,8 +1853,9 @@ var PipelineEngine = class {
|
|
|
1592
1853
|
}
|
|
1593
1854
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1594
1855
|
const run = {
|
|
1595
|
-
id: `run-${Date.now().toString(36)}-${
|
|
1856
|
+
id: `run-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
|
|
1596
1857
|
pipelineId: def.id,
|
|
1858
|
+
objective,
|
|
1597
1859
|
status: "running",
|
|
1598
1860
|
startedAt: now,
|
|
1599
1861
|
stepRuns: []
|
|
@@ -1620,7 +1882,9 @@ var PipelineEngine = class {
|
|
|
1620
1882
|
const ctx = {
|
|
1621
1883
|
objective,
|
|
1622
1884
|
...role ? { role } : {},
|
|
1623
|
-
combinedContext: contextParts.join("\n\n")
|
|
1885
|
+
combinedContext: contextParts.join("\n\n"),
|
|
1886
|
+
reports: [...allReports],
|
|
1887
|
+
pipelineRunId: run.id
|
|
1624
1888
|
};
|
|
1625
1889
|
return this.executeStep(step, ctx);
|
|
1626
1890
|
})
|
|
@@ -1639,6 +1903,7 @@ ${contribution}`);
|
|
|
1639
1903
|
stepId: step.id,
|
|
1640
1904
|
status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
|
|
1641
1905
|
...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
|
|
1906
|
+
...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
|
|
1642
1907
|
...outcome.error !== void 0 ? { error: outcome.error } : {}
|
|
1643
1908
|
};
|
|
1644
1909
|
run.stepRuns.push(stepRun);
|
|
@@ -1649,9 +1914,20 @@ ${contribution}`);
|
|
|
1649
1914
|
}
|
|
1650
1915
|
const failed = [...outcomes.values()].some((o) => o.status === "failed");
|
|
1651
1916
|
const finalStatus = failed ? "failed" : "completed";
|
|
1917
|
+
const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
|
|
1918
|
+
let resultSummary;
|
|
1919
|
+
for (let i = def.steps.length - 1; i >= 0; i -= 1) {
|
|
1920
|
+
const step = def.steps[i];
|
|
1921
|
+
const outcome = outcomes.get(step.id);
|
|
1922
|
+
if (outcome?.status !== "completed" || outcome.taskIds.length === 0) continue;
|
|
1923
|
+
const stepTasks = (await Promise.all(outcome.taskIds.map((id) => this.options.tasks.get(id)))).filter((task) => task !== void 0);
|
|
1924
|
+
const summaries = stepTasks.map((task) => task.result?.summary).filter((summary) => summary !== void 0);
|
|
1925
|
+
if (summaries.length > 0) resultSummary = summaries.join("\n");
|
|
1926
|
+
break;
|
|
1927
|
+
}
|
|
1652
1928
|
let combinedReportId;
|
|
1653
1929
|
if (this.options.reports) {
|
|
1654
|
-
const taskIds =
|
|
1930
|
+
const taskIds = def.steps.filter((step) => step.kind === "research" || step.kind === "analyze").flatMap((step) => outcomes.get(step.id)?.taskIds ?? []);
|
|
1655
1931
|
const tasks = (await Promise.all(taskIds.map((id) => this.options.tasks.get(id)))).filter((task) => task !== void 0);
|
|
1656
1932
|
const collected = await this.options.reports.collectFromTasks(tasks);
|
|
1657
1933
|
await this.options.reports.saveReports(collected);
|
|
@@ -1670,7 +1946,9 @@ ${contribution}`);
|
|
|
1670
1946
|
status: finalStatus,
|
|
1671
1947
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1672
1948
|
...run.reports !== void 0 && run.reports.length > 0 ? { reports: run.reports } : {},
|
|
1673
|
-
...combinedReportId !== void 0 ? { combinedReportId } : {}
|
|
1949
|
+
...combinedReportId !== void 0 ? { combinedReportId } : {},
|
|
1950
|
+
...planId !== void 0 ? { planId } : {},
|
|
1951
|
+
...resultSummary !== void 0 ? { resultSummary } : {}
|
|
1674
1952
|
};
|
|
1675
1953
|
await this.options.store.save(finished);
|
|
1676
1954
|
this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
|
|
@@ -1780,33 +2058,34 @@ ${contribution}`);
|
|
|
1780
2058
|
};
|
|
1781
2059
|
}
|
|
1782
2060
|
async runPlan(step, ctx) {
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
{
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
try {
|
|
1802
|
-
const planText = await this.getChat()(step.model, messages);
|
|
2061
|
+
const request = {
|
|
2062
|
+
objective: ctx.objective,
|
|
2063
|
+
reports: ctx.reports,
|
|
2064
|
+
...ctx.combinedContext.length > 0 ? { context: ctx.combinedContext } : {},
|
|
2065
|
+
...ctx.role ? { role: ctx.role } : {},
|
|
2066
|
+
...ctx.pipelineRunId !== void 0 ? { pipelineRunId: ctx.pipelineRunId } : {}
|
|
2067
|
+
};
|
|
2068
|
+
let planner;
|
|
2069
|
+
if (step.model) {
|
|
2070
|
+
planner = new ModelPlanner({
|
|
2071
|
+
model: step.model,
|
|
2072
|
+
chat: this.getChat(),
|
|
2073
|
+
fallback: new HeuristicPlanner(),
|
|
2074
|
+
log: this.log
|
|
2075
|
+
});
|
|
2076
|
+
} else if (this.options.planner) {
|
|
2077
|
+
planner = this.options.planner;
|
|
2078
|
+
} else {
|
|
1803
2079
|
return {
|
|
1804
2080
|
status: "completed",
|
|
1805
|
-
summary:
|
|
1806
|
-
contextContribution: planText,
|
|
2081
|
+
summary: "(plan: no model or planner configured \u2014 skipped)",
|
|
1807
2082
|
taskIds: [],
|
|
1808
2083
|
reports: []
|
|
1809
2084
|
};
|
|
2085
|
+
}
|
|
2086
|
+
let plan;
|
|
2087
|
+
try {
|
|
2088
|
+
plan = await planner.plan(request);
|
|
1810
2089
|
} catch (error) {
|
|
1811
2090
|
return {
|
|
1812
2091
|
status: "failed",
|
|
@@ -1816,6 +2095,15 @@ Produce the final plan.`
|
|
|
1816
2095
|
reports: []
|
|
1817
2096
|
};
|
|
1818
2097
|
}
|
|
2098
|
+
await this.options.plans?.save(plan);
|
|
2099
|
+
return {
|
|
2100
|
+
status: "completed",
|
|
2101
|
+
summary: plan.summary,
|
|
2102
|
+
contextContribution: formatPlan(plan),
|
|
2103
|
+
taskIds: [],
|
|
2104
|
+
planId: plan.id,
|
|
2105
|
+
reports: []
|
|
2106
|
+
};
|
|
1819
2107
|
}
|
|
1820
2108
|
getChat() {
|
|
1821
2109
|
if (this.options.chat) return this.options.chat;
|
|
@@ -1825,6 +2113,21 @@ Produce the final plan.`
|
|
|
1825
2113
|
);
|
|
1826
2114
|
}
|
|
1827
2115
|
};
|
|
2116
|
+
function formatPlan(plan) {
|
|
2117
|
+
const lines = [
|
|
2118
|
+
`# Implementation Plan (by ${plan.generatedBy})`,
|
|
2119
|
+
plan.summary,
|
|
2120
|
+
"Steps:",
|
|
2121
|
+
...plan.steps.map((step) => {
|
|
2122
|
+
const targets = step.targets && step.targets.length > 0 ? ` [targets: ${step.targets.join(", ")}]` : "";
|
|
2123
|
+
return `${step.order}. ${step.title}${targets}`;
|
|
2124
|
+
})
|
|
2125
|
+
];
|
|
2126
|
+
if (plan.risks && plan.risks.length > 0) {
|
|
2127
|
+
lines.push("Risks:", ...plan.risks.map((risk) => `- ${risk}`));
|
|
2128
|
+
}
|
|
2129
|
+
return lines.join("\n");
|
|
2130
|
+
}
|
|
1828
2131
|
function createDefaultChatFn(providers) {
|
|
1829
2132
|
return async (modelRef, messages) => {
|
|
1830
2133
|
const separator = modelRef.indexOf(":");
|
|
@@ -1905,9 +2208,86 @@ var FilePipelineRunStore = class {
|
|
|
1905
2208
|
}
|
|
1906
2209
|
};
|
|
1907
2210
|
|
|
2211
|
+
// src/planner/store.ts
|
|
2212
|
+
import { mkdir as mkdir4, readFile as readFile4, rename as rename4, writeFile as writeFile4 } from "fs/promises";
|
|
2213
|
+
import { dirname as dirname4 } from "path";
|
|
2214
|
+
var FilePlanStore = class {
|
|
2215
|
+
filePath;
|
|
2216
|
+
queue = Promise.resolve();
|
|
2217
|
+
constructor(directory = `${process.cwd()}/.omninode`) {
|
|
2218
|
+
this.filePath = `${directory}/plans.json`;
|
|
2219
|
+
}
|
|
2220
|
+
async save(plan) {
|
|
2221
|
+
return this.synchronized(async () => {
|
|
2222
|
+
const plans = await this.load();
|
|
2223
|
+
const index = plans.findIndex((p) => p.id === plan.id);
|
|
2224
|
+
if (index >= 0) plans[index] = plan;
|
|
2225
|
+
else plans.push(plan);
|
|
2226
|
+
await this.write(plans);
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
async get(id) {
|
|
2230
|
+
return this.synchronized(async () => {
|
|
2231
|
+
const plans = await this.load();
|
|
2232
|
+
return plans.find((p) => p.id === id);
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
async list(filter = {}) {
|
|
2236
|
+
return this.synchronized(async () => {
|
|
2237
|
+
const plans = await this.load();
|
|
2238
|
+
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));
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
synchronized(operation) {
|
|
2242
|
+
const result = this.queue.then(operation, operation);
|
|
2243
|
+
this.queue = result.then(
|
|
2244
|
+
() => void 0,
|
|
2245
|
+
() => void 0
|
|
2246
|
+
);
|
|
2247
|
+
return result;
|
|
2248
|
+
}
|
|
2249
|
+
async load() {
|
|
2250
|
+
try {
|
|
2251
|
+
const raw = await readFile4(this.filePath, "utf8");
|
|
2252
|
+
const parsed = JSON.parse(raw);
|
|
2253
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
2254
|
+
} catch {
|
|
2255
|
+
return [];
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
async write(plans) {
|
|
2259
|
+
const tmp = `${this.filePath}.tmp`;
|
|
2260
|
+
await mkdir4(dirname4(this.filePath), { recursive: true });
|
|
2261
|
+
await writeFile4(tmp, `${JSON.stringify(plans, null, 2)}
|
|
2262
|
+
`, "utf8");
|
|
2263
|
+
await rename4(tmp, this.filePath);
|
|
2264
|
+
}
|
|
2265
|
+
};
|
|
2266
|
+
|
|
2267
|
+
// src/planner/factory.ts
|
|
2268
|
+
function buildPlanner(plannerConfig, chat, log = logger) {
|
|
2269
|
+
if (!plannerConfig) return void 0;
|
|
2270
|
+
if (plannerConfig.kind === "heuristic") {
|
|
2271
|
+
return new HeuristicPlanner();
|
|
2272
|
+
}
|
|
2273
|
+
if (!plannerConfig.model) {
|
|
2274
|
+
throw new ConfigError(
|
|
2275
|
+
"CONFIG_INVALID",
|
|
2276
|
+
'Planner kind "model" needs a model reference ("provider:model-id") in omninode.yaml.'
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
return new ModelPlanner({
|
|
2280
|
+
model: plannerConfig.model,
|
|
2281
|
+
chat,
|
|
2282
|
+
...plannerConfig.instruction !== void 0 ? { instruction: plannerConfig.instruction } : {},
|
|
2283
|
+
fallback: new HeuristicPlanner(),
|
|
2284
|
+
log
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
|
|
1908
2288
|
// src/reports/aggregate.ts
|
|
1909
|
-
import { randomBytes as
|
|
1910
|
-
var
|
|
2289
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
2290
|
+
var SEVERITY_RANK2 = {
|
|
1911
2291
|
info: 0,
|
|
1912
2292
|
low: 1,
|
|
1913
2293
|
medium: 2,
|
|
@@ -1930,8 +2310,8 @@ function aggregateReports(reports, options = {}) {
|
|
|
1930
2310
|
const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
|
|
1931
2311
|
const summary = `${reports.length} report(s) from ${sources.length > 0 ? sources.join(", ") : "no agents"}: ${findingGroups.length} unique finding group(s) (${totalRawFindings} raw), ${recommendationGroups.length} recommendation group(s).`;
|
|
1932
2312
|
return {
|
|
1933
|
-
id: `combined-${Date.now().toString(36)}-${
|
|
1934
|
-
title: options.objective !== void 0 ?
|
|
2313
|
+
id: `combined-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
|
|
2314
|
+
title: options.objective !== void 0 ? truncate2(options.objective, 120) : "Combined report",
|
|
1935
2315
|
...options.objective !== void 0 ? { objective: options.objective } : {},
|
|
1936
2316
|
...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
|
|
1937
2317
|
taskIds: options.taskIds ?? [],
|
|
@@ -1944,7 +2324,7 @@ function aggregateReports(reports, options = {}) {
|
|
|
1944
2324
|
};
|
|
1945
2325
|
}
|
|
1946
2326
|
function rank(finding) {
|
|
1947
|
-
return finding.severity ?
|
|
2327
|
+
return finding.severity ? SEVERITY_RANK2[finding.severity] : -1;
|
|
1948
2328
|
}
|
|
1949
2329
|
function mergeFinding(groups, finding, report, threshold) {
|
|
1950
2330
|
const source = { agent: report.agent, reportId: report.id };
|
|
@@ -1954,7 +2334,7 @@ function mergeFinding(groups, finding, report, threshold) {
|
|
|
1954
2334
|
group.details.push(finding.detail);
|
|
1955
2335
|
group.sources.push(source);
|
|
1956
2336
|
group.evidence.push(...finding.evidence ?? []);
|
|
1957
|
-
if (finding.severity && (!group.severity ||
|
|
2337
|
+
if (finding.severity && (!group.severity || SEVERITY_RANK2[finding.severity] > SEVERITY_RANK2[group.severity])) {
|
|
1958
2338
|
group.severity = finding.severity;
|
|
1959
2339
|
}
|
|
1960
2340
|
return;
|
|
@@ -1994,7 +2374,7 @@ function tokens(text) {
|
|
|
1994
2374
|
text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
|
|
1995
2375
|
);
|
|
1996
2376
|
}
|
|
1997
|
-
function
|
|
2377
|
+
function truncate2(text, max) {
|
|
1998
2378
|
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
1999
2379
|
}
|
|
2000
2380
|
|
|
@@ -2038,14 +2418,14 @@ function extractReportFromText(raw, meta) {
|
|
|
2038
2418
|
const nonEmpty = trimmed.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
2039
2419
|
const proseLine = nonEmpty.find((line) => !line.startsWith("#") && !BULLET.test(line)) ?? nonEmpty[0] ?? "";
|
|
2040
2420
|
const cleaned = proseLine.replace(/^#{1,6}\s+/, "").replace(BULLET, "$1");
|
|
2041
|
-
const summary = cleaned.length > 0 ?
|
|
2421
|
+
const summary = cleaned.length > 0 ? truncate3(cleaned, 200) : "(no output)";
|
|
2042
2422
|
const sections = parseSections(trimmed);
|
|
2043
2423
|
const findingTexts = collectBullets(sections, [/finding/, /issue/, /problem/, /vulnerab/]);
|
|
2044
2424
|
const recommendationTexts = collectBullets(sections, [/recommend/, /suggestion/, /next step/, /action/]);
|
|
2045
2425
|
const evidenceTexts = collectBullets(sections, [/evidence/, /proof/, /log/]);
|
|
2046
2426
|
const findings = findingTexts.map((text, index) => ({
|
|
2047
2427
|
id: `${meta.taskId}-f-${index + 1}`,
|
|
2048
|
-
title:
|
|
2428
|
+
title: truncate3(text, 120),
|
|
2049
2429
|
detail: text,
|
|
2050
2430
|
severity: guessSeverity(text)
|
|
2051
2431
|
}));
|
|
@@ -2056,12 +2436,12 @@ function extractReportFromText(raw, meta) {
|
|
|
2056
2436
|
...meta.model !== void 0 ? { model: meta.model } : {},
|
|
2057
2437
|
summary,
|
|
2058
2438
|
findings,
|
|
2059
|
-
recommendations: recommendationTexts.map((t) =>
|
|
2439
|
+
recommendations: recommendationTexts.map((t) => truncate3(t, 200)),
|
|
2060
2440
|
metadata: evidenceTexts.length > 0 ? { evidence: evidenceTexts } : void 0,
|
|
2061
2441
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2062
2442
|
};
|
|
2063
2443
|
}
|
|
2064
|
-
function
|
|
2444
|
+
function truncate3(text, max) {
|
|
2065
2445
|
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
2066
2446
|
}
|
|
2067
2447
|
|
|
@@ -2116,8 +2496,8 @@ var ReportService = class {
|
|
|
2116
2496
|
};
|
|
2117
2497
|
|
|
2118
2498
|
// src/reports/store.ts
|
|
2119
|
-
import { mkdir as
|
|
2120
|
-
import { dirname as
|
|
2499
|
+
import { mkdir as mkdir5, readFile as readFile5, rename as rename5, writeFile as writeFile5 } from "fs/promises";
|
|
2500
|
+
import { dirname as dirname5 } from "path";
|
|
2121
2501
|
var FileReportStore = class {
|
|
2122
2502
|
filePath;
|
|
2123
2503
|
queue = Promise.resolve();
|
|
@@ -2181,7 +2561,7 @@ var FileReportStore = class {
|
|
|
2181
2561
|
}
|
|
2182
2562
|
async load() {
|
|
2183
2563
|
try {
|
|
2184
|
-
const raw = await
|
|
2564
|
+
const raw = await readFile5(this.filePath, "utf8");
|
|
2185
2565
|
const parsed = JSON.parse(raw);
|
|
2186
2566
|
return Array.isArray(parsed) ? parsed : [];
|
|
2187
2567
|
} catch {
|
|
@@ -2190,10 +2570,10 @@ var FileReportStore = class {
|
|
|
2190
2570
|
}
|
|
2191
2571
|
async write(entries) {
|
|
2192
2572
|
const tmp = `${this.filePath}.tmp`;
|
|
2193
|
-
await
|
|
2194
|
-
await
|
|
2573
|
+
await mkdir5(dirname5(this.filePath), { recursive: true });
|
|
2574
|
+
await writeFile5(tmp, `${JSON.stringify(entries, null, 2)}
|
|
2195
2575
|
`, "utf8");
|
|
2196
|
-
await
|
|
2576
|
+
await rename5(tmp, this.filePath);
|
|
2197
2577
|
}
|
|
2198
2578
|
};
|
|
2199
2579
|
|
|
@@ -2201,6 +2581,7 @@ var FileReportStore = class {
|
|
|
2201
2581
|
function buildPipelineEngine(config, log = logger) {
|
|
2202
2582
|
const agents = buildAgentRegistry(config, log);
|
|
2203
2583
|
const roles = buildRoleRegistry(config);
|
|
2584
|
+
const chat = createDefaultChatFn(config.project.providers);
|
|
2204
2585
|
const tasks = new TaskEngine({
|
|
2205
2586
|
agents,
|
|
2206
2587
|
roles,
|
|
@@ -2214,14 +2595,16 @@ function buildPipelineEngine(config, log = logger) {
|
|
|
2214
2595
|
roles,
|
|
2215
2596
|
providers: config.project.providers,
|
|
2216
2597
|
store: new FilePipelineRunStore(),
|
|
2217
|
-
chat
|
|
2598
|
+
chat,
|
|
2599
|
+
...config.project.planner !== void 0 ? { planner: buildPlanner(config.project.planner, chat, log) } : {},
|
|
2600
|
+
plans: new FilePlanStore(),
|
|
2218
2601
|
reports: new ReportService(new FileReportStore(), log),
|
|
2219
2602
|
log
|
|
2220
2603
|
});
|
|
2221
2604
|
}
|
|
2222
2605
|
|
|
2223
2606
|
// src/version.ts
|
|
2224
|
-
var OMNINODE_VERSION = "0.
|
|
2607
|
+
var OMNINODE_VERSION = "0.10.0";
|
|
2225
2608
|
export {
|
|
2226
2609
|
AgentError,
|
|
2227
2610
|
AgentRegistry,
|
|
@@ -2242,6 +2625,7 @@ export {
|
|
|
2242
2625
|
OpenAICompatibleProvider,
|
|
2243
2626
|
PipelineEngine,
|
|
2244
2627
|
PipelineError,
|
|
2628
|
+
PlannerError,
|
|
2245
2629
|
ProcessAgent,
|
|
2246
2630
|
ProtocolError,
|
|
2247
2631
|
ProviderError,
|
|
@@ -2273,6 +2657,7 @@ export {
|
|
|
2273
2657
|
logger,
|
|
2274
2658
|
pipelineConfigSchema,
|
|
2275
2659
|
pipelineStepConfigSchema,
|
|
2660
|
+
plannerConfigSchema,
|
|
2276
2661
|
projectConfigSchema,
|
|
2277
2662
|
providerConfigSchema,
|
|
2278
2663
|
registryKey,
|