@hilbras/omninode 0.6.0 → 0.8.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 +31 -3
- package/dist/cli/index.js +798 -21
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +224 -12
- package/dist/index.js +657 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -160,7 +160,11 @@ var projectConfigSchema = z.object({
|
|
|
160
160
|
agents: z.array(agentConfigSchema).default([]),
|
|
161
161
|
roles: z.array(roleConfigSchema).default([]),
|
|
162
162
|
pipelines: z.array(pipelineConfigSchema).default([]),
|
|
163
|
-
memory: z.object({
|
|
163
|
+
memory: z.object({
|
|
164
|
+
provider: z.string().min(1).default("local"),
|
|
165
|
+
base_url: z.string().url().optional(),
|
|
166
|
+
api_key_env_var: z.string().min(1).optional()
|
|
167
|
+
}).strict().optional()
|
|
164
168
|
}).strict();
|
|
165
169
|
var appConfigSchema = z.object({
|
|
166
170
|
project: projectConfigSchema,
|
|
@@ -230,7 +234,13 @@ function toAppConfig(yaml) {
|
|
|
230
234
|
agents: yaml.project.agents.map(toAgentConfig),
|
|
231
235
|
roles: yaml.project.roles.map(toRoleDefinition),
|
|
232
236
|
pipelines: yaml.project.pipelines.map(toPipelineDefinition),
|
|
233
|
-
...yaml.project.memory !== void 0 ? {
|
|
237
|
+
...yaml.project.memory !== void 0 ? {
|
|
238
|
+
memory: {
|
|
239
|
+
provider: yaml.project.memory.provider,
|
|
240
|
+
baseUrl: yaml.project.memory.base_url,
|
|
241
|
+
apiKeyEnvVar: yaml.project.memory.api_key_env_var
|
|
242
|
+
}
|
|
243
|
+
} : {}
|
|
234
244
|
},
|
|
235
245
|
...yaml.logging !== void 0 ? { logging: yaml.logging } : {}
|
|
236
246
|
};
|
|
@@ -1079,13 +1089,24 @@ var TaskEngine = class {
|
|
|
1079
1089
|
const role = task.role ? this.options.roles.get(task.role) : void 0;
|
|
1080
1090
|
const running = await this.transition(task, "running");
|
|
1081
1091
|
this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
|
|
1092
|
+
let background = running.context?.background;
|
|
1093
|
+
if (this.options.memory) {
|
|
1094
|
+
const memoryContext = await this.options.memory.gatherContext({
|
|
1095
|
+
objective: running.objective,
|
|
1096
|
+
...running.role ? { role: running.role } : {},
|
|
1097
|
+
...running.project ? { project: running.project } : {}
|
|
1098
|
+
});
|
|
1099
|
+
if (memoryContext.length > 0) {
|
|
1100
|
+
background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1082
1103
|
let output;
|
|
1083
1104
|
try {
|
|
1084
1105
|
output = await agent.run({
|
|
1085
1106
|
taskId: running.id,
|
|
1086
1107
|
objective: running.objective,
|
|
1087
1108
|
...role ? { role } : {},
|
|
1088
|
-
...
|
|
1109
|
+
...background !== void 0 ? { context: background } : {}
|
|
1089
1110
|
});
|
|
1090
1111
|
} catch (error) {
|
|
1091
1112
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1097,12 +1118,18 @@ var TaskEngine = class {
|
|
|
1097
1118
|
const result = {
|
|
1098
1119
|
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
1099
1120
|
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
1121
|
+
// Keep the verbatim output (bounded) for report extraction and audit.
|
|
1122
|
+
...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
|
|
1100
1123
|
...output.error !== void 0 ? { error: output.error } : {},
|
|
1101
1124
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1102
1125
|
};
|
|
1103
1126
|
const finalStatus = output.status === "completed" ? "completed" : "failed";
|
|
1127
|
+
const finished = await this.transition(running, finalStatus, { result });
|
|
1104
1128
|
this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
|
|
1105
|
-
|
|
1129
|
+
if (this.options.memory) {
|
|
1130
|
+
await this.options.memory.recordTaskOutcome(finished);
|
|
1131
|
+
}
|
|
1132
|
+
return finished;
|
|
1106
1133
|
}
|
|
1107
1134
|
async cancel(id) {
|
|
1108
1135
|
const task = await this.mustGet(id);
|
|
@@ -1147,12 +1174,306 @@ function generateTaskId() {
|
|
|
1147
1174
|
return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
1148
1175
|
}
|
|
1149
1176
|
|
|
1177
|
+
// src/memory/local.ts
|
|
1178
|
+
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
1179
|
+
import { dirname as dirname2 } from "path";
|
|
1180
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
1181
|
+
"the",
|
|
1182
|
+
"and",
|
|
1183
|
+
"for",
|
|
1184
|
+
"with",
|
|
1185
|
+
"this",
|
|
1186
|
+
"that",
|
|
1187
|
+
"from",
|
|
1188
|
+
"into",
|
|
1189
|
+
"was",
|
|
1190
|
+
"were",
|
|
1191
|
+
"have",
|
|
1192
|
+
"has",
|
|
1193
|
+
"had",
|
|
1194
|
+
"are",
|
|
1195
|
+
"but",
|
|
1196
|
+
"not",
|
|
1197
|
+
"you",
|
|
1198
|
+
"his",
|
|
1199
|
+
"her",
|
|
1200
|
+
"its",
|
|
1201
|
+
"their",
|
|
1202
|
+
"them",
|
|
1203
|
+
"then",
|
|
1204
|
+
"than",
|
|
1205
|
+
"when",
|
|
1206
|
+
"what",
|
|
1207
|
+
"which",
|
|
1208
|
+
"will",
|
|
1209
|
+
"would",
|
|
1210
|
+
"been",
|
|
1211
|
+
"were",
|
|
1212
|
+
"also",
|
|
1213
|
+
"because",
|
|
1214
|
+
"about",
|
|
1215
|
+
"after",
|
|
1216
|
+
"before"
|
|
1217
|
+
]);
|
|
1218
|
+
function keywords(text) {
|
|
1219
|
+
return new Set(
|
|
1220
|
+
(text ?? "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 4 && !STOPWORDS.has(t))
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
function relevanceScore(queryKeywords, entry) {
|
|
1224
|
+
if (queryKeywords.size === 0) return 0;
|
|
1225
|
+
const entryKeywords = keywords(`${entry.content} ${entry.key}`);
|
|
1226
|
+
let overlap = 0;
|
|
1227
|
+
for (const keyword of queryKeywords) {
|
|
1228
|
+
if (entryKeywords.has(keyword)) overlap += 1;
|
|
1229
|
+
}
|
|
1230
|
+
let tagMatches = 0;
|
|
1231
|
+
for (const tag of entry.tags ?? []) {
|
|
1232
|
+
if (queryKeywords.has(tag.toLowerCase())) tagMatches += 1;
|
|
1233
|
+
}
|
|
1234
|
+
return overlap * 2 + tagMatches * 1.5;
|
|
1235
|
+
}
|
|
1236
|
+
var LocalMemoryProvider = class {
|
|
1237
|
+
name = "local";
|
|
1238
|
+
filePath;
|
|
1239
|
+
queue = Promise.resolve();
|
|
1240
|
+
constructor(directory = `${process.cwd()}/.omninode`) {
|
|
1241
|
+
this.filePath = `${directory}/memory.json`;
|
|
1242
|
+
}
|
|
1243
|
+
async query(query) {
|
|
1244
|
+
return this.synchronized(async () => {
|
|
1245
|
+
const entries = await this.load();
|
|
1246
|
+
const filtered = entries.filter((entry) => query.scope ? entry.scope === query.scope : true).filter(
|
|
1247
|
+
(entry) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry.tags ?? []).includes(tag)) : true
|
|
1248
|
+
);
|
|
1249
|
+
if (!query.text || query.text.trim().length === 0) {
|
|
1250
|
+
return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
|
|
1251
|
+
}
|
|
1252
|
+
const queryKeywords = keywords(query.text);
|
|
1253
|
+
return filtered.map((entry) => ({ entry, score: relevanceScore(queryKeywords, entry) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry }) => entry);
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
async write(entry) {
|
|
1257
|
+
return this.synchronized(async () => {
|
|
1258
|
+
const entries = await this.load();
|
|
1259
|
+
const index = entries.findIndex((e) => e.key === entry.key);
|
|
1260
|
+
const stamped = {
|
|
1261
|
+
...entry,
|
|
1262
|
+
createdAt: entry.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1263
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1264
|
+
};
|
|
1265
|
+
if (index >= 0) entries[index] = stamped;
|
|
1266
|
+
else entries.push(stamped);
|
|
1267
|
+
await this.saveAll(entries);
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
async count() {
|
|
1271
|
+
return (await this.load()).length;
|
|
1272
|
+
}
|
|
1273
|
+
synchronized(operation) {
|
|
1274
|
+
const result = this.queue.then(operation, operation);
|
|
1275
|
+
this.queue = result.then(
|
|
1276
|
+
() => void 0,
|
|
1277
|
+
() => void 0
|
|
1278
|
+
);
|
|
1279
|
+
return result;
|
|
1280
|
+
}
|
|
1281
|
+
async load() {
|
|
1282
|
+
try {
|
|
1283
|
+
const raw = await readFile2(this.filePath, "utf8");
|
|
1284
|
+
const parsed = JSON.parse(raw);
|
|
1285
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1286
|
+
} catch {
|
|
1287
|
+
return [];
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
async saveAll(entries) {
|
|
1291
|
+
const tmp = `${this.filePath}.tmp`;
|
|
1292
|
+
await mkdir2(dirname2(this.filePath), { recursive: true });
|
|
1293
|
+
await writeFile2(tmp, `${JSON.stringify(entries, null, 2)}
|
|
1294
|
+
`, "utf8");
|
|
1295
|
+
await rename2(tmp, this.filePath);
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
// src/memory/remembera.ts
|
|
1300
|
+
var RememberaMemoryProvider = class {
|
|
1301
|
+
name = "remembera";
|
|
1302
|
+
baseUrl;
|
|
1303
|
+
apiKeyEnvVar;
|
|
1304
|
+
env;
|
|
1305
|
+
log;
|
|
1306
|
+
constructor(config, log = logger) {
|
|
1307
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
1308
|
+
this.apiKeyEnvVar = config.apiKeyEnvVar;
|
|
1309
|
+
this.env = config.env ?? process.env;
|
|
1310
|
+
this.log = log.child({ component: "remembera" });
|
|
1311
|
+
}
|
|
1312
|
+
headers() {
|
|
1313
|
+
const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
|
|
1314
|
+
return key ? { authorization: `Bearer ${key}` } : {};
|
|
1315
|
+
}
|
|
1316
|
+
async query(query) {
|
|
1317
|
+
const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
|
|
1318
|
+
if (status !== 200) {
|
|
1319
|
+
throw new MemoryError(`Remembera query returned HTTP ${status}.`);
|
|
1320
|
+
}
|
|
1321
|
+
const entries = body.entries;
|
|
1322
|
+
if (!Array.isArray(entries)) {
|
|
1323
|
+
throw new MemoryError("Remembera query response had no entries array.");
|
|
1324
|
+
}
|
|
1325
|
+
return entries.map((entry) => this.toEntry(entry)).filter((entry) => entry !== void 0);
|
|
1326
|
+
}
|
|
1327
|
+
async write(entry) {
|
|
1328
|
+
const { status } = await this.request(`${this.baseUrl}/api/memory`, entry);
|
|
1329
|
+
if (status !== 200 && status !== 201) {
|
|
1330
|
+
throw new MemoryError(`Remembera write returned HTTP ${status}.`);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
async request(url, body) {
|
|
1334
|
+
const headers = this.headers();
|
|
1335
|
+
try {
|
|
1336
|
+
return await requestJson(url, {
|
|
1337
|
+
method: "POST",
|
|
1338
|
+
headers,
|
|
1339
|
+
body
|
|
1340
|
+
});
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1343
|
+
throw new MemoryError(`Remembera request to ${url} failed: ${message}`, { cause: error });
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
toEntry(raw) {
|
|
1347
|
+
if (raw === null || typeof raw !== "object") {
|
|
1348
|
+
this.log.warn("Skipping malformed Remembera memory entry.");
|
|
1349
|
+
return void 0;
|
|
1350
|
+
}
|
|
1351
|
+
const entry = raw;
|
|
1352
|
+
if (typeof entry.key !== "string" || typeof entry.content !== "string") {
|
|
1353
|
+
this.log.warn("Skipping Remembera memory entry without key/content.");
|
|
1354
|
+
return void 0;
|
|
1355
|
+
}
|
|
1356
|
+
const scope = entry.scope;
|
|
1357
|
+
return {
|
|
1358
|
+
key: entry.key,
|
|
1359
|
+
content: entry.content,
|
|
1360
|
+
scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
|
|
1361
|
+
...Array.isArray(entry.tags) ? { tags: entry.tags.filter((t) => typeof t === "string") } : {},
|
|
1362
|
+
...typeof entry.createdAt === "string" ? { createdAt: entry.createdAt } : {},
|
|
1363
|
+
...typeof entry.updatedAt === "string" ? { updatedAt: entry.updatedAt } : {}
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
|
|
1368
|
+
// src/memory/factory.ts
|
|
1369
|
+
function createMemoryProvider(config) {
|
|
1370
|
+
if (!config || config.provider === "local") {
|
|
1371
|
+
return new LocalMemoryProvider();
|
|
1372
|
+
}
|
|
1373
|
+
if (config.provider === "remembera") {
|
|
1374
|
+
if (!config.baseUrl) {
|
|
1375
|
+
throw new ConfigError(
|
|
1376
|
+
"CONFIG_INVALID",
|
|
1377
|
+
`Memory provider "remembera" needs a base_url in omninode.yaml.`
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
return new RememberaMemoryProvider({
|
|
1381
|
+
baseUrl: config.baseUrl,
|
|
1382
|
+
apiKeyEnvVar: config.apiKeyEnvVar
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
throw new ConfigError(
|
|
1386
|
+
"CONFIG_INVALID",
|
|
1387
|
+
`Unknown memory provider "${config.provider}". Available: local, remembera.`
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// src/memory/service.ts
|
|
1392
|
+
var MAX_CONTEXT_CHARS = 4e3;
|
|
1393
|
+
var MemoryService = class {
|
|
1394
|
+
constructor(provider, log = logger) {
|
|
1395
|
+
this.provider = provider;
|
|
1396
|
+
this.log = log.child({ component: "memory", provider: provider.name });
|
|
1397
|
+
}
|
|
1398
|
+
provider;
|
|
1399
|
+
log;
|
|
1400
|
+
get providerRef() {
|
|
1401
|
+
return this.provider;
|
|
1402
|
+
}
|
|
1403
|
+
get providerName() {
|
|
1404
|
+
return this.provider.name;
|
|
1405
|
+
}
|
|
1406
|
+
/** Relevant context for a task objective; empty string when nothing matches. */
|
|
1407
|
+
async gatherContext(query) {
|
|
1408
|
+
try {
|
|
1409
|
+
const entries = await this.provider.query({
|
|
1410
|
+
text: query.objective,
|
|
1411
|
+
...query.role ? { tags: [query.role] } : {},
|
|
1412
|
+
limit: 8
|
|
1413
|
+
});
|
|
1414
|
+
if (entries.length === 0) return "";
|
|
1415
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
1416
|
+
for (const entry of entries) {
|
|
1417
|
+
byScope.set(entry.scope, [...byScope.get(entry.scope) ?? [], entry]);
|
|
1418
|
+
}
|
|
1419
|
+
const parts = [];
|
|
1420
|
+
for (const scope of ["project", "task", "role", "knowledge"]) {
|
|
1421
|
+
const scopeEntries = byScope.get(scope) ?? [];
|
|
1422
|
+
if (scopeEntries.length === 0) continue;
|
|
1423
|
+
const lines = scopeEntries.map((entry) => `- ${entry.content}`).join("\n");
|
|
1424
|
+
parts.push(`### Relevant memory (${scope})
|
|
1425
|
+
${lines}`);
|
|
1426
|
+
}
|
|
1427
|
+
const context = parts.join("\n\n");
|
|
1428
|
+
return context.length > MAX_CONTEXT_CHARS ? `${context.slice(0, MAX_CONTEXT_CHARS)}\u2026` : context;
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
this.log.warn(
|
|
1431
|
+
`Memory query failed (continuing without memory): ${error instanceof Error ? error.message : String(error)}`
|
|
1432
|
+
);
|
|
1433
|
+
return "";
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
/** Records a finished task's outcome — best-effort. */
|
|
1437
|
+
async recordTaskOutcome(task) {
|
|
1438
|
+
const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
|
|
1439
|
+
try {
|
|
1440
|
+
await this.provider.write({
|
|
1441
|
+
key: `task:${task.id}`,
|
|
1442
|
+
scope: "task",
|
|
1443
|
+
content: `Objective: ${task.objective}
|
|
1444
|
+
Status: ${task.status}
|
|
1445
|
+
Outcome: ${outcome}`,
|
|
1446
|
+
tags: ["task", ...task.agent ? [task.agent] : [], ...task.role ? [task.role] : []],
|
|
1447
|
+
createdAt: task.createdAt
|
|
1448
|
+
});
|
|
1449
|
+
const significant = (task.result?.reports ?? []).flatMap(
|
|
1450
|
+
(report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
|
|
1451
|
+
);
|
|
1452
|
+
for (const { report, finding } of significant) {
|
|
1453
|
+
await this.provider.write({
|
|
1454
|
+
key: `finding:${report.id}:${finding.id}`,
|
|
1455
|
+
scope: "project",
|
|
1456
|
+
content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
|
|
1457
|
+
tags: ["known-problem", report.agent]
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
} catch (error) {
|
|
1461
|
+
this.log.warn(
|
|
1462
|
+
`Memory write failed (continuing): ${error instanceof Error ? error.message : String(error)}`
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1150
1468
|
// src/tasks/engine-factory.ts
|
|
1151
1469
|
function createTaskEngine(config, log = logger, directory) {
|
|
1470
|
+
const agents = buildAgentRegistry(config, log);
|
|
1471
|
+
const roles = buildRoleRegistry(config);
|
|
1152
1472
|
return new TaskEngine({
|
|
1153
|
-
agents
|
|
1154
|
-
roles
|
|
1473
|
+
agents,
|
|
1474
|
+
roles,
|
|
1155
1475
|
store: new FileTaskStore(directory),
|
|
1476
|
+
...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
|
|
1156
1477
|
log
|
|
1157
1478
|
});
|
|
1158
1479
|
}
|
|
@@ -1328,11 +1649,28 @@ ${contribution}`);
|
|
|
1328
1649
|
}
|
|
1329
1650
|
const failed = [...outcomes.values()].some((o) => o.status === "failed");
|
|
1330
1651
|
const finalStatus = failed ? "failed" : "completed";
|
|
1652
|
+
let combinedReportId;
|
|
1653
|
+
if (this.options.reports) {
|
|
1654
|
+
const taskIds = [...outcomes.values()].flatMap((o) => o.taskIds);
|
|
1655
|
+
const tasks = (await Promise.all(taskIds.map((id) => this.options.tasks.get(id)))).filter((task) => task !== void 0);
|
|
1656
|
+
const collected = await this.options.reports.collectFromTasks(tasks);
|
|
1657
|
+
await this.options.reports.saveReports(collected);
|
|
1658
|
+
const combined = await this.options.reports.generateCombined(collected, {
|
|
1659
|
+
objective,
|
|
1660
|
+
pipelineRunId: run.id,
|
|
1661
|
+
taskIds
|
|
1662
|
+
});
|
|
1663
|
+
combinedReportId = combined?.id;
|
|
1664
|
+
if (collected.length > 0) {
|
|
1665
|
+
run.reports = collected;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1331
1668
|
const finished = {
|
|
1332
1669
|
...run,
|
|
1333
1670
|
status: finalStatus,
|
|
1334
1671
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1335
|
-
...
|
|
1672
|
+
...run.reports !== void 0 && run.reports.length > 0 ? { reports: run.reports } : {},
|
|
1673
|
+
...combinedReportId !== void 0 ? { combinedReportId } : {}
|
|
1336
1674
|
};
|
|
1337
1675
|
await this.options.store.save(finished);
|
|
1338
1676
|
this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
|
|
@@ -1512,8 +1850,8 @@ function createDefaultChatFn(providers) {
|
|
|
1512
1850
|
}
|
|
1513
1851
|
|
|
1514
1852
|
// src/pipelines/store.ts
|
|
1515
|
-
import { mkdir as
|
|
1516
|
-
import { dirname as
|
|
1853
|
+
import { mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile3 } from "fs/promises";
|
|
1854
|
+
import { dirname as dirname3 } from "path";
|
|
1517
1855
|
var FilePipelineRunStore = class {
|
|
1518
1856
|
filePath;
|
|
1519
1857
|
queue = Promise.resolve();
|
|
@@ -1551,7 +1889,7 @@ var FilePipelineRunStore = class {
|
|
|
1551
1889
|
}
|
|
1552
1890
|
async load() {
|
|
1553
1891
|
try {
|
|
1554
|
-
const raw = await
|
|
1892
|
+
const raw = await readFile3(this.filePath, "utf8");
|
|
1555
1893
|
const parsed = JSON.parse(raw);
|
|
1556
1894
|
return Array.isArray(parsed) ? parsed : [];
|
|
1557
1895
|
} catch {
|
|
@@ -1560,10 +1898,302 @@ var FilePipelineRunStore = class {
|
|
|
1560
1898
|
}
|
|
1561
1899
|
async write(runs) {
|
|
1562
1900
|
const tmp = `${this.filePath}.tmp`;
|
|
1563
|
-
await
|
|
1564
|
-
await
|
|
1901
|
+
await mkdir3(dirname3(this.filePath), { recursive: true });
|
|
1902
|
+
await writeFile3(tmp, `${JSON.stringify(runs, null, 2)}
|
|
1565
1903
|
`, "utf8");
|
|
1566
|
-
await
|
|
1904
|
+
await rename3(tmp, this.filePath);
|
|
1905
|
+
}
|
|
1906
|
+
};
|
|
1907
|
+
|
|
1908
|
+
// src/reports/aggregate.ts
|
|
1909
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
1910
|
+
var SEVERITY_RANK = {
|
|
1911
|
+
info: 0,
|
|
1912
|
+
low: 1,
|
|
1913
|
+
medium: 2,
|
|
1914
|
+
high: 3,
|
|
1915
|
+
critical: 4
|
|
1916
|
+
};
|
|
1917
|
+
function aggregateReports(reports, options = {}) {
|
|
1918
|
+
const threshold = options.similarityThreshold ?? 0.7;
|
|
1919
|
+
const findingGroups = [];
|
|
1920
|
+
const recommendationGroups = [];
|
|
1921
|
+
for (const report of reports) {
|
|
1922
|
+
for (const finding of report.findings) {
|
|
1923
|
+
mergeFinding(findingGroups, finding, report, threshold);
|
|
1924
|
+
}
|
|
1925
|
+
for (const text of report.recommendations) {
|
|
1926
|
+
mergeRecommendation(recommendationGroups, text, report, threshold);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
const sources = [...new Set(reports.map((r) => r.agent))].sort();
|
|
1930
|
+
const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
|
|
1931
|
+
const summary = `${reports.length} report(s) from ${sources.length > 0 ? sources.join(", ") : "no agents"}: ${findingGroups.length} unique finding group(s) (${totalRawFindings} raw), ${recommendationGroups.length} recommendation group(s).`;
|
|
1932
|
+
return {
|
|
1933
|
+
id: `combined-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
|
|
1934
|
+
title: options.objective !== void 0 ? truncate(options.objective, 120) : "Combined report",
|
|
1935
|
+
...options.objective !== void 0 ? { objective: options.objective } : {},
|
|
1936
|
+
...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
|
|
1937
|
+
taskIds: options.taskIds ?? [],
|
|
1938
|
+
summary,
|
|
1939
|
+
findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
|
|
1940
|
+
recommendations: recommendationGroups,
|
|
1941
|
+
sources,
|
|
1942
|
+
reportIds: reports.map((r) => r.id),
|
|
1943
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
function rank(finding) {
|
|
1947
|
+
return finding.severity ? SEVERITY_RANK[finding.severity] : -1;
|
|
1948
|
+
}
|
|
1949
|
+
function mergeFinding(groups, finding, report, threshold) {
|
|
1950
|
+
const source = { agent: report.agent, reportId: report.id };
|
|
1951
|
+
const group = groups.find((g) => similar(g.title, finding.title, threshold));
|
|
1952
|
+
if (group) {
|
|
1953
|
+
group.occurrences += 1;
|
|
1954
|
+
group.details.push(finding.detail);
|
|
1955
|
+
group.sources.push(source);
|
|
1956
|
+
group.evidence.push(...finding.evidence ?? []);
|
|
1957
|
+
if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
|
|
1958
|
+
group.severity = finding.severity;
|
|
1959
|
+
}
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
groups.push({
|
|
1963
|
+
title: finding.title,
|
|
1964
|
+
details: [finding.detail],
|
|
1965
|
+
sources: [source],
|
|
1966
|
+
occurrences: 1,
|
|
1967
|
+
...finding.severity ? { severity: finding.severity } : {},
|
|
1968
|
+
evidence: [...finding.evidence ?? []]
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
function mergeRecommendation(groups, text, report, threshold) {
|
|
1972
|
+
const source = { agent: report.agent, reportId: report.id };
|
|
1973
|
+
const group = groups.find((g) => similar(g.text, text, threshold));
|
|
1974
|
+
if (group) {
|
|
1975
|
+
group.occurrences += 1;
|
|
1976
|
+
group.sources.push(source);
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
groups.push({ text, sources: [source], occurrences: 1 });
|
|
1980
|
+
}
|
|
1981
|
+
function similar(a, b, threshold) {
|
|
1982
|
+
const left = tokens(a);
|
|
1983
|
+
const right = tokens(b);
|
|
1984
|
+
if (left.size === 0 || right.size === 0) return false;
|
|
1985
|
+
let intersection = 0;
|
|
1986
|
+
for (const token of left) {
|
|
1987
|
+
if (right.has(token)) intersection += 1;
|
|
1988
|
+
}
|
|
1989
|
+
const union = left.size + right.size - intersection;
|
|
1990
|
+
return intersection / union >= threshold;
|
|
1991
|
+
}
|
|
1992
|
+
function tokens(text) {
|
|
1993
|
+
return new Set(
|
|
1994
|
+
text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
function truncate(text, max) {
|
|
1998
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// src/reports/extract.ts
|
|
2002
|
+
var BULLET = /^(?:[-*•]|\d+[.)])\s+(.*)$/;
|
|
2003
|
+
function parseSections(raw) {
|
|
2004
|
+
const sections = [];
|
|
2005
|
+
let current = { heading: "", bullets: [] };
|
|
2006
|
+
for (const line of raw.split("\n")) {
|
|
2007
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line.trim());
|
|
2008
|
+
if (heading) {
|
|
2009
|
+
if (current.heading.length > 0 || current.bullets.length > 0) {
|
|
2010
|
+
sections.push(current);
|
|
2011
|
+
}
|
|
2012
|
+
current = { heading: heading[2]?.toLowerCase() ?? "", bullets: [] };
|
|
2013
|
+
continue;
|
|
2014
|
+
}
|
|
2015
|
+
const bullet = BULLET.exec(line.trim());
|
|
2016
|
+
if (bullet && current.heading.length > 0) {
|
|
2017
|
+
current.bullets.push(bullet[1]?.trim() ?? "");
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
if (current.heading.length > 0 || current.bullets.length > 0) {
|
|
2021
|
+
sections.push(current);
|
|
2022
|
+
}
|
|
2023
|
+
return sections;
|
|
2024
|
+
}
|
|
2025
|
+
function collectBullets(sections, patterns) {
|
|
2026
|
+
const matches = sections.filter((section) => patterns.some((p) => p.test(section.heading)));
|
|
2027
|
+
return matches.flatMap((section) => section.bullets).filter((b) => b.length > 0);
|
|
2028
|
+
}
|
|
2029
|
+
function guessSeverity(text) {
|
|
2030
|
+
if (/critical|severe/i.test(text)) return "critical";
|
|
2031
|
+
if (/\bhigh\b|major/i.test(text)) return "high";
|
|
2032
|
+
if (/medium|moderate/i.test(text)) return "medium";
|
|
2033
|
+
if (/\blow\b|minor/i.test(text)) return "low";
|
|
2034
|
+
return void 0;
|
|
2035
|
+
}
|
|
2036
|
+
function extractReportFromText(raw, meta) {
|
|
2037
|
+
const trimmed = raw.trim();
|
|
2038
|
+
const nonEmpty = trimmed.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
2039
|
+
const proseLine = nonEmpty.find((line) => !line.startsWith("#") && !BULLET.test(line)) ?? nonEmpty[0] ?? "";
|
|
2040
|
+
const cleaned = proseLine.replace(/^#{1,6}\s+/, "").replace(BULLET, "$1");
|
|
2041
|
+
const summary = cleaned.length > 0 ? truncate2(cleaned, 200) : "(no output)";
|
|
2042
|
+
const sections = parseSections(trimmed);
|
|
2043
|
+
const findingTexts = collectBullets(sections, [/finding/, /issue/, /problem/, /vulnerab/]);
|
|
2044
|
+
const recommendationTexts = collectBullets(sections, [/recommend/, /suggestion/, /next step/, /action/]);
|
|
2045
|
+
const evidenceTexts = collectBullets(sections, [/evidence/, /proof/, /log/]);
|
|
2046
|
+
const findings = findingTexts.map((text, index) => ({
|
|
2047
|
+
id: `${meta.taskId}-f-${index + 1}`,
|
|
2048
|
+
title: truncate2(text, 120),
|
|
2049
|
+
detail: text,
|
|
2050
|
+
severity: guessSeverity(text)
|
|
2051
|
+
}));
|
|
2052
|
+
return {
|
|
2053
|
+
id: `${meta.taskId}-report`,
|
|
2054
|
+
taskId: meta.taskId,
|
|
2055
|
+
agent: meta.agent,
|
|
2056
|
+
...meta.model !== void 0 ? { model: meta.model } : {},
|
|
2057
|
+
summary,
|
|
2058
|
+
findings,
|
|
2059
|
+
recommendations: recommendationTexts.map((t) => truncate2(t, 200)),
|
|
2060
|
+
metadata: evidenceTexts.length > 0 ? { evidence: evidenceTexts } : void 0,
|
|
2061
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
function truncate2(text, max) {
|
|
2065
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// src/reports/service.ts
|
|
2069
|
+
var ReportService = class {
|
|
2070
|
+
constructor(store, log = logger) {
|
|
2071
|
+
this.store = store;
|
|
2072
|
+
this.log = log.child({ component: "reports" });
|
|
2073
|
+
}
|
|
2074
|
+
store;
|
|
2075
|
+
log;
|
|
2076
|
+
get storeRef() {
|
|
2077
|
+
return this.store;
|
|
2078
|
+
}
|
|
2079
|
+
/** Normalizes finished tasks into reports (§17 — normalizer). */
|
|
2080
|
+
async collectFromTasks(tasks) {
|
|
2081
|
+
const reports = [];
|
|
2082
|
+
for (const task of tasks) {
|
|
2083
|
+
const structured = task.result?.reports ?? [];
|
|
2084
|
+
if (structured.length > 0) {
|
|
2085
|
+
reports.push(...structured);
|
|
2086
|
+
continue;
|
|
2087
|
+
}
|
|
2088
|
+
const text = task.result?.rawOutput ?? task.result?.summary ?? "";
|
|
2089
|
+
if (text.trim().length === 0) continue;
|
|
2090
|
+
reports.push(
|
|
2091
|
+
extractReportFromText(text, {
|
|
2092
|
+
taskId: task.id,
|
|
2093
|
+
agent: task.agent ?? "unknown"
|
|
2094
|
+
})
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2097
|
+
return reports;
|
|
2098
|
+
}
|
|
2099
|
+
async saveReports(reports) {
|
|
2100
|
+
if (reports.length === 0) return;
|
|
2101
|
+
await this.store.saveReports(reports);
|
|
2102
|
+
this.log.info(`Stored ${reports.length} report(s).`);
|
|
2103
|
+
}
|
|
2104
|
+
async generateCombined(reports, meta = {}) {
|
|
2105
|
+
if (reports.length === 0) return void 0;
|
|
2106
|
+
const options = {
|
|
2107
|
+
...meta.objective !== void 0 ? { objective: meta.objective } : {},
|
|
2108
|
+
...meta.pipelineRunId !== void 0 ? { pipelineRunId: meta.pipelineRunId } : {},
|
|
2109
|
+
...meta.taskIds !== void 0 ? { taskIds: meta.taskIds } : {}
|
|
2110
|
+
};
|
|
2111
|
+
const combined = aggregateReports(reports, options);
|
|
2112
|
+
await this.store.saveCombined(combined);
|
|
2113
|
+
this.log.info(`Generated combined report ${combined.id}.`);
|
|
2114
|
+
return combined;
|
|
2115
|
+
}
|
|
2116
|
+
};
|
|
2117
|
+
|
|
2118
|
+
// src/reports/store.ts
|
|
2119
|
+
import { mkdir as mkdir4, readFile as readFile4, rename as rename4, writeFile as writeFile4 } from "fs/promises";
|
|
2120
|
+
import { dirname as dirname4 } from "path";
|
|
2121
|
+
var FileReportStore = class {
|
|
2122
|
+
filePath;
|
|
2123
|
+
queue = Promise.resolve();
|
|
2124
|
+
constructor(directory = `${process.cwd()}/.omninode`) {
|
|
2125
|
+
this.filePath = `${directory}/reports.json`;
|
|
2126
|
+
}
|
|
2127
|
+
async saveReports(reports) {
|
|
2128
|
+
return this.synchronized(async () => {
|
|
2129
|
+
const entries = await this.load();
|
|
2130
|
+
for (const report of reports) {
|
|
2131
|
+
const index = entries.findIndex(
|
|
2132
|
+
(e) => e.kind === "report" && e.report.id === report.id
|
|
2133
|
+
);
|
|
2134
|
+
const entry = { kind: "report", report };
|
|
2135
|
+
if (index >= 0) entries[index] = entry;
|
|
2136
|
+
else entries.push(entry);
|
|
2137
|
+
}
|
|
2138
|
+
await this.write(entries);
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
async listReports(filter = {}) {
|
|
2142
|
+
return this.synchronized(async () => {
|
|
2143
|
+
const entries = await this.load();
|
|
2144
|
+
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));
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
async saveCombined(combined) {
|
|
2148
|
+
return this.synchronized(async () => {
|
|
2149
|
+
const entries = await this.load();
|
|
2150
|
+
const index = entries.findIndex(
|
|
2151
|
+
(e) => e.kind === "combined" && e.combined.id === combined.id
|
|
2152
|
+
);
|
|
2153
|
+
const entry = { kind: "combined", combined };
|
|
2154
|
+
if (index >= 0) entries[index] = entry;
|
|
2155
|
+
else entries.push(entry);
|
|
2156
|
+
await this.write(entries);
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
async getCombined(id) {
|
|
2160
|
+
return this.synchronized(async () => {
|
|
2161
|
+
const entries = await this.load();
|
|
2162
|
+
const found = entries.find((e) => e.kind === "combined" && e.combined.id === id);
|
|
2163
|
+
return found?.kind === "combined" ? found.combined : void 0;
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
async listCombined(filter = {}) {
|
|
2167
|
+
return this.synchronized(async () => {
|
|
2168
|
+
const entries = await this.load();
|
|
2169
|
+
return entries.filter((e) => e.kind === "combined").map((e) => e.combined).filter(
|
|
2170
|
+
(combined) => filter.pipelineRunId ? combined.pipelineRunId === filter.pipelineRunId : true
|
|
2171
|
+
).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
synchronized(operation) {
|
|
2175
|
+
const result = this.queue.then(operation, operation);
|
|
2176
|
+
this.queue = result.then(
|
|
2177
|
+
() => void 0,
|
|
2178
|
+
() => void 0
|
|
2179
|
+
);
|
|
2180
|
+
return result;
|
|
2181
|
+
}
|
|
2182
|
+
async load() {
|
|
2183
|
+
try {
|
|
2184
|
+
const raw = await readFile4(this.filePath, "utf8");
|
|
2185
|
+
const parsed = JSON.parse(raw);
|
|
2186
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
2187
|
+
} catch {
|
|
2188
|
+
return [];
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
async write(entries) {
|
|
2192
|
+
const tmp = `${this.filePath}.tmp`;
|
|
2193
|
+
await mkdir4(dirname4(this.filePath), { recursive: true });
|
|
2194
|
+
await writeFile4(tmp, `${JSON.stringify(entries, null, 2)}
|
|
2195
|
+
`, "utf8");
|
|
2196
|
+
await rename4(tmp, this.filePath);
|
|
1567
2197
|
}
|
|
1568
2198
|
};
|
|
1569
2199
|
|
|
@@ -1575,6 +2205,7 @@ function buildPipelineEngine(config, log = logger) {
|
|
|
1575
2205
|
agents,
|
|
1576
2206
|
roles,
|
|
1577
2207
|
store: new FileTaskStore(),
|
|
2208
|
+
...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
|
|
1578
2209
|
log
|
|
1579
2210
|
});
|
|
1580
2211
|
return new PipelineEngine({
|
|
@@ -1584,21 +2215,25 @@ function buildPipelineEngine(config, log = logger) {
|
|
|
1584
2215
|
providers: config.project.providers,
|
|
1585
2216
|
store: new FilePipelineRunStore(),
|
|
1586
2217
|
chat: createDefaultChatFn(config.project.providers),
|
|
2218
|
+
reports: new ReportService(new FileReportStore(), log),
|
|
1587
2219
|
log
|
|
1588
2220
|
});
|
|
1589
2221
|
}
|
|
1590
2222
|
|
|
1591
2223
|
// src/version.ts
|
|
1592
|
-
var OMNINODE_VERSION = "0.
|
|
2224
|
+
var OMNINODE_VERSION = "0.8.0";
|
|
1593
2225
|
export {
|
|
1594
2226
|
AgentError,
|
|
1595
2227
|
AgentRegistry,
|
|
1596
2228
|
ConfigError,
|
|
1597
2229
|
ConsoleLogSink,
|
|
1598
2230
|
FilePipelineRunStore,
|
|
2231
|
+
FileReportStore,
|
|
1599
2232
|
FileTaskStore,
|
|
2233
|
+
LocalMemoryProvider,
|
|
1600
2234
|
Logger,
|
|
1601
2235
|
MemoryError,
|
|
2236
|
+
MemoryService,
|
|
1602
2237
|
ModelRegistry,
|
|
1603
2238
|
OMNINODE_VERSION,
|
|
1604
2239
|
OPENAI_COMPATIBLE_TYPES,
|
|
@@ -1610,10 +2245,13 @@ export {
|
|
|
1610
2245
|
ProcessAgent,
|
|
1611
2246
|
ProtocolError,
|
|
1612
2247
|
ProviderError,
|
|
2248
|
+
RememberaMemoryProvider,
|
|
2249
|
+
ReportService,
|
|
1613
2250
|
RoleRegistry,
|
|
1614
2251
|
TaskEngine,
|
|
1615
2252
|
TaskError,
|
|
1616
2253
|
agentConfigSchema,
|
|
2254
|
+
aggregateReports,
|
|
1617
2255
|
appConfigSchema,
|
|
1618
2256
|
authHeaders,
|
|
1619
2257
|
buildAgentRegistry,
|
|
@@ -1622,12 +2260,15 @@ export {
|
|
|
1622
2260
|
composeTaskText,
|
|
1623
2261
|
createAgent,
|
|
1624
2262
|
createDefaultChatFn,
|
|
2263
|
+
createMemoryProvider,
|
|
1625
2264
|
createProvider,
|
|
1626
2265
|
createTaskEngine,
|
|
1627
2266
|
defaultProjectConfigYaml,
|
|
1628
2267
|
expandEnvRefs,
|
|
2268
|
+
extractReportFromText,
|
|
1629
2269
|
findConfigFile,
|
|
1630
2270
|
isOmniNodeError,
|
|
2271
|
+
keywords,
|
|
1631
2272
|
loadConfig,
|
|
1632
2273
|
logger,
|
|
1633
2274
|
pipelineConfigSchema,
|
|
@@ -1637,6 +2278,7 @@ export {
|
|
|
1637
2278
|
registryKey,
|
|
1638
2279
|
requestJson,
|
|
1639
2280
|
resolveApiKey,
|
|
1640
|
-
roleConfigSchema
|
|
2281
|
+
roleConfigSchema,
|
|
2282
|
+
similar
|
|
1641
2283
|
};
|
|
1642
2284
|
//# sourceMappingURL=index.js.map
|