@hilbras/omninode 0.7.0 → 0.9.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 +826 -45
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +289 -14
- package/dist/index.js +741 -57
- 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.9.0";
|
|
10
10
|
|
|
11
11
|
// src/errors/index.ts
|
|
12
12
|
var OmniNodeError = class extends Error {
|
|
@@ -49,6 +49,16 @@ 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
|
+
};
|
|
57
|
+
var MemoryError = class extends OmniNodeError {
|
|
58
|
+
constructor(message, options = {}) {
|
|
59
|
+
super("MEMORY_UNAVAILABLE", message, options);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
52
62
|
function isOmniNodeError(value) {
|
|
53
63
|
return value instanceof OmniNodeError;
|
|
54
64
|
}
|
|
@@ -106,13 +116,23 @@ var pipelineConfigSchema = z.object({
|
|
|
106
116
|
role: z.string().optional(),
|
|
107
117
|
steps: z.array(pipelineStepConfigSchema).min(1)
|
|
108
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();
|
|
109
124
|
var projectConfigSchema = z.object({
|
|
110
125
|
name: z.string().min(1),
|
|
111
126
|
providers: z.array(providerConfigSchema).default([]),
|
|
112
127
|
agents: z.array(agentConfigSchema).default([]),
|
|
113
128
|
roles: z.array(roleConfigSchema).default([]),
|
|
114
129
|
pipelines: z.array(pipelineConfigSchema).default([]),
|
|
115
|
-
|
|
130
|
+
planner: plannerConfigSchema.optional(),
|
|
131
|
+
memory: z.object({
|
|
132
|
+
provider: z.string().min(1).default("local"),
|
|
133
|
+
base_url: z.string().url().optional(),
|
|
134
|
+
api_key_env_var: z.string().min(1).optional()
|
|
135
|
+
}).strict().optional()
|
|
116
136
|
}).strict();
|
|
117
137
|
var appConfigSchema = z.object({
|
|
118
138
|
project: projectConfigSchema,
|
|
@@ -182,7 +202,20 @@ function toAppConfig(yaml) {
|
|
|
182
202
|
agents: yaml.project.agents.map(toAgentConfig),
|
|
183
203
|
roles: yaml.project.roles.map(toRoleDefinition),
|
|
184
204
|
pipelines: yaml.project.pipelines.map(toPipelineDefinition),
|
|
185
|
-
...yaml.project.
|
|
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
|
+
} : {},
|
|
212
|
+
...yaml.project.memory !== void 0 ? {
|
|
213
|
+
memory: {
|
|
214
|
+
provider: yaml.project.memory.provider,
|
|
215
|
+
baseUrl: yaml.project.memory.base_url,
|
|
216
|
+
apiKeyEnvVar: yaml.project.memory.api_key_env_var
|
|
217
|
+
}
|
|
218
|
+
} : {}
|
|
186
219
|
},
|
|
187
220
|
...yaml.logging !== void 0 ? { logging: yaml.logging } : {}
|
|
188
221
|
};
|
|
@@ -1389,13 +1422,24 @@ var TaskEngine = class {
|
|
|
1389
1422
|
const role = task.role ? this.options.roles.get(task.role) : void 0;
|
|
1390
1423
|
const running = await this.transition(task, "running");
|
|
1391
1424
|
this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
|
|
1425
|
+
let background = running.context?.background;
|
|
1426
|
+
if (this.options.memory) {
|
|
1427
|
+
const memoryContext = await this.options.memory.gatherContext({
|
|
1428
|
+
objective: running.objective,
|
|
1429
|
+
...running.role ? { role: running.role } : {},
|
|
1430
|
+
...running.project ? { project: running.project } : {}
|
|
1431
|
+
});
|
|
1432
|
+
if (memoryContext.length > 0) {
|
|
1433
|
+
background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1392
1436
|
let output;
|
|
1393
1437
|
try {
|
|
1394
1438
|
output = await agent.run({
|
|
1395
1439
|
taskId: running.id,
|
|
1396
1440
|
objective: running.objective,
|
|
1397
1441
|
...role ? { role } : {},
|
|
1398
|
-
...
|
|
1442
|
+
...background !== void 0 ? { context: background } : {}
|
|
1399
1443
|
});
|
|
1400
1444
|
} catch (error) {
|
|
1401
1445
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1413,8 +1457,12 @@ var TaskEngine = class {
|
|
|
1413
1457
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1414
1458
|
};
|
|
1415
1459
|
const finalStatus = output.status === "completed" ? "completed" : "failed";
|
|
1460
|
+
const finished = await this.transition(running, finalStatus, { result });
|
|
1416
1461
|
this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
|
|
1417
|
-
|
|
1462
|
+
if (this.options.memory) {
|
|
1463
|
+
await this.options.memory.recordTaskOutcome(finished);
|
|
1464
|
+
}
|
|
1465
|
+
return finished;
|
|
1418
1466
|
}
|
|
1419
1467
|
async cancel(id) {
|
|
1420
1468
|
const task = await this.mustGet(id);
|
|
@@ -1459,6 +1507,297 @@ function generateTaskId() {
|
|
|
1459
1507
|
return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
1460
1508
|
}
|
|
1461
1509
|
|
|
1510
|
+
// src/memory/local.ts
|
|
1511
|
+
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
1512
|
+
import { dirname as dirname2 } from "path";
|
|
1513
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
1514
|
+
"the",
|
|
1515
|
+
"and",
|
|
1516
|
+
"for",
|
|
1517
|
+
"with",
|
|
1518
|
+
"this",
|
|
1519
|
+
"that",
|
|
1520
|
+
"from",
|
|
1521
|
+
"into",
|
|
1522
|
+
"was",
|
|
1523
|
+
"were",
|
|
1524
|
+
"have",
|
|
1525
|
+
"has",
|
|
1526
|
+
"had",
|
|
1527
|
+
"are",
|
|
1528
|
+
"but",
|
|
1529
|
+
"not",
|
|
1530
|
+
"you",
|
|
1531
|
+
"his",
|
|
1532
|
+
"her",
|
|
1533
|
+
"its",
|
|
1534
|
+
"their",
|
|
1535
|
+
"them",
|
|
1536
|
+
"then",
|
|
1537
|
+
"than",
|
|
1538
|
+
"when",
|
|
1539
|
+
"what",
|
|
1540
|
+
"which",
|
|
1541
|
+
"will",
|
|
1542
|
+
"would",
|
|
1543
|
+
"been",
|
|
1544
|
+
"were",
|
|
1545
|
+
"also",
|
|
1546
|
+
"because",
|
|
1547
|
+
"about",
|
|
1548
|
+
"after",
|
|
1549
|
+
"before"
|
|
1550
|
+
]);
|
|
1551
|
+
function keywords(text) {
|
|
1552
|
+
return new Set(
|
|
1553
|
+
(text ?? "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 4 && !STOPWORDS.has(t))
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1556
|
+
function relevanceScore(queryKeywords, entry2) {
|
|
1557
|
+
if (queryKeywords.size === 0) return 0;
|
|
1558
|
+
const entryKeywords = keywords(`${entry2.content} ${entry2.key}`);
|
|
1559
|
+
let overlap = 0;
|
|
1560
|
+
for (const keyword of queryKeywords) {
|
|
1561
|
+
if (entryKeywords.has(keyword)) overlap += 1;
|
|
1562
|
+
}
|
|
1563
|
+
let tagMatches = 0;
|
|
1564
|
+
for (const tag of entry2.tags ?? []) {
|
|
1565
|
+
if (queryKeywords.has(tag.toLowerCase())) tagMatches += 1;
|
|
1566
|
+
}
|
|
1567
|
+
return overlap * 2 + tagMatches * 1.5;
|
|
1568
|
+
}
|
|
1569
|
+
var LocalMemoryProvider = class {
|
|
1570
|
+
name = "local";
|
|
1571
|
+
filePath;
|
|
1572
|
+
queue = Promise.resolve();
|
|
1573
|
+
constructor(directory = `${process.cwd()}/.omninode`) {
|
|
1574
|
+
this.filePath = `${directory}/memory.json`;
|
|
1575
|
+
}
|
|
1576
|
+
async query(query) {
|
|
1577
|
+
return this.synchronized(async () => {
|
|
1578
|
+
const entries = await this.load();
|
|
1579
|
+
const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter(
|
|
1580
|
+
(entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
|
|
1581
|
+
);
|
|
1582
|
+
if (!query.text || query.text.trim().length === 0) {
|
|
1583
|
+
return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
|
|
1584
|
+
}
|
|
1585
|
+
const queryKeywords = keywords(query.text);
|
|
1586
|
+
return filtered.map((entry2) => ({ entry: entry2, score: relevanceScore(queryKeywords, entry2) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry: entry2 }) => entry2);
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
async write(entry2) {
|
|
1590
|
+
return this.synchronized(async () => {
|
|
1591
|
+
const entries = await this.load();
|
|
1592
|
+
const index = entries.findIndex((e) => e.key === entry2.key);
|
|
1593
|
+
const stamped = {
|
|
1594
|
+
...entry2,
|
|
1595
|
+
createdAt: entry2.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1596
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1597
|
+
};
|
|
1598
|
+
if (index >= 0) entries[index] = stamped;
|
|
1599
|
+
else entries.push(stamped);
|
|
1600
|
+
await this.saveAll(entries);
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
async count() {
|
|
1604
|
+
return (await this.load()).length;
|
|
1605
|
+
}
|
|
1606
|
+
synchronized(operation) {
|
|
1607
|
+
const result = this.queue.then(operation, operation);
|
|
1608
|
+
this.queue = result.then(
|
|
1609
|
+
() => void 0,
|
|
1610
|
+
() => void 0
|
|
1611
|
+
);
|
|
1612
|
+
return result;
|
|
1613
|
+
}
|
|
1614
|
+
async load() {
|
|
1615
|
+
try {
|
|
1616
|
+
const raw = await readFile2(this.filePath, "utf8");
|
|
1617
|
+
const parsed = JSON.parse(raw);
|
|
1618
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1619
|
+
} catch {
|
|
1620
|
+
return [];
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
async saveAll(entries) {
|
|
1624
|
+
const tmp = `${this.filePath}.tmp`;
|
|
1625
|
+
await mkdir2(dirname2(this.filePath), { recursive: true });
|
|
1626
|
+
await writeFile2(tmp, `${JSON.stringify(entries, null, 2)}
|
|
1627
|
+
`, "utf8");
|
|
1628
|
+
await rename2(tmp, this.filePath);
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
|
|
1632
|
+
// src/memory/remembera.ts
|
|
1633
|
+
var RememberaMemoryProvider = class {
|
|
1634
|
+
name = "remembera";
|
|
1635
|
+
baseUrl;
|
|
1636
|
+
apiKeyEnvVar;
|
|
1637
|
+
env;
|
|
1638
|
+
log;
|
|
1639
|
+
constructor(config, log = logger) {
|
|
1640
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
1641
|
+
this.apiKeyEnvVar = config.apiKeyEnvVar;
|
|
1642
|
+
this.env = config.env ?? process.env;
|
|
1643
|
+
this.log = log.child({ component: "remembera" });
|
|
1644
|
+
}
|
|
1645
|
+
headers() {
|
|
1646
|
+
const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
|
|
1647
|
+
return key ? { authorization: `Bearer ${key}` } : {};
|
|
1648
|
+
}
|
|
1649
|
+
async query(query) {
|
|
1650
|
+
const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
|
|
1651
|
+
if (status !== 200) {
|
|
1652
|
+
throw new MemoryError(`Remembera query returned HTTP ${status}.`);
|
|
1653
|
+
}
|
|
1654
|
+
const entries = body.entries;
|
|
1655
|
+
if (!Array.isArray(entries)) {
|
|
1656
|
+
throw new MemoryError("Remembera query response had no entries array.");
|
|
1657
|
+
}
|
|
1658
|
+
return entries.map((entry2) => this.toEntry(entry2)).filter((entry2) => entry2 !== void 0);
|
|
1659
|
+
}
|
|
1660
|
+
async write(entry2) {
|
|
1661
|
+
const { status } = await this.request(`${this.baseUrl}/api/memory`, entry2);
|
|
1662
|
+
if (status !== 200 && status !== 201) {
|
|
1663
|
+
throw new MemoryError(`Remembera write returned HTTP ${status}.`);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
async request(url, body) {
|
|
1667
|
+
const headers = this.headers();
|
|
1668
|
+
try {
|
|
1669
|
+
return await requestJson(url, {
|
|
1670
|
+
method: "POST",
|
|
1671
|
+
headers,
|
|
1672
|
+
body
|
|
1673
|
+
});
|
|
1674
|
+
} catch (error) {
|
|
1675
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1676
|
+
throw new MemoryError(`Remembera request to ${url} failed: ${message}`, { cause: error });
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
toEntry(raw) {
|
|
1680
|
+
if (raw === null || typeof raw !== "object") {
|
|
1681
|
+
this.log.warn("Skipping malformed Remembera memory entry.");
|
|
1682
|
+
return void 0;
|
|
1683
|
+
}
|
|
1684
|
+
const entry2 = raw;
|
|
1685
|
+
if (typeof entry2.key !== "string" || typeof entry2.content !== "string") {
|
|
1686
|
+
this.log.warn("Skipping Remembera memory entry without key/content.");
|
|
1687
|
+
return void 0;
|
|
1688
|
+
}
|
|
1689
|
+
const scope = entry2.scope;
|
|
1690
|
+
return {
|
|
1691
|
+
key: entry2.key,
|
|
1692
|
+
content: entry2.content,
|
|
1693
|
+
scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
|
|
1694
|
+
...Array.isArray(entry2.tags) ? { tags: entry2.tags.filter((t) => typeof t === "string") } : {},
|
|
1695
|
+
...typeof entry2.createdAt === "string" ? { createdAt: entry2.createdAt } : {},
|
|
1696
|
+
...typeof entry2.updatedAt === "string" ? { updatedAt: entry2.updatedAt } : {}
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
|
|
1701
|
+
// src/memory/factory.ts
|
|
1702
|
+
function createMemoryProvider(config) {
|
|
1703
|
+
if (!config || config.provider === "local") {
|
|
1704
|
+
return new LocalMemoryProvider();
|
|
1705
|
+
}
|
|
1706
|
+
if (config.provider === "remembera") {
|
|
1707
|
+
if (!config.baseUrl) {
|
|
1708
|
+
throw new ConfigError(
|
|
1709
|
+
"CONFIG_INVALID",
|
|
1710
|
+
`Memory provider "remembera" needs a base_url in omninode.yaml.`
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
return new RememberaMemoryProvider({
|
|
1714
|
+
baseUrl: config.baseUrl,
|
|
1715
|
+
apiKeyEnvVar: config.apiKeyEnvVar
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
throw new ConfigError(
|
|
1719
|
+
"CONFIG_INVALID",
|
|
1720
|
+
`Unknown memory provider "${config.provider}". Available: local, remembera.`
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// src/memory/service.ts
|
|
1725
|
+
var MAX_CONTEXT_CHARS = 4e3;
|
|
1726
|
+
var MemoryService = class {
|
|
1727
|
+
constructor(provider, log = logger) {
|
|
1728
|
+
this.provider = provider;
|
|
1729
|
+
this.log = log.child({ component: "memory", provider: provider.name });
|
|
1730
|
+
}
|
|
1731
|
+
provider;
|
|
1732
|
+
log;
|
|
1733
|
+
get providerRef() {
|
|
1734
|
+
return this.provider;
|
|
1735
|
+
}
|
|
1736
|
+
get providerName() {
|
|
1737
|
+
return this.provider.name;
|
|
1738
|
+
}
|
|
1739
|
+
/** Relevant context for a task objective; empty string when nothing matches. */
|
|
1740
|
+
async gatherContext(query) {
|
|
1741
|
+
try {
|
|
1742
|
+
const entries = await this.provider.query({
|
|
1743
|
+
text: query.objective,
|
|
1744
|
+
...query.role ? { tags: [query.role] } : {},
|
|
1745
|
+
limit: 8
|
|
1746
|
+
});
|
|
1747
|
+
if (entries.length === 0) return "";
|
|
1748
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
1749
|
+
for (const entry2 of entries) {
|
|
1750
|
+
byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
|
|
1751
|
+
}
|
|
1752
|
+
const parts = [];
|
|
1753
|
+
for (const scope of ["project", "task", "role", "knowledge"]) {
|
|
1754
|
+
const scopeEntries = byScope.get(scope) ?? [];
|
|
1755
|
+
if (scopeEntries.length === 0) continue;
|
|
1756
|
+
const lines = scopeEntries.map((entry2) => `- ${entry2.content}`).join("\n");
|
|
1757
|
+
parts.push(`### Relevant memory (${scope})
|
|
1758
|
+
${lines}`);
|
|
1759
|
+
}
|
|
1760
|
+
const context = parts.join("\n\n");
|
|
1761
|
+
return context.length > MAX_CONTEXT_CHARS ? `${context.slice(0, MAX_CONTEXT_CHARS)}\u2026` : context;
|
|
1762
|
+
} catch (error) {
|
|
1763
|
+
this.log.warn(
|
|
1764
|
+
`Memory query failed (continuing without memory): ${error instanceof Error ? error.message : String(error)}`
|
|
1765
|
+
);
|
|
1766
|
+
return "";
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
/** Records a finished task's outcome — best-effort. */
|
|
1770
|
+
async recordTaskOutcome(task) {
|
|
1771
|
+
const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
|
|
1772
|
+
try {
|
|
1773
|
+
await this.provider.write({
|
|
1774
|
+
key: `task:${task.id}`,
|
|
1775
|
+
scope: "task",
|
|
1776
|
+
content: `Objective: ${task.objective}
|
|
1777
|
+
Status: ${task.status}
|
|
1778
|
+
Outcome: ${outcome}`,
|
|
1779
|
+
tags: ["task", ...task.agent ? [task.agent] : [], ...task.role ? [task.role] : []],
|
|
1780
|
+
createdAt: task.createdAt
|
|
1781
|
+
});
|
|
1782
|
+
const significant = (task.result?.reports ?? []).flatMap(
|
|
1783
|
+
(report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
|
|
1784
|
+
);
|
|
1785
|
+
for (const { report, finding } of significant) {
|
|
1786
|
+
await this.provider.write({
|
|
1787
|
+
key: `finding:${report.id}:${finding.id}`,
|
|
1788
|
+
scope: "project",
|
|
1789
|
+
content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
|
|
1790
|
+
tags: ["known-problem", report.agent]
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
} catch (error) {
|
|
1794
|
+
this.log.warn(
|
|
1795
|
+
`Memory write failed (continuing): ${error instanceof Error ? error.message : String(error)}`
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1462
1801
|
// src/roles/index.ts
|
|
1463
1802
|
var RoleRegistry = class {
|
|
1464
1803
|
roles = /* @__PURE__ */ new Map();
|
|
@@ -1483,10 +1822,13 @@ function buildRoleRegistry(config) {
|
|
|
1483
1822
|
|
|
1484
1823
|
// src/tasks/engine-factory.ts
|
|
1485
1824
|
function createTaskEngine(config, log = logger, directory) {
|
|
1825
|
+
const agents = buildAgentRegistry(config, log);
|
|
1826
|
+
const roles = buildRoleRegistry(config);
|
|
1486
1827
|
return new TaskEngine({
|
|
1487
|
-
agents
|
|
1488
|
-
roles
|
|
1828
|
+
agents,
|
|
1829
|
+
roles,
|
|
1489
1830
|
store: new FileTaskStore(directory),
|
|
1831
|
+
...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
|
|
1490
1832
|
log
|
|
1491
1833
|
});
|
|
1492
1834
|
}
|
|
@@ -1788,8 +2130,8 @@ var ReportService = class {
|
|
|
1788
2130
|
};
|
|
1789
2131
|
|
|
1790
2132
|
// src/reports/store.ts
|
|
1791
|
-
import { mkdir as
|
|
1792
|
-
import { dirname as
|
|
2133
|
+
import { mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile3 } from "fs/promises";
|
|
2134
|
+
import { dirname as dirname3 } from "path";
|
|
1793
2135
|
var FileReportStore = class {
|
|
1794
2136
|
filePath;
|
|
1795
2137
|
queue = Promise.resolve();
|
|
@@ -1853,7 +2195,7 @@ var FileReportStore = class {
|
|
|
1853
2195
|
}
|
|
1854
2196
|
async load() {
|
|
1855
2197
|
try {
|
|
1856
|
-
const raw = await
|
|
2198
|
+
const raw = await readFile3(this.filePath, "utf8");
|
|
1857
2199
|
const parsed = JSON.parse(raw);
|
|
1858
2200
|
return Array.isArray(parsed) ? parsed : [];
|
|
1859
2201
|
} catch {
|
|
@@ -1862,10 +2204,10 @@ var FileReportStore = class {
|
|
|
1862
2204
|
}
|
|
1863
2205
|
async write(entries) {
|
|
1864
2206
|
const tmp = `${this.filePath}.tmp`;
|
|
1865
|
-
await
|
|
1866
|
-
await
|
|
2207
|
+
await mkdir3(dirname3(this.filePath), { recursive: true });
|
|
2208
|
+
await writeFile3(tmp, `${JSON.stringify(entries, null, 2)}
|
|
1867
2209
|
`, "utf8");
|
|
1868
|
-
await
|
|
2210
|
+
await rename3(tmp, this.filePath);
|
|
1869
2211
|
}
|
|
1870
2212
|
};
|
|
1871
2213
|
|
|
@@ -1958,7 +2300,231 @@ function registerReportCommands(program) {
|
|
|
1958
2300
|
}
|
|
1959
2301
|
|
|
1960
2302
|
// src/pipelines/engine.ts
|
|
2303
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
2304
|
+
|
|
2305
|
+
// src/planner/heuristic.ts
|
|
1961
2306
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
2307
|
+
var SEVERITY_RANK2 = {
|
|
2308
|
+
info: 0,
|
|
2309
|
+
low: 1,
|
|
2310
|
+
medium: 2,
|
|
2311
|
+
high: 3,
|
|
2312
|
+
critical: 4
|
|
2313
|
+
};
|
|
2314
|
+
var HeuristicPlanner = class {
|
|
2315
|
+
name = "heuristic";
|
|
2316
|
+
async plan(request) {
|
|
2317
|
+
const steps = [];
|
|
2318
|
+
const findings = request.reports.flatMap(
|
|
2319
|
+
(report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
|
|
2320
|
+
);
|
|
2321
|
+
const recommendations = dedupe(request.reports.flatMap((r) => r.recommendations));
|
|
2322
|
+
for (const [index, text] of recommendations.entries()) {
|
|
2323
|
+
steps.push({
|
|
2324
|
+
id: `step-${steps.length + 1}`,
|
|
2325
|
+
title: truncate5(text, 120),
|
|
2326
|
+
order: steps.length + 1,
|
|
2327
|
+
...index > 0 ? { dependsOn: [`step-${steps.length}`] } : {}
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
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"]);
|
|
2331
|
+
for (const { finding } of significant) {
|
|
2332
|
+
steps.push(remediationStep(finding, steps.length + 1));
|
|
2333
|
+
}
|
|
2334
|
+
if (steps.length === 0) {
|
|
2335
|
+
steps.push({
|
|
2336
|
+
id: "step-1",
|
|
2337
|
+
title: "Gather additional information",
|
|
2338
|
+
description: "The research reports contained no actionable recommendations or significant findings. Investigate the objective further before planning implementation work.",
|
|
2339
|
+
order: 1
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
const risks = significant.filter(({ finding }) => finding.severity === "critical").map(({ finding }) => `Critical finding unaddressed until its step completes: ${finding.title}`);
|
|
2343
|
+
return {
|
|
2344
|
+
id: `plan-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
|
|
2345
|
+
objective: request.objective,
|
|
2346
|
+
summary: `Derived from ${request.reports.length} report(s): ${recommendations.length} recommendation step(s), ${significant.length} remediation step(s).`,
|
|
2347
|
+
steps,
|
|
2348
|
+
...risks.length > 0 ? { risks } : {},
|
|
2349
|
+
generatedBy: "heuristic",
|
|
2350
|
+
...request.taskId !== void 0 ? { taskId: request.taskId } : {},
|
|
2351
|
+
...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
|
|
2352
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
};
|
|
2356
|
+
function remediationStep(finding, order) {
|
|
2357
|
+
return {
|
|
2358
|
+
id: `step-${order}`,
|
|
2359
|
+
title: `Fix: ${truncate5(finding.title, 100)}`,
|
|
2360
|
+
description: finding.detail,
|
|
2361
|
+
order,
|
|
2362
|
+
dependsOn: order > 1 ? [`step-${order - 1}`] : [],
|
|
2363
|
+
sourceFindings: [finding.id]
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
function dedupe(texts) {
|
|
2367
|
+
return [...new Set(texts.map((t) => t.trim()).filter((t) => t.length > 0))];
|
|
2368
|
+
}
|
|
2369
|
+
function truncate5(text, max) {
|
|
2370
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
// src/planner/model.ts
|
|
2374
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
2375
|
+
import { z as z2 } from "zod";
|
|
2376
|
+
|
|
2377
|
+
// src/planner/context.ts
|
|
2378
|
+
function buildPlannerContext(request) {
|
|
2379
|
+
const parts = [`# Objective
|
|
2380
|
+
${request.objective}`];
|
|
2381
|
+
if (request.role) {
|
|
2382
|
+
const roleLines = [`# Role
|
|
2383
|
+
${request.role.name} (${request.role.id})`];
|
|
2384
|
+
if (request.role.responsibilities.length > 0) {
|
|
2385
|
+
roleLines.push("Responsibilities:", ...request.role.responsibilities.map((r) => `- ${r}`));
|
|
2386
|
+
}
|
|
2387
|
+
if (request.role.expectedOutputs.length > 0) {
|
|
2388
|
+
roleLines.push("Expected outputs:", ...request.role.expectedOutputs.map((o) => `- ${o}`));
|
|
2389
|
+
}
|
|
2390
|
+
parts.push(roleLines.join("\n"));
|
|
2391
|
+
}
|
|
2392
|
+
if (request.reports.length > 0) {
|
|
2393
|
+
const agents = [...new Set(request.reports.map((r) => r.agent))].join(", ");
|
|
2394
|
+
const findingLines = request.reports.flatMap(
|
|
2395
|
+
(report) => report.findings.map(
|
|
2396
|
+
(finding) => `- [${finding.severity ?? "info"}] ${finding.title} (source: ${report.agent}, finding: ${finding.id})` + (finding.detail ? ` \u2014 ${finding.detail}` : "")
|
|
2397
|
+
)
|
|
2398
|
+
);
|
|
2399
|
+
parts.push(
|
|
2400
|
+
`# Findings (${request.reports.length} report(s) by ${agents})
|
|
2401
|
+
` + (findingLines.length > 0 ? findingLines.join("\n") : "(no findings reported)")
|
|
2402
|
+
);
|
|
2403
|
+
const recommendationLines = request.reports.flatMap(
|
|
2404
|
+
(report) => report.recommendations.map((text) => `- ${text} (source: ${report.agent})`)
|
|
2405
|
+
);
|
|
2406
|
+
if (recommendationLines.length > 0) {
|
|
2407
|
+
parts.push(`# Recommendations
|
|
2408
|
+
${recommendationLines.join("\n")}`);
|
|
2409
|
+
}
|
|
2410
|
+
const summaryLines = request.reports.map((report) => `- [${report.agent}] ${report.summary}`);
|
|
2411
|
+
parts.push(`# Report summaries
|
|
2412
|
+
${summaryLines.join("\n")}`);
|
|
2413
|
+
}
|
|
2414
|
+
if (request.memory && request.memory.length > 0) {
|
|
2415
|
+
parts.push(
|
|
2416
|
+
`# Relevant memory
|
|
2417
|
+
` + request.memory.map((entry2) => `- [${entry2.scope}] ${entry2.content}`).join("\n")
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
if (request.context && request.context.length > 0) {
|
|
2421
|
+
parts.push(`# Research context
|
|
2422
|
+
${request.context}`);
|
|
2423
|
+
}
|
|
2424
|
+
return parts.join("\n\n");
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// src/planner/model.ts
|
|
2428
|
+
var planStepJsonSchema = z2.object({
|
|
2429
|
+
id: z2.string().optional(),
|
|
2430
|
+
title: z2.string().min(1),
|
|
2431
|
+
description: z2.string().optional(),
|
|
2432
|
+
order: z2.number().int().optional(),
|
|
2433
|
+
targets: z2.array(z2.string()).optional(),
|
|
2434
|
+
acceptance_criteria: z2.array(z2.string()).optional(),
|
|
2435
|
+
depends_on: z2.array(z2.string()).optional(),
|
|
2436
|
+
source_findings: z2.array(z2.string()).optional()
|
|
2437
|
+
}).strict();
|
|
2438
|
+
var planJsonSchema = z2.object({
|
|
2439
|
+
summary: z2.string().min(1),
|
|
2440
|
+
steps: z2.array(planStepJsonSchema).min(1),
|
|
2441
|
+
risks: z2.array(z2.string()).optional(),
|
|
2442
|
+
notes: z2.array(z2.string()).optional()
|
|
2443
|
+
}).strict();
|
|
2444
|
+
var ModelPlanner = class {
|
|
2445
|
+
constructor(options) {
|
|
2446
|
+
this.options = options;
|
|
2447
|
+
this.log = (options.log ?? logger).child({ component: "planner", model: options.model });
|
|
2448
|
+
}
|
|
2449
|
+
options;
|
|
2450
|
+
name = "model";
|
|
2451
|
+
log;
|
|
2452
|
+
async plan(request) {
|
|
2453
|
+
const context = buildPlannerContext(request);
|
|
2454
|
+
const messages = [
|
|
2455
|
+
{
|
|
2456
|
+
role: "system",
|
|
2457
|
+
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 ?? "")
|
|
2458
|
+
},
|
|
2459
|
+
{ role: "user", content: context }
|
|
2460
|
+
];
|
|
2461
|
+
let responseText;
|
|
2462
|
+
try {
|
|
2463
|
+
responseText = await this.options.chat(this.options.model, messages);
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
return this.degrade(request, `planner model call failed: ${describe(error)}`);
|
|
2466
|
+
}
|
|
2467
|
+
const parsed = parsePlanJson(responseText);
|
|
2468
|
+
if (!parsed) {
|
|
2469
|
+
return this.degrade(request, "planner model output was not valid plan JSON");
|
|
2470
|
+
}
|
|
2471
|
+
const plan = {
|
|
2472
|
+
id: `plan-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
|
|
2473
|
+
objective: request.objective,
|
|
2474
|
+
summary: parsed.summary,
|
|
2475
|
+
steps: parsed.steps,
|
|
2476
|
+
...parsed.risks !== void 0 ? { risks: parsed.risks } : {},
|
|
2477
|
+
...parsed.notes !== void 0 ? { notes: parsed.notes } : {},
|
|
2478
|
+
generatedBy: `model:${this.options.model}`,
|
|
2479
|
+
model: this.options.model,
|
|
2480
|
+
...request.taskId !== void 0 ? { taskId: request.taskId } : {},
|
|
2481
|
+
...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
|
|
2482
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2483
|
+
};
|
|
2484
|
+
return plan;
|
|
2485
|
+
}
|
|
2486
|
+
async degrade(request, reason) {
|
|
2487
|
+
if (this.options.fallback) {
|
|
2488
|
+
this.log.warn(`${reason} \u2014 using the heuristic fallback planner.`);
|
|
2489
|
+
return this.options.fallback.plan(request);
|
|
2490
|
+
}
|
|
2491
|
+
throw new PlannerError("PLANNER_FAILED", `Planner failed: ${reason}.`);
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2494
|
+
function parsePlanJson(text) {
|
|
2495
|
+
const unfenced = text.replace(/```(?:json)?/gi, "");
|
|
2496
|
+
const start = unfenced.indexOf("{");
|
|
2497
|
+
const end = unfenced.lastIndexOf("}");
|
|
2498
|
+
if (start < 0 || end <= start) return void 0;
|
|
2499
|
+
try {
|
|
2500
|
+
const json = JSON.parse(unfenced.slice(start, end + 1));
|
|
2501
|
+
const parsed = planJsonSchema.safeParse(json);
|
|
2502
|
+
if (!parsed.success) return void 0;
|
|
2503
|
+
const steps = parsed.data.steps.map((step, index) => ({
|
|
2504
|
+
id: step.id ?? `step-${index + 1}`,
|
|
2505
|
+
title: step.title,
|
|
2506
|
+
description: step.description,
|
|
2507
|
+
order: step.order ?? index + 1,
|
|
2508
|
+
targets: step.targets,
|
|
2509
|
+
acceptanceCriteria: step.acceptance_criteria,
|
|
2510
|
+
dependsOn: step.depends_on,
|
|
2511
|
+
sourceFindings: step.source_findings
|
|
2512
|
+
}));
|
|
2513
|
+
return {
|
|
2514
|
+
summary: parsed.data.summary,
|
|
2515
|
+
steps: steps.sort((a, b) => a.order - b.order),
|
|
2516
|
+
risks: parsed.data.risks,
|
|
2517
|
+
notes: parsed.data.notes
|
|
2518
|
+
};
|
|
2519
|
+
} catch {
|
|
2520
|
+
return void 0;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
function describe(error) {
|
|
2524
|
+
return error instanceof Error ? error.message : String(error);
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
// src/pipelines/engine.ts
|
|
1962
2528
|
var PipelineEngine = class {
|
|
1963
2529
|
constructor(options) {
|
|
1964
2530
|
this.options = options;
|
|
@@ -2071,7 +2637,7 @@ var PipelineEngine = class {
|
|
|
2071
2637
|
}
|
|
2072
2638
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2073
2639
|
const run2 = {
|
|
2074
|
-
id: `run-${Date.now().toString(36)}-${
|
|
2640
|
+
id: `run-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
|
|
2075
2641
|
pipelineId: def.id,
|
|
2076
2642
|
status: "running",
|
|
2077
2643
|
startedAt: now,
|
|
@@ -2099,7 +2665,9 @@ var PipelineEngine = class {
|
|
|
2099
2665
|
const ctx = {
|
|
2100
2666
|
objective,
|
|
2101
2667
|
...role ? { role } : {},
|
|
2102
|
-
combinedContext: contextParts.join("\n\n")
|
|
2668
|
+
combinedContext: contextParts.join("\n\n"),
|
|
2669
|
+
reports: [...allReports],
|
|
2670
|
+
pipelineRunId: run2.id
|
|
2103
2671
|
};
|
|
2104
2672
|
return this.executeStep(step, ctx);
|
|
2105
2673
|
})
|
|
@@ -2118,6 +2686,7 @@ ${contribution}`);
|
|
|
2118
2686
|
stepId: step.id,
|
|
2119
2687
|
status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
|
|
2120
2688
|
...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
|
|
2689
|
+
...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
|
|
2121
2690
|
...outcome.error !== void 0 ? { error: outcome.error } : {}
|
|
2122
2691
|
};
|
|
2123
2692
|
run2.stepRuns.push(stepRun);
|
|
@@ -2128,6 +2697,7 @@ ${contribution}`);
|
|
|
2128
2697
|
}
|
|
2129
2698
|
const failed = [...outcomes.values()].some((o) => o.status === "failed");
|
|
2130
2699
|
const finalStatus = failed ? "failed" : "completed";
|
|
2700
|
+
const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
|
|
2131
2701
|
let combinedReportId;
|
|
2132
2702
|
if (this.options.reports) {
|
|
2133
2703
|
const taskIds = [...outcomes.values()].flatMap((o) => o.taskIds);
|
|
@@ -2149,7 +2719,8 @@ ${contribution}`);
|
|
|
2149
2719
|
status: finalStatus,
|
|
2150
2720
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2151
2721
|
...run2.reports !== void 0 && run2.reports.length > 0 ? { reports: run2.reports } : {},
|
|
2152
|
-
...combinedReportId !== void 0 ? { combinedReportId } : {}
|
|
2722
|
+
...combinedReportId !== void 0 ? { combinedReportId } : {},
|
|
2723
|
+
...planId !== void 0 ? { planId } : {}
|
|
2153
2724
|
};
|
|
2154
2725
|
await this.options.store.save(finished);
|
|
2155
2726
|
this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
|
|
@@ -2259,33 +2830,34 @@ ${contribution}`);
|
|
|
2259
2830
|
};
|
|
2260
2831
|
}
|
|
2261
2832
|
async runPlan(step, ctx) {
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
{
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
try {
|
|
2281
|
-
const planText = await this.getChat()(step.model, messages);
|
|
2833
|
+
const request = {
|
|
2834
|
+
objective: ctx.objective,
|
|
2835
|
+
reports: ctx.reports,
|
|
2836
|
+
...ctx.combinedContext.length > 0 ? { context: ctx.combinedContext } : {},
|
|
2837
|
+
...ctx.role ? { role: ctx.role } : {},
|
|
2838
|
+
...ctx.pipelineRunId !== void 0 ? { pipelineRunId: ctx.pipelineRunId } : {}
|
|
2839
|
+
};
|
|
2840
|
+
let planner;
|
|
2841
|
+
if (step.model) {
|
|
2842
|
+
planner = new ModelPlanner({
|
|
2843
|
+
model: step.model,
|
|
2844
|
+
chat: this.getChat(),
|
|
2845
|
+
fallback: new HeuristicPlanner(),
|
|
2846
|
+
log: this.log
|
|
2847
|
+
});
|
|
2848
|
+
} else if (this.options.planner) {
|
|
2849
|
+
planner = this.options.planner;
|
|
2850
|
+
} else {
|
|
2282
2851
|
return {
|
|
2283
2852
|
status: "completed",
|
|
2284
|
-
summary:
|
|
2285
|
-
contextContribution: planText,
|
|
2853
|
+
summary: "(plan: no model or planner configured \u2014 skipped)",
|
|
2286
2854
|
taskIds: [],
|
|
2287
2855
|
reports: []
|
|
2288
2856
|
};
|
|
2857
|
+
}
|
|
2858
|
+
let plan;
|
|
2859
|
+
try {
|
|
2860
|
+
plan = await planner.plan(request);
|
|
2289
2861
|
} catch (error) {
|
|
2290
2862
|
return {
|
|
2291
2863
|
status: "failed",
|
|
@@ -2295,6 +2867,15 @@ Produce the final plan.`
|
|
|
2295
2867
|
reports: []
|
|
2296
2868
|
};
|
|
2297
2869
|
}
|
|
2870
|
+
await this.options.plans?.save(plan);
|
|
2871
|
+
return {
|
|
2872
|
+
status: "completed",
|
|
2873
|
+
summary: plan.summary,
|
|
2874
|
+
contextContribution: formatPlan(plan),
|
|
2875
|
+
taskIds: [],
|
|
2876
|
+
planId: plan.id,
|
|
2877
|
+
reports: []
|
|
2878
|
+
};
|
|
2298
2879
|
}
|
|
2299
2880
|
getChat() {
|
|
2300
2881
|
if (this.options.chat) return this.options.chat;
|
|
@@ -2304,6 +2885,21 @@ Produce the final plan.`
|
|
|
2304
2885
|
);
|
|
2305
2886
|
}
|
|
2306
2887
|
};
|
|
2888
|
+
function formatPlan(plan) {
|
|
2889
|
+
const lines = [
|
|
2890
|
+
`# Implementation Plan (by ${plan.generatedBy})`,
|
|
2891
|
+
plan.summary,
|
|
2892
|
+
"Steps:",
|
|
2893
|
+
...plan.steps.map((step) => {
|
|
2894
|
+
const targets = step.targets && step.targets.length > 0 ? ` [targets: ${step.targets.join(", ")}]` : "";
|
|
2895
|
+
return `${step.order}. ${step.title}${targets}`;
|
|
2896
|
+
})
|
|
2897
|
+
];
|
|
2898
|
+
if (plan.risks && plan.risks.length > 0) {
|
|
2899
|
+
lines.push("Risks:", ...plan.risks.map((risk) => `- ${risk}`));
|
|
2900
|
+
}
|
|
2901
|
+
return lines.join("\n");
|
|
2902
|
+
}
|
|
2307
2903
|
function createDefaultChatFn(providers) {
|
|
2308
2904
|
return async (modelRef, messages) => {
|
|
2309
2905
|
const separator = modelRef.indexOf(":");
|
|
@@ -2329,8 +2925,8 @@ function createDefaultChatFn(providers) {
|
|
|
2329
2925
|
}
|
|
2330
2926
|
|
|
2331
2927
|
// src/pipelines/store.ts
|
|
2332
|
-
import { mkdir as
|
|
2333
|
-
import { dirname as
|
|
2928
|
+
import { mkdir as mkdir4, readFile as readFile4, rename as rename4, writeFile as writeFile4 } from "fs/promises";
|
|
2929
|
+
import { dirname as dirname4 } from "path";
|
|
2334
2930
|
var FilePipelineRunStore = class {
|
|
2335
2931
|
filePath;
|
|
2336
2932
|
queue = Promise.resolve();
|
|
@@ -2368,7 +2964,7 @@ var FilePipelineRunStore = class {
|
|
|
2368
2964
|
}
|
|
2369
2965
|
async load() {
|
|
2370
2966
|
try {
|
|
2371
|
-
const raw = await
|
|
2967
|
+
const raw = await readFile4(this.filePath, "utf8");
|
|
2372
2968
|
const parsed = JSON.parse(raw);
|
|
2373
2969
|
return Array.isArray(parsed) ? parsed : [];
|
|
2374
2970
|
} catch {
|
|
@@ -2377,21 +2973,100 @@ var FilePipelineRunStore = class {
|
|
|
2377
2973
|
}
|
|
2378
2974
|
async write(runs) {
|
|
2379
2975
|
const tmp = `${this.filePath}.tmp`;
|
|
2380
|
-
await
|
|
2381
|
-
await
|
|
2976
|
+
await mkdir4(dirname4(this.filePath), { recursive: true });
|
|
2977
|
+
await writeFile4(tmp, `${JSON.stringify(runs, null, 2)}
|
|
2382
2978
|
`, "utf8");
|
|
2383
|
-
await
|
|
2979
|
+
await rename4(tmp, this.filePath);
|
|
2384
2980
|
}
|
|
2385
2981
|
};
|
|
2386
2982
|
|
|
2983
|
+
// src/planner/store.ts
|
|
2984
|
+
import { mkdir as mkdir5, readFile as readFile5, rename as rename5, writeFile as writeFile5 } from "fs/promises";
|
|
2985
|
+
import { dirname as dirname5 } from "path";
|
|
2986
|
+
var FilePlanStore = class {
|
|
2987
|
+
filePath;
|
|
2988
|
+
queue = Promise.resolve();
|
|
2989
|
+
constructor(directory = `${process.cwd()}/.omninode`) {
|
|
2990
|
+
this.filePath = `${directory}/plans.json`;
|
|
2991
|
+
}
|
|
2992
|
+
async save(plan) {
|
|
2993
|
+
return this.synchronized(async () => {
|
|
2994
|
+
const plans = await this.load();
|
|
2995
|
+
const index = plans.findIndex((p) => p.id === plan.id);
|
|
2996
|
+
if (index >= 0) plans[index] = plan;
|
|
2997
|
+
else plans.push(plan);
|
|
2998
|
+
await this.write(plans);
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
async get(id) {
|
|
3002
|
+
return this.synchronized(async () => {
|
|
3003
|
+
const plans = await this.load();
|
|
3004
|
+
return plans.find((p) => p.id === id);
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
async list(filter = {}) {
|
|
3008
|
+
return this.synchronized(async () => {
|
|
3009
|
+
const plans = await this.load();
|
|
3010
|
+
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));
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
synchronized(operation) {
|
|
3014
|
+
const result = this.queue.then(operation, operation);
|
|
3015
|
+
this.queue = result.then(
|
|
3016
|
+
() => void 0,
|
|
3017
|
+
() => void 0
|
|
3018
|
+
);
|
|
3019
|
+
return result;
|
|
3020
|
+
}
|
|
3021
|
+
async load() {
|
|
3022
|
+
try {
|
|
3023
|
+
const raw = await readFile5(this.filePath, "utf8");
|
|
3024
|
+
const parsed = JSON.parse(raw);
|
|
3025
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3026
|
+
} catch {
|
|
3027
|
+
return [];
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
async write(plans) {
|
|
3031
|
+
const tmp = `${this.filePath}.tmp`;
|
|
3032
|
+
await mkdir5(dirname5(this.filePath), { recursive: true });
|
|
3033
|
+
await writeFile5(tmp, `${JSON.stringify(plans, null, 2)}
|
|
3034
|
+
`, "utf8");
|
|
3035
|
+
await rename5(tmp, this.filePath);
|
|
3036
|
+
}
|
|
3037
|
+
};
|
|
3038
|
+
|
|
3039
|
+
// src/planner/factory.ts
|
|
3040
|
+
function buildPlanner(plannerConfig, chat, log = logger) {
|
|
3041
|
+
if (!plannerConfig) return void 0;
|
|
3042
|
+
if (plannerConfig.kind === "heuristic") {
|
|
3043
|
+
return new HeuristicPlanner();
|
|
3044
|
+
}
|
|
3045
|
+
if (!plannerConfig.model) {
|
|
3046
|
+
throw new ConfigError(
|
|
3047
|
+
"CONFIG_INVALID",
|
|
3048
|
+
'Planner kind "model" needs a model reference ("provider:model-id") in omninode.yaml.'
|
|
3049
|
+
);
|
|
3050
|
+
}
|
|
3051
|
+
return new ModelPlanner({
|
|
3052
|
+
model: plannerConfig.model,
|
|
3053
|
+
chat,
|
|
3054
|
+
...plannerConfig.instruction !== void 0 ? { instruction: plannerConfig.instruction } : {},
|
|
3055
|
+
fallback: new HeuristicPlanner(),
|
|
3056
|
+
log
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
|
|
2387
3060
|
// src/pipelines/engine-factory.ts
|
|
2388
3061
|
function buildPipelineEngine(config, log = logger) {
|
|
2389
3062
|
const agents = buildAgentRegistry(config, log);
|
|
2390
3063
|
const roles = buildRoleRegistry(config);
|
|
3064
|
+
const chat = createDefaultChatFn(config.project.providers);
|
|
2391
3065
|
const tasks = new TaskEngine({
|
|
2392
3066
|
agents,
|
|
2393
3067
|
roles,
|
|
2394
3068
|
store: new FileTaskStore(),
|
|
3069
|
+
...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
|
|
2395
3070
|
log
|
|
2396
3071
|
});
|
|
2397
3072
|
return new PipelineEngine({
|
|
@@ -2400,7 +3075,9 @@ function buildPipelineEngine(config, log = logger) {
|
|
|
2400
3075
|
roles,
|
|
2401
3076
|
providers: config.project.providers,
|
|
2402
3077
|
store: new FilePipelineRunStore(),
|
|
2403
|
-
chat
|
|
3078
|
+
chat,
|
|
3079
|
+
...config.project.planner !== void 0 ? { planner: buildPlanner(config.project.planner, chat, log) } : {},
|
|
3080
|
+
plans: new FilePlanStore(),
|
|
2404
3081
|
reports: new ReportService(new FileReportStore(), log),
|
|
2405
3082
|
log
|
|
2406
3083
|
});
|
|
@@ -2443,6 +3120,9 @@ function registerPipelineCommands(program) {
|
|
|
2443
3120
|
if (finished.combinedReportId) {
|
|
2444
3121
|
console.log(` combined report: ${finished.combinedReportId}`);
|
|
2445
3122
|
}
|
|
3123
|
+
if (finished.planId) {
|
|
3124
|
+
console.log(` plan: ${finished.planId}`);
|
|
3125
|
+
}
|
|
2446
3126
|
console.log(`Pipeline ${finished.status}. Run id: ${finished.id}`);
|
|
2447
3127
|
if (finished.status !== "completed") process.exitCode = 1;
|
|
2448
3128
|
});
|
|
@@ -2493,6 +3173,105 @@ function findPipeline(config, id) {
|
|
|
2493
3173
|
return def;
|
|
2494
3174
|
}
|
|
2495
3175
|
|
|
3176
|
+
// src/cli/commands/memory.ts
|
|
3177
|
+
function registerMemoryCommands(program) {
|
|
3178
|
+
const memory = program.command("memory").description("Inspect and manage persistent project memory (\xA719\u2013\xA720).");
|
|
3179
|
+
memory.command("status").description("Show the active memory provider and how much is stored.").action(async () => {
|
|
3180
|
+
const config = loadConfig();
|
|
3181
|
+
const configured = config.project.memory?.provider ?? "local (default \u2014 add a `memory:` section to opt in)";
|
|
3182
|
+
const svc = new MemoryService(createMemoryProvider(config.project.memory));
|
|
3183
|
+
const entries = await svc.providerRef.query({ limit: 1e4 });
|
|
3184
|
+
console.log(`provider: ${configured}`);
|
|
3185
|
+
console.log(`entries: ${entries.length}`);
|
|
3186
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
3187
|
+
for (const entry2 of entries) {
|
|
3188
|
+
scopes.set(entry2.scope, (scopes.get(entry2.scope) ?? 0) + 1);
|
|
3189
|
+
}
|
|
3190
|
+
for (const [scope, count] of [...scopes.entries()].sort()) {
|
|
3191
|
+
console.log(` ${scope}: ${count}`);
|
|
3192
|
+
}
|
|
3193
|
+
});
|
|
3194
|
+
memory.command("query <text>").description("Search memory for entries relevant to the text.").option("-s, --scope <scope>", "Restrict to a scope: project, task, role or knowledge.").option("-n, --limit <count>", "Maximum entries to return.", (value) => Number(value), 10).action(async (text, options) => {
|
|
3195
|
+
const config = loadConfig();
|
|
3196
|
+
const svc = new MemoryService(createMemoryProvider(config.project.memory));
|
|
3197
|
+
const entries = await svc.providerRef.query({
|
|
3198
|
+
text,
|
|
3199
|
+
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
3200
|
+
limit: options.limit
|
|
3201
|
+
});
|
|
3202
|
+
if (entries.length === 0) {
|
|
3203
|
+
console.log("No relevant memory found.");
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3206
|
+
for (const entry2 of entries) {
|
|
3207
|
+
console.log(`[${entry2.scope}] ${entry2.key}`);
|
|
3208
|
+
console.log(` ${truncate(entry2.content.replace(/\n/g, " "), 120)}`);
|
|
3209
|
+
}
|
|
3210
|
+
});
|
|
3211
|
+
memory.command("write <text>").description("Write an entry to memory.").option("-s, --scope <scope>", "Memory scope: project, task, role or knowledge.", "project").option(
|
|
3212
|
+
"-t, --tags <tags>",
|
|
3213
|
+
"Comma-separated tags.",
|
|
3214
|
+
(value) => value.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0)
|
|
3215
|
+
).option("-k, --key <key>", "Entry key (defaults to a generated note key).").action(async (text, options) => {
|
|
3216
|
+
const config = loadConfig();
|
|
3217
|
+
const svc = new MemoryService(createMemoryProvider(config.project.memory));
|
|
3218
|
+
const key = options.key ?? `note:${Date.now().toString(36)}`;
|
|
3219
|
+
await svc.providerRef.write({
|
|
3220
|
+
key,
|
|
3221
|
+
scope: options.scope,
|
|
3222
|
+
content: text,
|
|
3223
|
+
...options.tags !== void 0 && options.tags.length > 0 ? { tags: options.tags } : {}
|
|
3224
|
+
});
|
|
3225
|
+
console.log(`Stored memory entry "${key}" (${options.scope}).`);
|
|
3226
|
+
});
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
// src/cli/commands/plan.ts
|
|
3230
|
+
function registerPlanCommands(program) {
|
|
3231
|
+
const plan = program.command("plan").description("Inspect implementation plans produced by the planner layer (\xA718).");
|
|
3232
|
+
plan.command("list").description("List stored plans.").option("--run <pipelineRunId>", "Only plans for this pipeline run.").action(async (options) => {
|
|
3233
|
+
const store = new FilePlanStore();
|
|
3234
|
+
const plans = await store.list({
|
|
3235
|
+
...options.run !== void 0 ? { pipelineRunId: options.run } : {}
|
|
3236
|
+
});
|
|
3237
|
+
if (plans.length === 0) {
|
|
3238
|
+
console.log("No plans stored yet. Run a pipeline with a plan step to generate one.");
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
for (const p of plans) {
|
|
3242
|
+
console.log(`${p.id} ${p.steps.length} step(s) by ${p.generatedBy} ${truncate(p.summary, 50)}`);
|
|
3243
|
+
}
|
|
3244
|
+
});
|
|
3245
|
+
plan.command("show <id>").description("Show a stored implementation plan in full.").action(async (id) => {
|
|
3246
|
+
const store = new FilePlanStore();
|
|
3247
|
+
const found = await store.get(id);
|
|
3248
|
+
if (!found) {
|
|
3249
|
+
throw new OmniNodeError("PLAN_INVALID", `Plan "${id}" was not found in the plan store.`, {
|
|
3250
|
+
details: { id }
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
console.log(`Plan ${found.id}`);
|
|
3254
|
+
console.log(` objective: ${found.objective}`);
|
|
3255
|
+
console.log(` by: ${found.generatedBy}`);
|
|
3256
|
+
console.log(` summary: ${found.summary}`);
|
|
3257
|
+
console.log(" steps:");
|
|
3258
|
+
for (const step of [...found.steps].sort((a, b) => a.order - b.order)) {
|
|
3259
|
+
const targets = step.targets && step.targets.length > 0 ? ` [${step.targets.join(", ")}]` : "";
|
|
3260
|
+
console.log(` ${step.order}. ${step.title}${targets}`);
|
|
3261
|
+
if (step.description) console.log(` ${truncate(step.description, 100)}`);
|
|
3262
|
+
for (const criterion of step.acceptanceCriteria ?? []) {
|
|
3263
|
+
console.log(` accept: ${criterion}`);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
if (found.risks && found.risks.length > 0) {
|
|
3267
|
+
console.log(" risks:");
|
|
3268
|
+
for (const risk of found.risks) {
|
|
3269
|
+
console.log(` - ${risk}`);
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
|
|
2496
3275
|
// src/cli/index.ts
|
|
2497
3276
|
function createProgram() {
|
|
2498
3277
|
const program = new Command();
|
|
@@ -2508,6 +3287,8 @@ function createProgram() {
|
|
|
2508
3287
|
registerReportCommands(program);
|
|
2509
3288
|
registerModelsCommand(program);
|
|
2510
3289
|
registerPipelineCommands(program);
|
|
3290
|
+
registerMemoryCommands(program);
|
|
3291
|
+
registerPlanCommands(program);
|
|
2511
3292
|
return program;
|
|
2512
3293
|
}
|
|
2513
3294
|
async function run(argv) {
|