@hilbras/omninode 0.5.2 → 0.7.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 +990 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +546 -61
- package/dist/index.js +815 -10
- 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.7.0";
|
|
10
10
|
|
|
11
11
|
// src/errors/index.ts
|
|
12
12
|
var OmniNodeError = class extends Error {
|
|
@@ -39,6 +39,11 @@ var TaskError = class extends OmniNodeError {
|
|
|
39
39
|
super(code, message, options);
|
|
40
40
|
}
|
|
41
41
|
};
|
|
42
|
+
var PipelineError = class extends OmniNodeError {
|
|
43
|
+
constructor(code, message, options = {}) {
|
|
44
|
+
super(code, message, options);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
42
47
|
var ProtocolError = class extends OmniNodeError {
|
|
43
48
|
constructor(message, options = {}) {
|
|
44
49
|
super("PROTOCOL_VIOLATION", message, options);
|
|
@@ -84,11 +89,29 @@ var roleConfigSchema = z.object({
|
|
|
84
89
|
system_prompt: z.string().optional(),
|
|
85
90
|
metadata: z.record(z.unknown()).optional()
|
|
86
91
|
}).strict();
|
|
92
|
+
var pipelineStepConfigSchema = z.object({
|
|
93
|
+
id: z.string().min(1),
|
|
94
|
+
kind: z.enum(["research", "collect", "analyze", "plan", "execute", "custom"]),
|
|
95
|
+
agents: z.array(z.string()).optional(),
|
|
96
|
+
agent: z.string().optional(),
|
|
97
|
+
model: z.string().optional(),
|
|
98
|
+
depends_on: z.array(z.string()).optional(),
|
|
99
|
+
retries: z.number().int().min(0).max(10).optional(),
|
|
100
|
+
condition: z.enum(["always", "on-success", "on-failure"]).optional()
|
|
101
|
+
}).strict();
|
|
102
|
+
var pipelineConfigSchema = z.object({
|
|
103
|
+
id: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, "pipeline id must be kebab-case"),
|
|
104
|
+
name: z.string().optional(),
|
|
105
|
+
objective: z.string().optional(),
|
|
106
|
+
role: z.string().optional(),
|
|
107
|
+
steps: z.array(pipelineStepConfigSchema).min(1)
|
|
108
|
+
}).strict();
|
|
87
109
|
var projectConfigSchema = z.object({
|
|
88
110
|
name: z.string().min(1),
|
|
89
111
|
providers: z.array(providerConfigSchema).default([]),
|
|
90
112
|
agents: z.array(agentConfigSchema).default([]),
|
|
91
113
|
roles: z.array(roleConfigSchema).default([]),
|
|
114
|
+
pipelines: z.array(pipelineConfigSchema).default([]),
|
|
92
115
|
memory: z.object({ provider: z.string().min(1) }).strict().optional()
|
|
93
116
|
}).strict();
|
|
94
117
|
var appConfigSchema = z.object({
|
|
@@ -158,11 +181,30 @@ function toAppConfig(yaml) {
|
|
|
158
181
|
providers: yaml.project.providers.map(toProviderConfig),
|
|
159
182
|
agents: yaml.project.agents.map(toAgentConfig),
|
|
160
183
|
roles: yaml.project.roles.map(toRoleDefinition),
|
|
184
|
+
pipelines: yaml.project.pipelines.map(toPipelineDefinition),
|
|
161
185
|
...yaml.project.memory !== void 0 ? { memory: yaml.project.memory } : {}
|
|
162
186
|
},
|
|
163
187
|
...yaml.logging !== void 0 ? { logging: yaml.logging } : {}
|
|
164
188
|
};
|
|
165
189
|
}
|
|
190
|
+
function toPipelineDefinition(raw) {
|
|
191
|
+
return {
|
|
192
|
+
id: raw.id,
|
|
193
|
+
name: raw.name,
|
|
194
|
+
objective: raw.objective,
|
|
195
|
+
role: raw.role,
|
|
196
|
+
steps: raw.steps.map((step) => ({
|
|
197
|
+
id: step.id,
|
|
198
|
+
kind: step.kind,
|
|
199
|
+
agents: step.agents,
|
|
200
|
+
agent: step.agent,
|
|
201
|
+
model: step.model,
|
|
202
|
+
dependsOn: step.depends_on,
|
|
203
|
+
retries: step.retries,
|
|
204
|
+
condition: step.condition
|
|
205
|
+
}))
|
|
206
|
+
};
|
|
207
|
+
}
|
|
166
208
|
function toProviderConfig(raw) {
|
|
167
209
|
return {
|
|
168
210
|
name: raw.name,
|
|
@@ -633,6 +675,9 @@ function notImplemented(command, when) {
|
|
|
633
675
|
`${command} is planned for ${when}. Edit omninode.yaml directly for now.`
|
|
634
676
|
);
|
|
635
677
|
}
|
|
678
|
+
function truncate(text, max) {
|
|
679
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
680
|
+
}
|
|
636
681
|
function appendToConfigList(listKey, item) {
|
|
637
682
|
const configPath = findConfigFile();
|
|
638
683
|
if (!configPath) {
|
|
@@ -1216,23 +1261,42 @@ import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
|
1216
1261
|
import { dirname } from "path";
|
|
1217
1262
|
var FileTaskStore = class {
|
|
1218
1263
|
filePath;
|
|
1264
|
+
/**
|
|
1265
|
+
* Read-modify-write operations are serialized: parallel pipeline tasks
|
|
1266
|
+
* would otherwise race on the JSON file (last writer wins).
|
|
1267
|
+
*/
|
|
1268
|
+
queue = Promise.resolve();
|
|
1219
1269
|
constructor(directory = `${process.cwd()}/.omninode`) {
|
|
1220
1270
|
this.filePath = `${directory}/tasks.json`;
|
|
1221
1271
|
}
|
|
1222
1272
|
async save(task) {
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1273
|
+
return this.synchronized(async () => {
|
|
1274
|
+
const tasks = await this.load();
|
|
1275
|
+
const index = tasks.findIndex((t) => t.id === task.id);
|
|
1276
|
+
if (index >= 0) tasks[index] = task;
|
|
1277
|
+
else tasks.push(task);
|
|
1278
|
+
await this.write(tasks);
|
|
1279
|
+
});
|
|
1228
1280
|
}
|
|
1229
1281
|
async get(id) {
|
|
1230
|
-
|
|
1231
|
-
|
|
1282
|
+
return this.synchronized(async () => {
|
|
1283
|
+
const tasks = await this.load();
|
|
1284
|
+
return tasks.find((t) => t.id === id);
|
|
1285
|
+
});
|
|
1232
1286
|
}
|
|
1233
1287
|
async list(filter = {}) {
|
|
1234
|
-
|
|
1235
|
-
|
|
1288
|
+
return this.synchronized(async () => {
|
|
1289
|
+
const tasks = await this.load();
|
|
1290
|
+
return tasks.filter((task) => filter.status ? task.status === filter.status : true).filter((task) => filter.project ? task.project === filter.project : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
synchronized(operation) {
|
|
1294
|
+
const result = this.queue.then(operation, operation);
|
|
1295
|
+
this.queue = result.then(
|
|
1296
|
+
() => void 0,
|
|
1297
|
+
() => void 0
|
|
1298
|
+
);
|
|
1299
|
+
return result;
|
|
1236
1300
|
}
|
|
1237
1301
|
async load() {
|
|
1238
1302
|
try {
|
|
@@ -1343,6 +1407,8 @@ var TaskEngine = class {
|
|
|
1343
1407
|
const result = {
|
|
1344
1408
|
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
1345
1409
|
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
1410
|
+
// Keep the verbatim output (bounded) for report extraction and audit.
|
|
1411
|
+
...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
|
|
1346
1412
|
...output.error !== void 0 ? { error: output.error } : {},
|
|
1347
1413
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1348
1414
|
};
|
|
@@ -1477,7 +1543,7 @@ function registerTaskCommands(program) {
|
|
|
1477
1543
|
return;
|
|
1478
1544
|
}
|
|
1479
1545
|
for (const t of tasks) {
|
|
1480
|
-
console.log(`${t.id} ${t.status.padEnd(10)} ${
|
|
1546
|
+
console.log(`${t.id} ${t.status.padEnd(10)} ${truncate2(t.objective, 60)}`);
|
|
1481
1547
|
}
|
|
1482
1548
|
});
|
|
1483
1549
|
task.command("cancel <task-id>").description("Cancel a task that has not finished yet.").action(async (taskId) => {
|
|
@@ -1507,16 +1573,924 @@ function printOutcome(task, options = {}) {
|
|
|
1507
1573
|
if (result.error) console.log(` error: ${result.error}`);
|
|
1508
1574
|
if (result.finishedAt) console.log(` ended: ${result.finishedAt}`);
|
|
1509
1575
|
}
|
|
1510
|
-
function
|
|
1576
|
+
function truncate2(text, max) {
|
|
1511
1577
|
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
1512
1578
|
}
|
|
1513
1579
|
|
|
1580
|
+
// src/reports/aggregate.ts
|
|
1581
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1582
|
+
var SEVERITY_RANK = {
|
|
1583
|
+
info: 0,
|
|
1584
|
+
low: 1,
|
|
1585
|
+
medium: 2,
|
|
1586
|
+
high: 3,
|
|
1587
|
+
critical: 4
|
|
1588
|
+
};
|
|
1589
|
+
function aggregateReports(reports, options = {}) {
|
|
1590
|
+
const threshold = options.similarityThreshold ?? 0.7;
|
|
1591
|
+
const findingGroups = [];
|
|
1592
|
+
const recommendationGroups = [];
|
|
1593
|
+
for (const report of reports) {
|
|
1594
|
+
for (const finding of report.findings) {
|
|
1595
|
+
mergeFinding(findingGroups, finding, report, threshold);
|
|
1596
|
+
}
|
|
1597
|
+
for (const text of report.recommendations) {
|
|
1598
|
+
mergeRecommendation(recommendationGroups, text, report, threshold);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
const sources = [...new Set(reports.map((r) => r.agent))].sort();
|
|
1602
|
+
const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
|
|
1603
|
+
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).`;
|
|
1604
|
+
return {
|
|
1605
|
+
id: `combined-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
|
|
1606
|
+
title: options.objective !== void 0 ? truncate3(options.objective, 120) : "Combined report",
|
|
1607
|
+
...options.objective !== void 0 ? { objective: options.objective } : {},
|
|
1608
|
+
...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
|
|
1609
|
+
taskIds: options.taskIds ?? [],
|
|
1610
|
+
summary,
|
|
1611
|
+
findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
|
|
1612
|
+
recommendations: recommendationGroups,
|
|
1613
|
+
sources,
|
|
1614
|
+
reportIds: reports.map((r) => r.id),
|
|
1615
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
function rank(finding) {
|
|
1619
|
+
return finding.severity ? SEVERITY_RANK[finding.severity] : -1;
|
|
1620
|
+
}
|
|
1621
|
+
function mergeFinding(groups, finding, report, threshold) {
|
|
1622
|
+
const source = { agent: report.agent, reportId: report.id };
|
|
1623
|
+
const group = groups.find((g) => similar(g.title, finding.title, threshold));
|
|
1624
|
+
if (group) {
|
|
1625
|
+
group.occurrences += 1;
|
|
1626
|
+
group.details.push(finding.detail);
|
|
1627
|
+
group.sources.push(source);
|
|
1628
|
+
group.evidence.push(...finding.evidence ?? []);
|
|
1629
|
+
if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
|
|
1630
|
+
group.severity = finding.severity;
|
|
1631
|
+
}
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
groups.push({
|
|
1635
|
+
title: finding.title,
|
|
1636
|
+
details: [finding.detail],
|
|
1637
|
+
sources: [source],
|
|
1638
|
+
occurrences: 1,
|
|
1639
|
+
...finding.severity ? { severity: finding.severity } : {},
|
|
1640
|
+
evidence: [...finding.evidence ?? []]
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
function mergeRecommendation(groups, text, report, threshold) {
|
|
1644
|
+
const source = { agent: report.agent, reportId: report.id };
|
|
1645
|
+
const group = groups.find((g) => similar(g.text, text, threshold));
|
|
1646
|
+
if (group) {
|
|
1647
|
+
group.occurrences += 1;
|
|
1648
|
+
group.sources.push(source);
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
groups.push({ text, sources: [source], occurrences: 1 });
|
|
1652
|
+
}
|
|
1653
|
+
function similar(a, b, threshold) {
|
|
1654
|
+
const left = tokens(a);
|
|
1655
|
+
const right = tokens(b);
|
|
1656
|
+
if (left.size === 0 || right.size === 0) return false;
|
|
1657
|
+
let intersection = 0;
|
|
1658
|
+
for (const token of left) {
|
|
1659
|
+
if (right.has(token)) intersection += 1;
|
|
1660
|
+
}
|
|
1661
|
+
const union = left.size + right.size - intersection;
|
|
1662
|
+
return intersection / union >= threshold;
|
|
1663
|
+
}
|
|
1664
|
+
function tokens(text) {
|
|
1665
|
+
return new Set(
|
|
1666
|
+
text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
function truncate3(text, max) {
|
|
1670
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// src/reports/extract.ts
|
|
1674
|
+
var BULLET = /^(?:[-*•]|\d+[.)])\s+(.*)$/;
|
|
1675
|
+
function parseSections(raw) {
|
|
1676
|
+
const sections = [];
|
|
1677
|
+
let current = { heading: "", bullets: [] };
|
|
1678
|
+
for (const line of raw.split("\n")) {
|
|
1679
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line.trim());
|
|
1680
|
+
if (heading) {
|
|
1681
|
+
if (current.heading.length > 0 || current.bullets.length > 0) {
|
|
1682
|
+
sections.push(current);
|
|
1683
|
+
}
|
|
1684
|
+
current = { heading: heading[2]?.toLowerCase() ?? "", bullets: [] };
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
const bullet = BULLET.exec(line.trim());
|
|
1688
|
+
if (bullet && current.heading.length > 0) {
|
|
1689
|
+
current.bullets.push(bullet[1]?.trim() ?? "");
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
if (current.heading.length > 0 || current.bullets.length > 0) {
|
|
1693
|
+
sections.push(current);
|
|
1694
|
+
}
|
|
1695
|
+
return sections;
|
|
1696
|
+
}
|
|
1697
|
+
function collectBullets(sections, patterns) {
|
|
1698
|
+
const matches = sections.filter((section) => patterns.some((p) => p.test(section.heading)));
|
|
1699
|
+
return matches.flatMap((section) => section.bullets).filter((b) => b.length > 0);
|
|
1700
|
+
}
|
|
1701
|
+
function guessSeverity(text) {
|
|
1702
|
+
if (/critical|severe/i.test(text)) return "critical";
|
|
1703
|
+
if (/\bhigh\b|major/i.test(text)) return "high";
|
|
1704
|
+
if (/medium|moderate/i.test(text)) return "medium";
|
|
1705
|
+
if (/\blow\b|minor/i.test(text)) return "low";
|
|
1706
|
+
return void 0;
|
|
1707
|
+
}
|
|
1708
|
+
function extractReportFromText(raw, meta) {
|
|
1709
|
+
const trimmed = raw.trim();
|
|
1710
|
+
const nonEmpty = trimmed.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1711
|
+
const proseLine = nonEmpty.find((line) => !line.startsWith("#") && !BULLET.test(line)) ?? nonEmpty[0] ?? "";
|
|
1712
|
+
const cleaned = proseLine.replace(/^#{1,6}\s+/, "").replace(BULLET, "$1");
|
|
1713
|
+
const summary = cleaned.length > 0 ? truncate4(cleaned, 200) : "(no output)";
|
|
1714
|
+
const sections = parseSections(trimmed);
|
|
1715
|
+
const findingTexts = collectBullets(sections, [/finding/, /issue/, /problem/, /vulnerab/]);
|
|
1716
|
+
const recommendationTexts = collectBullets(sections, [/recommend/, /suggestion/, /next step/, /action/]);
|
|
1717
|
+
const evidenceTexts = collectBullets(sections, [/evidence/, /proof/, /log/]);
|
|
1718
|
+
const findings = findingTexts.map((text, index) => ({
|
|
1719
|
+
id: `${meta.taskId}-f-${index + 1}`,
|
|
1720
|
+
title: truncate4(text, 120),
|
|
1721
|
+
detail: text,
|
|
1722
|
+
severity: guessSeverity(text)
|
|
1723
|
+
}));
|
|
1724
|
+
return {
|
|
1725
|
+
id: `${meta.taskId}-report`,
|
|
1726
|
+
taskId: meta.taskId,
|
|
1727
|
+
agent: meta.agent,
|
|
1728
|
+
...meta.model !== void 0 ? { model: meta.model } : {},
|
|
1729
|
+
summary,
|
|
1730
|
+
findings,
|
|
1731
|
+
recommendations: recommendationTexts.map((t) => truncate4(t, 200)),
|
|
1732
|
+
metadata: evidenceTexts.length > 0 ? { evidence: evidenceTexts } : void 0,
|
|
1733
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
function truncate4(text, max) {
|
|
1737
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// src/reports/service.ts
|
|
1741
|
+
var ReportService = class {
|
|
1742
|
+
constructor(store, log = logger) {
|
|
1743
|
+
this.store = store;
|
|
1744
|
+
this.log = log.child({ component: "reports" });
|
|
1745
|
+
}
|
|
1746
|
+
store;
|
|
1747
|
+
log;
|
|
1748
|
+
get storeRef() {
|
|
1749
|
+
return this.store;
|
|
1750
|
+
}
|
|
1751
|
+
/** Normalizes finished tasks into reports (§17 — normalizer). */
|
|
1752
|
+
async collectFromTasks(tasks) {
|
|
1753
|
+
const reports = [];
|
|
1754
|
+
for (const task of tasks) {
|
|
1755
|
+
const structured = task.result?.reports ?? [];
|
|
1756
|
+
if (structured.length > 0) {
|
|
1757
|
+
reports.push(...structured);
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
const text = task.result?.rawOutput ?? task.result?.summary ?? "";
|
|
1761
|
+
if (text.trim().length === 0) continue;
|
|
1762
|
+
reports.push(
|
|
1763
|
+
extractReportFromText(text, {
|
|
1764
|
+
taskId: task.id,
|
|
1765
|
+
agent: task.agent ?? "unknown"
|
|
1766
|
+
})
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
return reports;
|
|
1770
|
+
}
|
|
1771
|
+
async saveReports(reports) {
|
|
1772
|
+
if (reports.length === 0) return;
|
|
1773
|
+
await this.store.saveReports(reports);
|
|
1774
|
+
this.log.info(`Stored ${reports.length} report(s).`);
|
|
1775
|
+
}
|
|
1776
|
+
async generateCombined(reports, meta = {}) {
|
|
1777
|
+
if (reports.length === 0) return void 0;
|
|
1778
|
+
const options = {
|
|
1779
|
+
...meta.objective !== void 0 ? { objective: meta.objective } : {},
|
|
1780
|
+
...meta.pipelineRunId !== void 0 ? { pipelineRunId: meta.pipelineRunId } : {},
|
|
1781
|
+
...meta.taskIds !== void 0 ? { taskIds: meta.taskIds } : {}
|
|
1782
|
+
};
|
|
1783
|
+
const combined = aggregateReports(reports, options);
|
|
1784
|
+
await this.store.saveCombined(combined);
|
|
1785
|
+
this.log.info(`Generated combined report ${combined.id}.`);
|
|
1786
|
+
return combined;
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
|
|
1790
|
+
// src/reports/store.ts
|
|
1791
|
+
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
1792
|
+
import { dirname as dirname2 } from "path";
|
|
1793
|
+
var FileReportStore = class {
|
|
1794
|
+
filePath;
|
|
1795
|
+
queue = Promise.resolve();
|
|
1796
|
+
constructor(directory = `${process.cwd()}/.omninode`) {
|
|
1797
|
+
this.filePath = `${directory}/reports.json`;
|
|
1798
|
+
}
|
|
1799
|
+
async saveReports(reports) {
|
|
1800
|
+
return this.synchronized(async () => {
|
|
1801
|
+
const entries = await this.load();
|
|
1802
|
+
for (const report of reports) {
|
|
1803
|
+
const index = entries.findIndex(
|
|
1804
|
+
(e) => e.kind === "report" && e.report.id === report.id
|
|
1805
|
+
);
|
|
1806
|
+
const entry2 = { kind: "report", report };
|
|
1807
|
+
if (index >= 0) entries[index] = entry2;
|
|
1808
|
+
else entries.push(entry2);
|
|
1809
|
+
}
|
|
1810
|
+
await this.write(entries);
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
async listReports(filter = {}) {
|
|
1814
|
+
return this.synchronized(async () => {
|
|
1815
|
+
const entries = await this.load();
|
|
1816
|
+
return entries.filter((e) => e.kind === "report").map((e) => e.report).filter((report) => filter.taskId ? report.taskId === filter.taskId : true).filter((report) => filter.agent ? report.agent === filter.agent : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
async saveCombined(combined) {
|
|
1820
|
+
return this.synchronized(async () => {
|
|
1821
|
+
const entries = await this.load();
|
|
1822
|
+
const index = entries.findIndex(
|
|
1823
|
+
(e) => e.kind === "combined" && e.combined.id === combined.id
|
|
1824
|
+
);
|
|
1825
|
+
const entry2 = { kind: "combined", combined };
|
|
1826
|
+
if (index >= 0) entries[index] = entry2;
|
|
1827
|
+
else entries.push(entry2);
|
|
1828
|
+
await this.write(entries);
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
async getCombined(id) {
|
|
1832
|
+
return this.synchronized(async () => {
|
|
1833
|
+
const entries = await this.load();
|
|
1834
|
+
const found = entries.find((e) => e.kind === "combined" && e.combined.id === id);
|
|
1835
|
+
return found?.kind === "combined" ? found.combined : void 0;
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
async listCombined(filter = {}) {
|
|
1839
|
+
return this.synchronized(async () => {
|
|
1840
|
+
const entries = await this.load();
|
|
1841
|
+
return entries.filter((e) => e.kind === "combined").map((e) => e.combined).filter(
|
|
1842
|
+
(combined) => filter.pipelineRunId ? combined.pipelineRunId === filter.pipelineRunId : true
|
|
1843
|
+
).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
synchronized(operation) {
|
|
1847
|
+
const result = this.queue.then(operation, operation);
|
|
1848
|
+
this.queue = result.then(
|
|
1849
|
+
() => void 0,
|
|
1850
|
+
() => void 0
|
|
1851
|
+
);
|
|
1852
|
+
return result;
|
|
1853
|
+
}
|
|
1854
|
+
async load() {
|
|
1855
|
+
try {
|
|
1856
|
+
const raw = await readFile2(this.filePath, "utf8");
|
|
1857
|
+
const parsed = JSON.parse(raw);
|
|
1858
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1859
|
+
} catch {
|
|
1860
|
+
return [];
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
async write(entries) {
|
|
1864
|
+
const tmp = `${this.filePath}.tmp`;
|
|
1865
|
+
await mkdir2(dirname2(this.filePath), { recursive: true });
|
|
1866
|
+
await writeFile2(tmp, `${JSON.stringify(entries, null, 2)}
|
|
1867
|
+
`, "utf8");
|
|
1868
|
+
await rename2(tmp, this.filePath);
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
|
|
1514
1872
|
// src/cli/commands/reports.ts
|
|
1873
|
+
function service() {
|
|
1874
|
+
return new ReportService(new FileReportStore());
|
|
1875
|
+
}
|
|
1515
1876
|
function registerReportCommands(program) {
|
|
1516
|
-
const report = program.command("report").description("Inspect AI reports (
|
|
1517
|
-
report.command("list").description("List
|
|
1518
|
-
|
|
1877
|
+
const report = program.command("report").description("Inspect AI reports collected from tasks and pipelines (\xA716\u2013\xA717).");
|
|
1878
|
+
report.command("list").description("List stored reports.").option("--task <taskId>", "Only reports for this task.").option("--agent <name>", "Only reports from this agent.").action(async (options) => {
|
|
1879
|
+
const svc = service();
|
|
1880
|
+
const reports = await svc.storeRef.listReports({
|
|
1881
|
+
...options.task !== void 0 ? { taskId: options.task } : {},
|
|
1882
|
+
...options.agent !== void 0 ? { agent: options.agent } : {}
|
|
1883
|
+
});
|
|
1884
|
+
if (reports.length === 0) {
|
|
1885
|
+
console.log("No reports stored yet. Run a pipeline to collect some.");
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
for (const r of reports) {
|
|
1889
|
+
console.log(`${r.id} ${r.agent.padEnd(10)} ${truncate(r.summary, 60)}`);
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
report.command("combined").description("List combined reports (aggregated intelligence, \xA717).").option("--run <pipelineRunId>", "Only combined reports for this pipeline run.").action(async (options) => {
|
|
1893
|
+
const svc = service();
|
|
1894
|
+
const combined = await svc.storeRef.listCombined({
|
|
1895
|
+
...options.run !== void 0 ? { pipelineRunId: options.run } : {}
|
|
1896
|
+
});
|
|
1897
|
+
if (combined.length === 0) {
|
|
1898
|
+
console.log("No combined reports stored yet. Run a pipeline to generate one.");
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
for (const c of combined) {
|
|
1902
|
+
console.log(
|
|
1903
|
+
`${c.id} ${c.findings.length} finding(s) from ${c.sources.length} source(s)${c.pipelineRunId ? ` run=${c.pipelineRunId}` : ""}`
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1519
1906
|
});
|
|
1907
|
+
report.command("show <id>").description("Show a stored report or combined report in full.").action(async (id) => {
|
|
1908
|
+
const svc = service();
|
|
1909
|
+
const reports = await svc.storeRef.listReports();
|
|
1910
|
+
const raw = reports.find((r) => r.id === id);
|
|
1911
|
+
if (raw) {
|
|
1912
|
+
console.log(`Report ${raw.id}`);
|
|
1913
|
+
console.log(` agent: ${raw.agent}${raw.model ? ` (${raw.model})` : ""}`);
|
|
1914
|
+
console.log(` task: ${raw.taskId}`);
|
|
1915
|
+
console.log(` summary: ${raw.summary}`);
|
|
1916
|
+
if (raw.findings.length > 0) {
|
|
1917
|
+
console.log(" findings:");
|
|
1918
|
+
for (const finding of raw.findings) {
|
|
1919
|
+
console.log(` - [${finding.severity ?? "info"}] ${finding.title}`);
|
|
1920
|
+
for (const evidence of finding.evidence ?? []) {
|
|
1921
|
+
console.log(` evidence: ${evidence}`);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
if (raw.recommendations.length > 0) {
|
|
1926
|
+
console.log(" recommendations:");
|
|
1927
|
+
for (const recommendation of raw.recommendations) {
|
|
1928
|
+
console.log(` - ${recommendation}`);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
const combined = await svc.storeRef.getCombined(id);
|
|
1934
|
+
if (combined) {
|
|
1935
|
+
console.log(`Combined report ${combined.id}`);
|
|
1936
|
+
console.log(` sources: ${combined.sources.join(", ")}`);
|
|
1937
|
+
console.log(` reports: ${combined.reportIds.length}`);
|
|
1938
|
+
if (combined.pipelineRunId) console.log(` run: ${combined.pipelineRunId}`);
|
|
1939
|
+
console.log(` summary: ${combined.summary}`);
|
|
1940
|
+
if (combined.findings.length > 0) {
|
|
1941
|
+
console.log(" findings (grouped, duplicates merged):");
|
|
1942
|
+
for (const finding of combined.findings) {
|
|
1943
|
+
console.log(
|
|
1944
|
+
` - [${finding.severity ?? "info"}] ${finding.title} (x${finding.occurrences}, sources: ${finding.sources.map((s) => s.agent).join(", ")})`
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
if (combined.recommendations.length > 0) {
|
|
1949
|
+
console.log(" recommendations:");
|
|
1950
|
+
for (const recommendation of combined.recommendations) {
|
|
1951
|
+
console.log(` - ${recommendation.text} (x${recommendation.occurrences})`);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
throw new OmniNodeError("REPORT_NOT_FOUND", `Report "${id}" was not found in the report store.`);
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// src/pipelines/engine.ts
|
|
1961
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
1962
|
+
var PipelineEngine = class {
|
|
1963
|
+
constructor(options) {
|
|
1964
|
+
this.options = options;
|
|
1965
|
+
this.log = (options.log ?? logger).child({ component: "pipelines" });
|
|
1966
|
+
}
|
|
1967
|
+
options;
|
|
1968
|
+
log;
|
|
1969
|
+
get store() {
|
|
1970
|
+
return this.options.store;
|
|
1971
|
+
}
|
|
1972
|
+
/** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
|
|
1973
|
+
validate(def) {
|
|
1974
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1975
|
+
for (const step of def.steps) {
|
|
1976
|
+
if (ids.has(step.id)) {
|
|
1977
|
+
throw new PipelineError("PIPELINE_INVALID", `Duplicate step id "${step.id}".`);
|
|
1978
|
+
}
|
|
1979
|
+
ids.add(step.id);
|
|
1980
|
+
}
|
|
1981
|
+
for (const step of def.steps) {
|
|
1982
|
+
for (const dep of step.dependsOn ?? []) {
|
|
1983
|
+
if (!ids.has(dep)) {
|
|
1984
|
+
throw new PipelineError(
|
|
1985
|
+
"PIPELINE_INVALID",
|
|
1986
|
+
`Step "${step.id}" depends on unknown step "${dep}".`
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
if (step.kind === "research" && (step.agents ?? []).length === 0) {
|
|
1991
|
+
throw new PipelineError(
|
|
1992
|
+
"PIPELINE_INVALID",
|
|
1993
|
+
`Research step "${step.id}" needs at least one agent.`
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
if ((step.kind === "execute" || step.kind === "custom") && !step.agent) {
|
|
1997
|
+
throw new PipelineError("PIPELINE_INVALID", `Step "${step.id}" (${step.kind}) needs an agent.`);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
this.assertAcyclic(def);
|
|
2001
|
+
}
|
|
2002
|
+
assertAcyclic(def) {
|
|
2003
|
+
const deps = this.effectiveDependencies(def);
|
|
2004
|
+
const remaining = /* @__PURE__ */ new Map();
|
|
2005
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
2006
|
+
for (const [id, list] of deps) {
|
|
2007
|
+
remaining.set(id, list.length);
|
|
2008
|
+
for (const d of list) {
|
|
2009
|
+
dependents.set(d, [...dependents.get(d) ?? [], id]);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
const queue = [...deps.keys()].filter((id) => (remaining.get(id) ?? 0) === 0);
|
|
2013
|
+
let processed = 0;
|
|
2014
|
+
while (queue.length > 0) {
|
|
2015
|
+
const id = queue.shift();
|
|
2016
|
+
processed += 1;
|
|
2017
|
+
for (const next of dependents.get(id) ?? []) {
|
|
2018
|
+
const left = (remaining.get(next) ?? 0) - 1;
|
|
2019
|
+
remaining.set(next, left);
|
|
2020
|
+
if (left === 0) queue.push(next);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
if (processed < deps.size) {
|
|
2024
|
+
const stuck = [...deps.keys()].filter((id) => (remaining.get(id) ?? 0) > 0);
|
|
2025
|
+
throw new PipelineError(
|
|
2026
|
+
"PIPELINE_INVALID",
|
|
2027
|
+
`Pipeline "${def.id}" has a dependency cycle involving: ${stuck.join(", ")}.`
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* Steps run sequentially in definition order unless they declare explicit
|
|
2033
|
+
* dependencies, which enable parallel branches.
|
|
2034
|
+
*/
|
|
2035
|
+
effectiveDependencies(def) {
|
|
2036
|
+
const deps = /* @__PURE__ */ new Map();
|
|
2037
|
+
def.steps.forEach((step, index) => {
|
|
2038
|
+
const explicit = step.dependsOn ?? [];
|
|
2039
|
+
deps.set(
|
|
2040
|
+
step.id,
|
|
2041
|
+
explicit.length > 0 ? explicit : index > 0 ? [def.steps[index - 1].id] : []
|
|
2042
|
+
);
|
|
2043
|
+
});
|
|
2044
|
+
return deps;
|
|
2045
|
+
}
|
|
2046
|
+
async run(def, options = {}) {
|
|
2047
|
+
this.validate(def);
|
|
2048
|
+
const objective = (options.objective ?? def.objective ?? "").trim();
|
|
2049
|
+
if (objective.length === 0) {
|
|
2050
|
+
throw new PipelineError(
|
|
2051
|
+
"PIPELINE_INVALID",
|
|
2052
|
+
`Pipeline "${def.id}" needs an objective \u2014 pass one or set it in the definition.`
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
const role = def.role ? this.options.roles.get(def.role) : void 0;
|
|
2056
|
+
if (def.role && !role) {
|
|
2057
|
+
throw new PipelineError(
|
|
2058
|
+
"PIPELINE_INVALID",
|
|
2059
|
+
`Role "${def.role}" (pipeline "${def.id}") is not defined in project.roles.`
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
for (const step of def.steps) {
|
|
2063
|
+
for (const name of [...step.agents ?? [], ...step.agent ? [step.agent] : []]) {
|
|
2064
|
+
if (!this.options.agents.get(name)) {
|
|
2065
|
+
throw new PipelineError(
|
|
2066
|
+
"PIPELINE_INVALID",
|
|
2067
|
+
`Step "${step.id}" references unregistered agent "${name}".`
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2073
|
+
const run2 = {
|
|
2074
|
+
id: `run-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
|
|
2075
|
+
pipelineId: def.id,
|
|
2076
|
+
status: "running",
|
|
2077
|
+
startedAt: now,
|
|
2078
|
+
stepRuns: []
|
|
2079
|
+
};
|
|
2080
|
+
await this.options.store.save(run2);
|
|
2081
|
+
this.log.info(`Running pipeline ${def.id} as ${run2.id}.`);
|
|
2082
|
+
const deps = this.effectiveDependencies(def);
|
|
2083
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
2084
|
+
const pending = new Set(def.steps.map((s) => s.id));
|
|
2085
|
+
const contextParts = [];
|
|
2086
|
+
const allReports = [];
|
|
2087
|
+
while (pending.size > 0) {
|
|
2088
|
+
const ready = def.steps.filter(
|
|
2089
|
+
(step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
|
|
2090
|
+
);
|
|
2091
|
+
if (ready.length === 0) {
|
|
2092
|
+
throw new PipelineError("PIPELINE_FAILED", `Pipeline "${def.id}" scheduling stalled.`);
|
|
2093
|
+
}
|
|
2094
|
+
const settled = await Promise.all(
|
|
2095
|
+
ready.map(async (step) => {
|
|
2096
|
+
if (!this.shouldRun(step, deps.get(step.id) ?? [], outcomes)) {
|
|
2097
|
+
return { status: "skipped", summary: "", taskIds: [], reports: [] };
|
|
2098
|
+
}
|
|
2099
|
+
const ctx = {
|
|
2100
|
+
objective,
|
|
2101
|
+
...role ? { role } : {},
|
|
2102
|
+
combinedContext: contextParts.join("\n\n")
|
|
2103
|
+
};
|
|
2104
|
+
return this.executeStep(step, ctx);
|
|
2105
|
+
})
|
|
2106
|
+
);
|
|
2107
|
+
ready.forEach((step, index) => {
|
|
2108
|
+
const outcome = settled[index];
|
|
2109
|
+
outcomes.set(step.id, outcome);
|
|
2110
|
+
pending.delete(step.id);
|
|
2111
|
+
allReports.push(...outcome.reports);
|
|
2112
|
+
if (outcome.status !== "skipped" && outcome.summary.length > 0) {
|
|
2113
|
+
const contribution = outcome.contextContribution ?? outcome.summary;
|
|
2114
|
+
contextParts.push(`## Step "${step.id}" (${step.kind})
|
|
2115
|
+
${contribution}`);
|
|
2116
|
+
}
|
|
2117
|
+
const stepRun = {
|
|
2118
|
+
stepId: step.id,
|
|
2119
|
+
status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
|
|
2120
|
+
...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
|
|
2121
|
+
...outcome.error !== void 0 ? { error: outcome.error } : {}
|
|
2122
|
+
};
|
|
2123
|
+
run2.stepRuns.push(stepRun);
|
|
2124
|
+
options.onStep?.(stepRun);
|
|
2125
|
+
});
|
|
2126
|
+
run2.reports = [...allReports];
|
|
2127
|
+
await this.options.store.save(run2);
|
|
2128
|
+
}
|
|
2129
|
+
const failed = [...outcomes.values()].some((o) => o.status === "failed");
|
|
2130
|
+
const finalStatus = failed ? "failed" : "completed";
|
|
2131
|
+
let combinedReportId;
|
|
2132
|
+
if (this.options.reports) {
|
|
2133
|
+
const taskIds = [...outcomes.values()].flatMap((o) => o.taskIds);
|
|
2134
|
+
const tasks = (await Promise.all(taskIds.map((id) => this.options.tasks.get(id)))).filter((task) => task !== void 0);
|
|
2135
|
+
const collected = await this.options.reports.collectFromTasks(tasks);
|
|
2136
|
+
await this.options.reports.saveReports(collected);
|
|
2137
|
+
const combined = await this.options.reports.generateCombined(collected, {
|
|
2138
|
+
objective,
|
|
2139
|
+
pipelineRunId: run2.id,
|
|
2140
|
+
taskIds
|
|
2141
|
+
});
|
|
2142
|
+
combinedReportId = combined?.id;
|
|
2143
|
+
if (collected.length > 0) {
|
|
2144
|
+
run2.reports = collected;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
const finished = {
|
|
2148
|
+
...run2,
|
|
2149
|
+
status: finalStatus,
|
|
2150
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2151
|
+
...run2.reports !== void 0 && run2.reports.length > 0 ? { reports: run2.reports } : {},
|
|
2152
|
+
...combinedReportId !== void 0 ? { combinedReportId } : {}
|
|
2153
|
+
};
|
|
2154
|
+
await this.options.store.save(finished);
|
|
2155
|
+
this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
|
|
2156
|
+
return finished;
|
|
2157
|
+
}
|
|
2158
|
+
async get(runId) {
|
|
2159
|
+
return this.options.store.get(runId);
|
|
2160
|
+
}
|
|
2161
|
+
async listRuns(filter) {
|
|
2162
|
+
return this.options.store.list(filter);
|
|
2163
|
+
}
|
|
2164
|
+
shouldRun(step, deps, outcomes) {
|
|
2165
|
+
const condition = step.condition ?? "on-success";
|
|
2166
|
+
const depStatuses = deps.map((d) => outcomes.get(d)?.status);
|
|
2167
|
+
const anyFailed = depStatuses.includes("failed");
|
|
2168
|
+
const anySkipped = depStatuses.includes("skipped");
|
|
2169
|
+
if (condition === "always") return true;
|
|
2170
|
+
if (condition === "on-failure") return anyFailed;
|
|
2171
|
+
return !anyFailed && !anySkipped;
|
|
2172
|
+
}
|
|
2173
|
+
async executeStep(step, ctx) {
|
|
2174
|
+
const retries = step.retries ?? 0;
|
|
2175
|
+
let last;
|
|
2176
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
2177
|
+
if (attempt > 0) {
|
|
2178
|
+
this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
|
|
2179
|
+
}
|
|
2180
|
+
last = await this.executeStepOnce(step, ctx);
|
|
2181
|
+
if (last.status !== "failed") return last;
|
|
2182
|
+
}
|
|
2183
|
+
return last;
|
|
2184
|
+
}
|
|
2185
|
+
async executeStepOnce(step, ctx) {
|
|
2186
|
+
switch (step.kind) {
|
|
2187
|
+
case "research":
|
|
2188
|
+
return this.runFanOut(step, ctx);
|
|
2189
|
+
case "collect":
|
|
2190
|
+
return { status: "completed", summary: "(collect: outputs gathered from previous steps)", taskIds: [], reports: [] };
|
|
2191
|
+
case "analyze":
|
|
2192
|
+
if (!step.agent) {
|
|
2193
|
+
return { status: "completed", summary: "(analyze: pass-through, no agent assigned)", taskIds: [], reports: [] };
|
|
2194
|
+
}
|
|
2195
|
+
return this.runSingle(step, ctx, step.agent);
|
|
2196
|
+
case "plan":
|
|
2197
|
+
return this.runPlan(step, ctx);
|
|
2198
|
+
case "execute":
|
|
2199
|
+
case "custom":
|
|
2200
|
+
return this.runSingle(step, ctx, step.agent);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
/** Research fan-out: one task per agent, in parallel (§15). */
|
|
2204
|
+
async runFanOut(step, ctx) {
|
|
2205
|
+
const agentNames = step.agents ?? [];
|
|
2206
|
+
const created = await Promise.all(
|
|
2207
|
+
agentNames.map(
|
|
2208
|
+
(name) => this.options.tasks.create({
|
|
2209
|
+
objective: ctx.objective,
|
|
2210
|
+
agent: name,
|
|
2211
|
+
...ctx.role ? { role: ctx.role.id } : {},
|
|
2212
|
+
...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
|
|
2213
|
+
})
|
|
2214
|
+
)
|
|
2215
|
+
);
|
|
2216
|
+
const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
|
|
2217
|
+
const failedTasks = results.filter((r) => r.status !== "completed");
|
|
2218
|
+
const lines = results.map((result, index) => {
|
|
2219
|
+
const agentName = agentNames[index] ?? "unknown";
|
|
2220
|
+
const outcome = result.result?.summary ?? result.result?.error ?? "(no output)";
|
|
2221
|
+
return `- ${agentName}: ${outcome}`;
|
|
2222
|
+
});
|
|
2223
|
+
const reports = results.flatMap((r) => r.result?.reports ?? []);
|
|
2224
|
+
const summary = lines.join("\n");
|
|
2225
|
+
if (failedTasks.length > 0) {
|
|
2226
|
+
const firstError = failedTasks[0]?.result?.error ?? "unknown error";
|
|
2227
|
+
return {
|
|
2228
|
+
status: "failed",
|
|
2229
|
+
summary,
|
|
2230
|
+
taskIds: created.map((t) => t.id),
|
|
2231
|
+
error: `${failedTasks.length}/${results.length} task(s) failed \u2014 ${firstError}`,
|
|
2232
|
+
reports
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
return { status: "completed", summary, taskIds: created.map((t) => t.id), reports };
|
|
2236
|
+
}
|
|
2237
|
+
async runSingle(step, ctx, agentName) {
|
|
2238
|
+
const task = await this.options.tasks.create({
|
|
2239
|
+
objective: ctx.objective,
|
|
2240
|
+
agent: agentName,
|
|
2241
|
+
...ctx.role ? { role: ctx.role.id } : {},
|
|
2242
|
+
...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
|
|
2243
|
+
});
|
|
2244
|
+
const result = await this.options.tasks.run(task.id);
|
|
2245
|
+
if (result.status !== "completed") {
|
|
2246
|
+
return {
|
|
2247
|
+
status: "failed",
|
|
2248
|
+
summary: result.result?.summary ?? "",
|
|
2249
|
+
taskIds: [task.id],
|
|
2250
|
+
error: result.result?.error ?? `task ${task.id} did not complete`,
|
|
2251
|
+
reports: result.result?.reports ?? []
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
return {
|
|
2255
|
+
status: "completed",
|
|
2256
|
+
summary: result.result?.summary ?? "(no output)",
|
|
2257
|
+
taskIds: [task.id],
|
|
2258
|
+
reports: result.result?.reports ?? []
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
async runPlan(step, ctx) {
|
|
2262
|
+
if (!step.model) {
|
|
2263
|
+
return { status: "completed", summary: "(plan: no model configured \u2014 skipped)", taskIds: [], reports: [] };
|
|
2264
|
+
}
|
|
2265
|
+
const messages = [
|
|
2266
|
+
{
|
|
2267
|
+
role: "system",
|
|
2268
|
+
content: "You are a planning assistant inside OmniNode. Given the objective and the combined research context, produce a prioritized, actionable plan."
|
|
2269
|
+
},
|
|
2270
|
+
{
|
|
2271
|
+
role: "user",
|
|
2272
|
+
content: `# Objective
|
|
2273
|
+
${ctx.objective}
|
|
2274
|
+
|
|
2275
|
+
${ctx.combinedContext}
|
|
2276
|
+
|
|
2277
|
+
Produce the final plan.`
|
|
2278
|
+
}
|
|
2279
|
+
];
|
|
2280
|
+
try {
|
|
2281
|
+
const planText = await this.getChat()(step.model, messages);
|
|
2282
|
+
return {
|
|
2283
|
+
status: "completed",
|
|
2284
|
+
summary: planText.split("\n", 1)[0] ?? planText,
|
|
2285
|
+
contextContribution: planText,
|
|
2286
|
+
taskIds: [],
|
|
2287
|
+
reports: []
|
|
2288
|
+
};
|
|
2289
|
+
} catch (error) {
|
|
2290
|
+
return {
|
|
2291
|
+
status: "failed",
|
|
2292
|
+
summary: "",
|
|
2293
|
+
taskIds: [],
|
|
2294
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2295
|
+
reports: []
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
getChat() {
|
|
2300
|
+
if (this.options.chat) return this.options.chat;
|
|
2301
|
+
throw new PipelineError(
|
|
2302
|
+
"PIPELINE_FAILED",
|
|
2303
|
+
"No chat function available \u2014 configure a provider for plan steps."
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2307
|
+
function createDefaultChatFn(providers) {
|
|
2308
|
+
return async (modelRef, messages) => {
|
|
2309
|
+
const separator = modelRef.indexOf(":");
|
|
2310
|
+
if (separator <= 0 || separator === modelRef.length - 1) {
|
|
2311
|
+
throw new PipelineError(
|
|
2312
|
+
"PIPELINE_INVALID",
|
|
2313
|
+
`Invalid model reference "${modelRef}". Use "provider:model-id".`
|
|
2314
|
+
);
|
|
2315
|
+
}
|
|
2316
|
+
const providerName = modelRef.slice(0, separator);
|
|
2317
|
+
const modelId = modelRef.slice(separator + 1);
|
|
2318
|
+
const providerConfig = providers.find((p) => p.name === providerName);
|
|
2319
|
+
if (!providerConfig) {
|
|
2320
|
+
throw new PipelineError(
|
|
2321
|
+
"PIPELINE_INVALID",
|
|
2322
|
+
`Provider "${providerName}" (model "${modelRef}") is not configured.`
|
|
2323
|
+
);
|
|
2324
|
+
}
|
|
2325
|
+
const provider = createProvider(providerConfig);
|
|
2326
|
+
const response = await provider.chat({ model: modelId, messages });
|
|
2327
|
+
return response.content;
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// src/pipelines/store.ts
|
|
2332
|
+
import { mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile3 } from "fs/promises";
|
|
2333
|
+
import { dirname as dirname3 } from "path";
|
|
2334
|
+
var FilePipelineRunStore = class {
|
|
2335
|
+
filePath;
|
|
2336
|
+
queue = Promise.resolve();
|
|
2337
|
+
constructor(directory = `${process.cwd()}/.omninode`) {
|
|
2338
|
+
this.filePath = `${directory}/pipelines.json`;
|
|
2339
|
+
}
|
|
2340
|
+
async save(run2) {
|
|
2341
|
+
return this.synchronized(async () => {
|
|
2342
|
+
const runs = await this.load();
|
|
2343
|
+
const index = runs.findIndex((r) => r.id === run2.id);
|
|
2344
|
+
if (index >= 0) runs[index] = run2;
|
|
2345
|
+
else runs.push(run2);
|
|
2346
|
+
await this.write(runs);
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
async get(id) {
|
|
2350
|
+
return this.synchronized(async () => {
|
|
2351
|
+
const runs = await this.load();
|
|
2352
|
+
return runs.find((r) => r.id === id);
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
async list(filter = {}) {
|
|
2356
|
+
return this.synchronized(async () => {
|
|
2357
|
+
const runs = await this.load();
|
|
2358
|
+
return runs.filter((run2) => filter.pipelineId ? run2.pipelineId === filter.pipelineId : true).filter((run2) => filter.status ? run2.status === filter.status : true).sort((a, b) => (a.startedAt ?? "").localeCompare(b.startedAt ?? ""));
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
synchronized(operation) {
|
|
2362
|
+
const result = this.queue.then(operation, operation);
|
|
2363
|
+
this.queue = result.then(
|
|
2364
|
+
() => void 0,
|
|
2365
|
+
() => void 0
|
|
2366
|
+
);
|
|
2367
|
+
return result;
|
|
2368
|
+
}
|
|
2369
|
+
async load() {
|
|
2370
|
+
try {
|
|
2371
|
+
const raw = await readFile3(this.filePath, "utf8");
|
|
2372
|
+
const parsed = JSON.parse(raw);
|
|
2373
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
2374
|
+
} catch {
|
|
2375
|
+
return [];
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
async write(runs) {
|
|
2379
|
+
const tmp = `${this.filePath}.tmp`;
|
|
2380
|
+
await mkdir3(dirname3(this.filePath), { recursive: true });
|
|
2381
|
+
await writeFile3(tmp, `${JSON.stringify(runs, null, 2)}
|
|
2382
|
+
`, "utf8");
|
|
2383
|
+
await rename3(tmp, this.filePath);
|
|
2384
|
+
}
|
|
2385
|
+
};
|
|
2386
|
+
|
|
2387
|
+
// src/pipelines/engine-factory.ts
|
|
2388
|
+
function buildPipelineEngine(config, log = logger) {
|
|
2389
|
+
const agents = buildAgentRegistry(config, log);
|
|
2390
|
+
const roles = buildRoleRegistry(config);
|
|
2391
|
+
const tasks = new TaskEngine({
|
|
2392
|
+
agents,
|
|
2393
|
+
roles,
|
|
2394
|
+
store: new FileTaskStore(),
|
|
2395
|
+
log
|
|
2396
|
+
});
|
|
2397
|
+
return new PipelineEngine({
|
|
2398
|
+
tasks,
|
|
2399
|
+
agents,
|
|
2400
|
+
roles,
|
|
2401
|
+
providers: config.project.providers,
|
|
2402
|
+
store: new FilePipelineRunStore(),
|
|
2403
|
+
chat: createDefaultChatFn(config.project.providers),
|
|
2404
|
+
reports: new ReportService(new FileReportStore(), log),
|
|
2405
|
+
log
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
// src/cli/commands/pipelines.ts
|
|
2410
|
+
function registerPipelineCommands(program) {
|
|
2411
|
+
const pipeline = program.command("pipeline").description("Run multi-step AI pipelines defined in omninode.yaml (\xA714).");
|
|
2412
|
+
pipeline.command("list").description("List configured pipelines.").action(async () => {
|
|
2413
|
+
const config = loadConfig();
|
|
2414
|
+
const pipelines = config.project.pipelines;
|
|
2415
|
+
if (pipelines.length === 0) {
|
|
2416
|
+
console.log("No pipelines configured. Add one under `project.pipelines` in omninode.yaml.");
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
for (const def of pipelines) {
|
|
2420
|
+
const name = def.name ? ` ${def.name}` : "";
|
|
2421
|
+
console.log(`${def.id}${name} ${def.steps.length} step(s)`);
|
|
2422
|
+
}
|
|
2423
|
+
});
|
|
2424
|
+
pipeline.command("run <id> [objective]").description(
|
|
2425
|
+
"Run a pipeline. The objective argument overrides the definition's default objective."
|
|
2426
|
+
).action(async (id, objective) => {
|
|
2427
|
+
const config = loadConfig();
|
|
2428
|
+
const def = findPipeline(config, id);
|
|
2429
|
+
const engine = buildPipelineEngine(config);
|
|
2430
|
+
console.log(
|
|
2431
|
+
`Running pipeline "${def.id}" (${def.steps.length} step(s))${objective ? ` \u2014 "${objective}"` : ""}...`
|
|
2432
|
+
);
|
|
2433
|
+
const finished = await engine.run(def, {
|
|
2434
|
+
...objective !== void 0 ? { objective } : {},
|
|
2435
|
+
onStep: (stepRun) => {
|
|
2436
|
+
const suffix = stepRun.error ? ` \u2014 ${stepRun.error}` : "";
|
|
2437
|
+
console.log(` [${stepRun.stepId}] ${stepRun.status}${suffix}`);
|
|
2438
|
+
}
|
|
2439
|
+
});
|
|
2440
|
+
if (finished.reports && finished.reports.length > 0) {
|
|
2441
|
+
console.log(` reports: ${finished.reports.length} structured report(s)`);
|
|
2442
|
+
}
|
|
2443
|
+
if (finished.combinedReportId) {
|
|
2444
|
+
console.log(` combined report: ${finished.combinedReportId}`);
|
|
2445
|
+
}
|
|
2446
|
+
console.log(`Pipeline ${finished.status}. Run id: ${finished.id}`);
|
|
2447
|
+
if (finished.status !== "completed") process.exitCode = 1;
|
|
2448
|
+
});
|
|
2449
|
+
pipeline.command("runs <id>").description("List recorded runs of a pipeline.").action(async (id) => {
|
|
2450
|
+
const config = loadConfig();
|
|
2451
|
+
const engine = buildPipelineEngine(config);
|
|
2452
|
+
const runs = await engine.listRuns({ pipelineId: id });
|
|
2453
|
+
if (runs.length === 0) {
|
|
2454
|
+
console.log(`No recorded runs for pipeline "${id}".`);
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2457
|
+
for (const run2 of runs) {
|
|
2458
|
+
console.log(`${run2.id} ${run2.status.padEnd(10)} ${run2.startedAt ?? "?"}`);
|
|
2459
|
+
}
|
|
2460
|
+
});
|
|
2461
|
+
pipeline.command("status <run-id>").description("Show the recorded state of a pipeline run.").action(async (runId) => {
|
|
2462
|
+
const config = loadConfig();
|
|
2463
|
+
const engine = buildPipelineEngine(config);
|
|
2464
|
+
const run2 = await engine.get(runId);
|
|
2465
|
+
if (!run2) {
|
|
2466
|
+
throw new PipelineError(
|
|
2467
|
+
"PIPELINE_NOT_FOUND",
|
|
2468
|
+
`Pipeline run "${runId}" does not exist in this project's run store.`
|
|
2469
|
+
);
|
|
2470
|
+
}
|
|
2471
|
+
console.log(` id: ${run2.id}`);
|
|
2472
|
+
console.log(` pipeline: ${run2.pipelineId}`);
|
|
2473
|
+
console.log(` status: ${run2.status}`);
|
|
2474
|
+
console.log(` started: ${run2.startedAt ?? "?"}`);
|
|
2475
|
+
if (run2.finishedAt) console.log(` finished: ${run2.finishedAt}`);
|
|
2476
|
+
for (const stepRun of run2.stepRuns) {
|
|
2477
|
+
console.log(` [${stepRun.stepId}] ${stepRun.status}`);
|
|
2478
|
+
}
|
|
2479
|
+
if (run2.reports && run2.reports.length > 0) {
|
|
2480
|
+
console.log(` reports: ${run2.reports.length}`);
|
|
2481
|
+
}
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
function findPipeline(config, id) {
|
|
2485
|
+
const def = config.project.pipelines.find((p) => p.id === id);
|
|
2486
|
+
if (!def) {
|
|
2487
|
+
const available = config.project.pipelines.map((p) => p.id).join(", ") || "(none)";
|
|
2488
|
+
throw new PipelineError(
|
|
2489
|
+
"PIPELINE_NOT_FOUND",
|
|
2490
|
+
`Pipeline "${id}" is not configured. Configured pipelines: ${available}.`
|
|
2491
|
+
);
|
|
2492
|
+
}
|
|
2493
|
+
return def;
|
|
1520
2494
|
}
|
|
1521
2495
|
|
|
1522
2496
|
// src/cli/index.ts
|
|
@@ -1533,6 +2507,7 @@ function createProgram() {
|
|
|
1533
2507
|
registerTaskCommands(program);
|
|
1534
2508
|
registerReportCommands(program);
|
|
1535
2509
|
registerModelsCommand(program);
|
|
2510
|
+
registerPipelineCommands(program);
|
|
1536
2511
|
return program;
|
|
1537
2512
|
}
|
|
1538
2513
|
async function run(argv) {
|