@hilbras/omninode 2.0.0-alpha.12 → 2.0.0-alpha.14
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 +16 -1
- package/dist/cli/chunk-6Z6O63SS.js +708 -0
- package/dist/cli/chunk-6Z6O63SS.js.map +1 -0
- package/dist/cli/index.js +1278 -1572
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/providers-OYXBAQTX.js +36 -0
- package/dist/cli/providers-OYXBAQTX.js.map +1 -0
- package/dist/index.d.ts +102 -31
- package/dist/index.js +200 -48
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AgentError,
|
|
4
|
+
ConfigError,
|
|
5
|
+
MemoryError,
|
|
6
|
+
OmniHilbrasProvider,
|
|
7
|
+
OmniNodeError,
|
|
8
|
+
PipelineError,
|
|
9
|
+
PlannerError,
|
|
10
|
+
ProviderError,
|
|
11
|
+
TaskError,
|
|
12
|
+
configureLogger,
|
|
13
|
+
createProvider,
|
|
14
|
+
isOmniNodeError,
|
|
15
|
+
logger,
|
|
16
|
+
requestJson,
|
|
17
|
+
resolveApiKey
|
|
18
|
+
} from "./chunk-6Z6O63SS.js";
|
|
2
19
|
|
|
3
20
|
// src/cli/index.ts
|
|
4
21
|
import { pathToFileURL } from "url";
|
|
@@ -6,60 +23,7 @@ import { realpathSync } from "fs";
|
|
|
6
23
|
import { Command } from "commander";
|
|
7
24
|
|
|
8
25
|
// src/version.ts
|
|
9
|
-
var OMNINODE_VERSION = "2.0.0-alpha.
|
|
10
|
-
|
|
11
|
-
// src/errors/index.ts
|
|
12
|
-
var OmniNodeError = class extends Error {
|
|
13
|
-
code;
|
|
14
|
-
details;
|
|
15
|
-
constructor(code, message, options = {}) {
|
|
16
|
-
super(message, { cause: options.cause });
|
|
17
|
-
this.name = new.target.name;
|
|
18
|
-
this.code = code;
|
|
19
|
-
this.details = options.details;
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
var ConfigError = class extends OmniNodeError {
|
|
23
|
-
constructor(code, message, options = {}) {
|
|
24
|
-
super(code, message, options);
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
var ProviderError = class extends OmniNodeError {
|
|
28
|
-
constructor(code, message, options = {}) {
|
|
29
|
-
super(code, message, options);
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
var AgentError = class extends OmniNodeError {
|
|
33
|
-
constructor(code, message, options = {}) {
|
|
34
|
-
super(code, message, options);
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
var TaskError = class extends OmniNodeError {
|
|
38
|
-
constructor(code, message, options = {}) {
|
|
39
|
-
super(code, message, options);
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
var PipelineError = class extends OmniNodeError {
|
|
43
|
-
constructor(code, message, options = {}) {
|
|
44
|
-
super(code, message, options);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
var PlannerError = class extends OmniNodeError {
|
|
48
|
-
constructor(code, message, options = {}) {
|
|
49
|
-
super(code, message, options);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
var MemoryError = class extends OmniNodeError {
|
|
53
|
-
constructor(message, options = {}) {
|
|
54
|
-
super("MEMORY_UNAVAILABLE", message, options);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
function isOmniNodeError(value) {
|
|
58
|
-
return value instanceof OmniNodeError;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// src/cli/commands/init.ts
|
|
62
|
-
import { existsSync as existsSync2, writeFileSync } from "fs";
|
|
26
|
+
var OMNINODE_VERSION = "2.0.0-alpha.14";
|
|
63
27
|
|
|
64
28
|
// src/config/schema.ts
|
|
65
29
|
import { z } from "zod";
|
|
@@ -142,9 +106,45 @@ var projectConfigSchema = z.object({
|
|
|
142
106
|
}).strict();
|
|
143
107
|
var appConfigSchema = z.object({
|
|
144
108
|
project: projectConfigSchema,
|
|
145
|
-
logging: z.object({
|
|
109
|
+
logging: z.object({
|
|
110
|
+
level: z.enum(["debug", "info", "warn", "error"]),
|
|
111
|
+
format: z.enum(["text", "json"]).optional()
|
|
112
|
+
}).strict().optional()
|
|
146
113
|
}).strict();
|
|
147
114
|
|
|
115
|
+
// src/config/secrets.ts
|
|
116
|
+
var SAFE_KEY_SUFFIXES = ["_env", "_env_var", "_file", "_path", "_ref", "_id", "_url", "_command"];
|
|
117
|
+
var SECRET_KEY = /^\s*([A-Za-z0-9_-]*(?:api[_-]?key|token|password|passwd|secret|credential)[A-Za-z0-9_-]*)\s*:\s*(.*)$/i;
|
|
118
|
+
var SAFE_VALUE = /^\s*(?:\$\{[^}]*\}|\$[A-Z_][A-Z0-9_]*|<[^>]*>|""|''|null|~)\s*$/i;
|
|
119
|
+
function scanForInlineSecrets(source) {
|
|
120
|
+
const findings = [];
|
|
121
|
+
source.split("\n").forEach((line, index) => {
|
|
122
|
+
const trimmed = line.trim();
|
|
123
|
+
if (trimmed.startsWith("#")) return;
|
|
124
|
+
const match = SECRET_KEY.exec(line);
|
|
125
|
+
if (!match) return;
|
|
126
|
+
const key = match[1] ?? "";
|
|
127
|
+
const rawValue = (match[2] ?? "").trim().replace(/,\s*$/, "");
|
|
128
|
+
const keyIsReference = SAFE_KEY_SUFFIXES.some((suffix) => key.toLowerCase().endsWith(suffix));
|
|
129
|
+
if (keyIsReference) return;
|
|
130
|
+
const value = rawValue.replace(/^["']|["']$/g, "");
|
|
131
|
+
if (value.length === 0 || SAFE_VALUE.test(rawValue)) return;
|
|
132
|
+
if (value.length < 8) return;
|
|
133
|
+
findings.push({
|
|
134
|
+
line: index + 1,
|
|
135
|
+
key,
|
|
136
|
+
excerpt: `${key}: ${value.slice(0, 3)}\u2026 (${value.length} chars, redacted)`
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
return findings;
|
|
140
|
+
}
|
|
141
|
+
function describeSecretFindings(file, findings) {
|
|
142
|
+
const lines = findings.map((f) => ` line ${f.line}: ${f.excerpt}`).join("\n");
|
|
143
|
+
return `${file} appears to contain ${findings.length} inline secret(s):
|
|
144
|
+
${lines}
|
|
145
|
+
OmniNode never stores credentials in configuration. Replace each value with an environment-variable reference, e.g. api_key_env_var: MY_API_KEY.`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
148
|
// src/config/loader.ts
|
|
149
149
|
import { existsSync, readFileSync } from "fs";
|
|
150
150
|
import { parse as parseYaml } from "yaml";
|
|
@@ -181,6 +181,10 @@ function loadConfig(options = {}) {
|
|
|
181
181
|
);
|
|
182
182
|
}
|
|
183
183
|
const raw = readFileSync(path, "utf8");
|
|
184
|
+
const secretFindings = scanForInlineSecrets(raw);
|
|
185
|
+
if (secretFindings.length > 0) {
|
|
186
|
+
throw new ConfigError("CONFIG_INVALID", describeSecretFindings(path, secretFindings));
|
|
187
|
+
}
|
|
184
188
|
const expanded = expandEnvRefs(raw, env, path);
|
|
185
189
|
let data;
|
|
186
190
|
try {
|
|
@@ -327,6 +331,7 @@ function defaultProjectConfigYaml(projectName = "My Project") {
|
|
|
327
331
|
}
|
|
328
332
|
|
|
329
333
|
// src/cli/commands/init.ts
|
|
334
|
+
import { existsSync as existsSync2, writeFileSync } from "fs";
|
|
330
335
|
async function runInit(options) {
|
|
331
336
|
const target = `${process.cwd()}/omninode.yaml`;
|
|
332
337
|
if (!options.force && existsSync2(target)) {
|
|
@@ -352,578 +357,6 @@ function registerProjectCommands(program) {
|
|
|
352
357
|
project.command("init").description("Alias of `omninode init`.").option("-f, --force", "Overwrite an existing omninode.yaml.").option("-n, --name <name>", "Project name to write into the configuration.", "My Project").action(async (options) => runInit(options));
|
|
353
358
|
}
|
|
354
359
|
|
|
355
|
-
// src/providers/auth.ts
|
|
356
|
-
function resolveApiKey(config, env = process.env) {
|
|
357
|
-
if (!config.apiKeyEnvVar) return void 0;
|
|
358
|
-
const value = env[config.apiKeyEnvVar];
|
|
359
|
-
if (!value) {
|
|
360
|
-
throw new ProviderError(
|
|
361
|
-
"PROVIDER_AUTH_FAILED",
|
|
362
|
-
`Environment variable "${config.apiKeyEnvVar}" (referenced by provider "${config.name}") is not set.`
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
return value;
|
|
366
|
-
}
|
|
367
|
-
function authHeaders(config, env = process.env) {
|
|
368
|
-
const key = resolveApiKey(config, env);
|
|
369
|
-
const base = { ...config.headers ?? {} };
|
|
370
|
-
return key ? { ...base, authorization: `Bearer ${key}` } : base;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// src/providers/http.ts
|
|
374
|
-
var HttpResponseError = class extends Error {
|
|
375
|
-
constructor(message, status, cause) {
|
|
376
|
-
super(message, { cause });
|
|
377
|
-
this.status = status;
|
|
378
|
-
this.name = "HttpResponseError";
|
|
379
|
-
}
|
|
380
|
-
status;
|
|
381
|
-
};
|
|
382
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
383
|
-
async function requestJson(url, options = {}) {
|
|
384
|
-
const { method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
|
|
385
|
-
const response = await fetch(url, {
|
|
386
|
-
method,
|
|
387
|
-
headers: {
|
|
388
|
-
accept: "application/json",
|
|
389
|
-
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
390
|
-
...headers
|
|
391
|
-
},
|
|
392
|
-
...body !== void 0 ? { body: JSON.stringify(body) } : {},
|
|
393
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
394
|
-
});
|
|
395
|
-
let parsed;
|
|
396
|
-
try {
|
|
397
|
-
parsed = await response.json();
|
|
398
|
-
} catch (error) {
|
|
399
|
-
throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
|
|
400
|
-
}
|
|
401
|
-
return { status: response.status, body: parsed };
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// src/logger/index.ts
|
|
405
|
-
var LEVEL_ORDER = {
|
|
406
|
-
debug: 10,
|
|
407
|
-
info: 20,
|
|
408
|
-
warn: 30,
|
|
409
|
-
error: 40
|
|
410
|
-
};
|
|
411
|
-
var ConsoleLogSink = class {
|
|
412
|
-
constructor(stream = process.stderr) {
|
|
413
|
-
this.stream = stream;
|
|
414
|
-
}
|
|
415
|
-
stream;
|
|
416
|
-
write(_level, line) {
|
|
417
|
-
this.stream.write(line + "\n");
|
|
418
|
-
}
|
|
419
|
-
};
|
|
420
|
-
var Logger = class _Logger {
|
|
421
|
-
level;
|
|
422
|
-
sink;
|
|
423
|
-
context;
|
|
424
|
-
constructor(options = {}) {
|
|
425
|
-
this.level = options.level ?? "info";
|
|
426
|
-
this.sink = options.sink ?? new ConsoleLogSink();
|
|
427
|
-
this.context = options.context ?? {};
|
|
428
|
-
}
|
|
429
|
-
child(context) {
|
|
430
|
-
return new _Logger({
|
|
431
|
-
level: this.level,
|
|
432
|
-
sink: this.sink,
|
|
433
|
-
context: { ...this.context, ...context }
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
debug(message, data) {
|
|
437
|
-
this.log("debug", message, data);
|
|
438
|
-
}
|
|
439
|
-
info(message, data) {
|
|
440
|
-
this.log("info", message, data);
|
|
441
|
-
}
|
|
442
|
-
warn(message, data) {
|
|
443
|
-
this.log("warn", message, data);
|
|
444
|
-
}
|
|
445
|
-
error(message, data) {
|
|
446
|
-
this.log("error", message, data);
|
|
447
|
-
}
|
|
448
|
-
log(level, message, data) {
|
|
449
|
-
if (LEVEL_ORDER[level] < LEVEL_ORDER[this.level]) return;
|
|
450
|
-
const parts = [(/* @__PURE__ */ new Date()).toISOString(), level.toUpperCase(), message];
|
|
451
|
-
const merged = { ...this.context, ...data };
|
|
452
|
-
if (Object.keys(merged).length > 0) {
|
|
453
|
-
parts.push(JSON.stringify(merged));
|
|
454
|
-
}
|
|
455
|
-
this.sink.write(level, parts.join(" "));
|
|
456
|
-
}
|
|
457
|
-
};
|
|
458
|
-
var logger = new Logger();
|
|
459
|
-
|
|
460
|
-
// src/providers/errors.ts
|
|
461
|
-
function kindToCode(kind) {
|
|
462
|
-
switch (kind) {
|
|
463
|
-
case "AUTHENTICATION_ERROR":
|
|
464
|
-
return "PROVIDER_AUTH_FAILED";
|
|
465
|
-
case "MODEL_NOT_FOUND":
|
|
466
|
-
return "MODEL_NOT_FOUND";
|
|
467
|
-
default:
|
|
468
|
-
return "PROVIDER_UNAVAILABLE";
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
function isRetryable(kind) {
|
|
472
|
-
return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
|
|
473
|
-
}
|
|
474
|
-
function kindFromStatus(status) {
|
|
475
|
-
if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
|
|
476
|
-
if (status === 429) return "RATE_LIMIT_ERROR";
|
|
477
|
-
if (status === 404) return "MODEL_NOT_FOUND";
|
|
478
|
-
if (status === 408 || status === 504) return "TIMEOUT";
|
|
479
|
-
if (status >= 500) return "SERVER_ERROR";
|
|
480
|
-
if (status >= 400) return "INVALID_REQUEST";
|
|
481
|
-
return "UNKNOWN_ERROR";
|
|
482
|
-
}
|
|
483
|
-
function extractMessage(body) {
|
|
484
|
-
if (body === null || typeof body !== "object") return void 0;
|
|
485
|
-
const root = body;
|
|
486
|
-
const error = root.error ?? root;
|
|
487
|
-
const message = error.message ?? error.detail ?? root.message;
|
|
488
|
-
return typeof message === "string" ? message : void 0;
|
|
489
|
-
}
|
|
490
|
-
function parseRetryAfter(value, now = Date.now()) {
|
|
491
|
-
if (!value) return void 0;
|
|
492
|
-
const seconds = Number(value);
|
|
493
|
-
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
494
|
-
const date = Date.parse(value);
|
|
495
|
-
return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
|
|
496
|
-
}
|
|
497
|
-
function isTimeoutError(error) {
|
|
498
|
-
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
|
|
499
|
-
}
|
|
500
|
-
function fallbackMessage(kind, context, status) {
|
|
501
|
-
const { provider, operation } = context;
|
|
502
|
-
switch (kind) {
|
|
503
|
-
case "AUTHENTICATION_ERROR":
|
|
504
|
-
return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
|
|
505
|
-
case "RATE_LIMIT_ERROR":
|
|
506
|
-
return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
|
|
507
|
-
case "MODEL_NOT_FOUND":
|
|
508
|
-
return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
|
|
509
|
-
case "TIMEOUT":
|
|
510
|
-
return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
|
|
511
|
-
case "SERVER_ERROR":
|
|
512
|
-
return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
|
|
513
|
-
case "INVALID_REQUEST":
|
|
514
|
-
return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
|
|
515
|
-
default:
|
|
516
|
-
return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
function providerHttpError(context) {
|
|
520
|
-
const status = context.status ?? 0;
|
|
521
|
-
const kind = kindFromStatus(status);
|
|
522
|
-
const detail = extractMessage(context.body);
|
|
523
|
-
const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
|
|
524
|
-
return new ProviderError(
|
|
525
|
-
context.code ?? kindToCode(kind),
|
|
526
|
-
detail ?? fallbackMessage(kind, context, status),
|
|
527
|
-
{
|
|
528
|
-
cause: context.cause,
|
|
529
|
-
details: {
|
|
530
|
-
kind,
|
|
531
|
-
provider: context.provider,
|
|
532
|
-
operation: context.operation,
|
|
533
|
-
status,
|
|
534
|
-
retryable: isRetryable(kind),
|
|
535
|
-
...retryAfterMs !== void 0 ? { retryAfterMs } : {},
|
|
536
|
-
...detail ? { providerMessage: detail } : {}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
|
-
function providerTransportError(context) {
|
|
542
|
-
const cause = context.cause;
|
|
543
|
-
const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
|
|
544
|
-
const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
|
|
545
|
-
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
|
|
546
|
-
cause: context.cause,
|
|
547
|
-
details: {
|
|
548
|
-
kind,
|
|
549
|
-
provider: context.provider,
|
|
550
|
-
operation: context.operation,
|
|
551
|
-
retryable: isRetryable(kind)
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// src/providers/openai-compatible/index.ts
|
|
557
|
-
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
558
|
-
var OpenAICompatibleProvider = class {
|
|
559
|
-
config;
|
|
560
|
-
providerId;
|
|
561
|
-
authentication;
|
|
562
|
-
log;
|
|
563
|
-
/** Discovered models, so getModel() answers without another round-trip. */
|
|
564
|
-
discovered;
|
|
565
|
-
env;
|
|
566
|
-
constructor(config, log = logger, env = process.env) {
|
|
567
|
-
this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
|
|
568
|
-
this.env = env;
|
|
569
|
-
this.providerId = config.providerId ?? config.name;
|
|
570
|
-
this.authentication = {
|
|
571
|
-
method: config.apiKeyEnvVar ? "bearer" : "none",
|
|
572
|
-
...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
|
|
573
|
-
configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
|
|
574
|
-
};
|
|
575
|
-
this.log = log.child({ provider: config.name });
|
|
576
|
-
}
|
|
577
|
-
/** Provider-level request timeout (roadmap §6.6/§11) with the shared default. */
|
|
578
|
-
get requestTimeoutMs() {
|
|
579
|
-
return this.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
580
|
-
}
|
|
581
|
-
/** Authenticate + discover (roadmap §10, Model Discovery). */
|
|
582
|
-
async connect() {
|
|
583
|
-
authHeaders(this.config, this.env);
|
|
584
|
-
const models = await this.listModels();
|
|
585
|
-
this.discovered = models;
|
|
586
|
-
return models;
|
|
587
|
-
}
|
|
588
|
-
async getModel(modelId) {
|
|
589
|
-
const models = this.discovered ?? await this.connect();
|
|
590
|
-
return models.find((m) => m.id === modelId);
|
|
591
|
-
}
|
|
592
|
-
/** Raw /models body (protected so adapters can capture gateway-level metadata). */
|
|
593
|
-
async rawModelsResponse() {
|
|
594
|
-
const response = await this.modelsRequest();
|
|
595
|
-
return response.body;
|
|
596
|
-
}
|
|
597
|
-
async modelsRequest() {
|
|
598
|
-
let response;
|
|
599
|
-
try {
|
|
600
|
-
response = await requestJson(`${this.config.baseUrl}/models`, {
|
|
601
|
-
headers: authHeaders(this.config, this.env),
|
|
602
|
-
timeoutMs: this.requestTimeoutMs
|
|
603
|
-
});
|
|
604
|
-
} catch (error) {
|
|
605
|
-
if (error instanceof ProviderError) throw error;
|
|
606
|
-
throw providerTransportError({ provider: this.config.name, operation: "listModels", cause: error });
|
|
607
|
-
}
|
|
608
|
-
const { status, body } = response;
|
|
609
|
-
if (status !== 200) {
|
|
610
|
-
throw providerHttpError({
|
|
611
|
-
provider: this.config.name,
|
|
612
|
-
operation: "listModels",
|
|
613
|
-
status,
|
|
614
|
-
body
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
return response;
|
|
618
|
-
}
|
|
619
|
-
async listModels() {
|
|
620
|
-
const body = await this.rawModelsResponse();
|
|
621
|
-
const entries = extractModelEntries(body, this.config);
|
|
622
|
-
const models = [];
|
|
623
|
-
let skipped = 0;
|
|
624
|
-
for (const entry2 of entries) {
|
|
625
|
-
if (typeof entry2.id !== "string" || entry2.id.length === 0) {
|
|
626
|
-
skipped += 1;
|
|
627
|
-
continue;
|
|
628
|
-
}
|
|
629
|
-
models.push(this.toModelInfo(entry2));
|
|
630
|
-
}
|
|
631
|
-
if (skipped > 0) {
|
|
632
|
-
this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
|
|
633
|
-
}
|
|
634
|
-
return models;
|
|
635
|
-
}
|
|
636
|
-
/** Maps a raw model entry onto the normalized metadata shape (§10). */
|
|
637
|
-
toModelInfo(entry2) {
|
|
638
|
-
const parameters = Array.isArray(entry2.supported_parameters) ? entry2.supported_parameters.filter((p) => typeof p === "string") : void 0;
|
|
639
|
-
const contextWindow = typeof entry2.context_length === "number" ? entry2.context_length : typeof entry2.context_window === "number" ? entry2.context_window : void 0;
|
|
640
|
-
const inputTypes = normalizeModalities(entry2.input_modalities);
|
|
641
|
-
const outputTypes = normalizeModalities(entry2.output_modalities);
|
|
642
|
-
return {
|
|
643
|
-
provider: this.config.name,
|
|
644
|
-
id: entry2.id,
|
|
645
|
-
displayName: entry2.id,
|
|
646
|
-
...typeof entry2.name === "string" ? { name: entry2.name } : {},
|
|
647
|
-
status: "available",
|
|
648
|
-
...contextWindow !== void 0 ? { contextWindow } : {},
|
|
649
|
-
...inputTypes !== void 0 ? { inputTypes } : {},
|
|
650
|
-
...outputTypes !== void 0 ? { outputTypes } : {},
|
|
651
|
-
...parameters !== void 0 ? {
|
|
652
|
-
supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
|
|
653
|
-
supportsStructuredOutput: parameters.includes("response_format"),
|
|
654
|
-
supportsStreaming: parameters.includes("stream")
|
|
655
|
-
} : {},
|
|
656
|
-
metadata: {
|
|
657
|
-
...typeof entry2.owned_by === "string" ? { ownedBy: entry2.owned_by } : {},
|
|
658
|
-
...typeof entry2.created === "number" ? { createdAt: entry2.created } : {},
|
|
659
|
-
...parameters !== void 0 ? { supportedParameters: parameters } : {}
|
|
660
|
-
}
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
/** Streaming where the gateway supports it (§11). */
|
|
664
|
-
stream(request, onChunk) {
|
|
665
|
-
return streamChatCompletion(this.config, request, onChunk, this.env);
|
|
666
|
-
}
|
|
667
|
-
async healthCheck() {
|
|
668
|
-
const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
669
|
-
try {
|
|
670
|
-
const started = Date.now();
|
|
671
|
-
const models = await this.listModels();
|
|
672
|
-
return {
|
|
673
|
-
name: this.config.name,
|
|
674
|
-
type: this.config.type,
|
|
675
|
-
baseUrl: this.config.baseUrl,
|
|
676
|
-
health: "healthy",
|
|
677
|
-
connected: true,
|
|
678
|
-
modelCount: models.length,
|
|
679
|
-
lastCheckedAt,
|
|
680
|
-
message: `Discovered ${models.length} model(s) in ${Date.now() - started}ms.`
|
|
681
|
-
};
|
|
682
|
-
} catch (error) {
|
|
683
|
-
return {
|
|
684
|
-
name: this.config.name,
|
|
685
|
-
type: this.config.type,
|
|
686
|
-
baseUrl: this.config.baseUrl,
|
|
687
|
-
health: inferHealth(error),
|
|
688
|
-
connected: false,
|
|
689
|
-
modelCount: 0,
|
|
690
|
-
lastCheckedAt,
|
|
691
|
-
message: error instanceof Error ? error.message : String(error)
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
async chat(request) {
|
|
696
|
-
let response;
|
|
697
|
-
try {
|
|
698
|
-
response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
|
|
699
|
-
method: "POST",
|
|
700
|
-
headers: authHeaders(this.config, this.env),
|
|
701
|
-
timeoutMs: this.requestTimeoutMs,
|
|
702
|
-
body: {
|
|
703
|
-
model: request.model,
|
|
704
|
-
messages: request.messages,
|
|
705
|
-
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
706
|
-
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
|
|
707
|
-
...request.stop !== void 0 ? { stop: request.stop } : {}
|
|
708
|
-
}
|
|
709
|
-
});
|
|
710
|
-
} catch (error) {
|
|
711
|
-
if (error instanceof ProviderError) throw error;
|
|
712
|
-
throw providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
|
|
713
|
-
}
|
|
714
|
-
const { status, body } = response;
|
|
715
|
-
if (status !== 200) {
|
|
716
|
-
throw providerHttpError({
|
|
717
|
-
provider: this.config.name,
|
|
718
|
-
operation: "chat",
|
|
719
|
-
status,
|
|
720
|
-
body,
|
|
721
|
-
...status === 404 ? {
|
|
722
|
-
code: "MODEL_NOT_FOUND"
|
|
723
|
-
} : {}
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
const payload = body;
|
|
727
|
-
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
728
|
-
const first = choices[0];
|
|
729
|
-
if (!first || typeof first.message?.content !== "string") {
|
|
730
|
-
throw unavailable(this.config, "Chat completion response had no message content.", status);
|
|
731
|
-
}
|
|
732
|
-
const usage = payload.usage ?? {};
|
|
733
|
-
return {
|
|
734
|
-
model: typeof payload.model === "string" ? payload.model : request.model,
|
|
735
|
-
content: first.message.content,
|
|
736
|
-
finishReason: typeof first.finish_reason === "string" ? first.finish_reason : void 0,
|
|
737
|
-
usage: {
|
|
738
|
-
promptTokens: typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : void 0,
|
|
739
|
-
completionTokens: typeof usage.completion_tokens === "number" ? usage.completion_tokens : void 0,
|
|
740
|
-
totalTokens: typeof usage.total_tokens === "number" ? usage.total_tokens : void 0
|
|
741
|
-
},
|
|
742
|
-
raw: body
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
};
|
|
746
|
-
async function streamChatCompletion(config, request, onChunk, env = process.env) {
|
|
747
|
-
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
748
|
-
const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
749
|
-
let response;
|
|
750
|
-
try {
|
|
751
|
-
response = await fetch(`${baseUrl}/chat/completions`, {
|
|
752
|
-
method: "POST",
|
|
753
|
-
headers: {
|
|
754
|
-
accept: "text/event-stream",
|
|
755
|
-
"content-type": "application/json",
|
|
756
|
-
...authHeaders(config, env)
|
|
757
|
-
},
|
|
758
|
-
body: JSON.stringify({
|
|
759
|
-
model: request.model,
|
|
760
|
-
messages: request.messages,
|
|
761
|
-
stream: true,
|
|
762
|
-
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
763
|
-
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {}
|
|
764
|
-
}),
|
|
765
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
766
|
-
});
|
|
767
|
-
} catch (error) {
|
|
768
|
-
throw providerTransportError({ provider: config.name, operation: "stream", cause: error });
|
|
769
|
-
}
|
|
770
|
-
if (!response.ok) {
|
|
771
|
-
const body = await response.json().catch(() => void 0);
|
|
772
|
-
throw providerHttpError({ provider: config.name, operation: "stream", status: response.status, body });
|
|
773
|
-
}
|
|
774
|
-
if (!response.body) {
|
|
775
|
-
throw providerHttpError({
|
|
776
|
-
provider: config.name,
|
|
777
|
-
operation: "stream",
|
|
778
|
-
status: 500,
|
|
779
|
-
body: { error: { message: "streaming is not supported by this endpoint" } }
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
let content = "";
|
|
783
|
-
let model = request.model;
|
|
784
|
-
let finishReason;
|
|
785
|
-
const decoder = new TextDecoder();
|
|
786
|
-
let buffer = "";
|
|
787
|
-
for await (const piece of response.body) {
|
|
788
|
-
buffer += decoder.decode(piece, { stream: true });
|
|
789
|
-
let newline = buffer.indexOf("\n");
|
|
790
|
-
while (newline >= 0) {
|
|
791
|
-
const line = buffer.slice(0, newline).trim();
|
|
792
|
-
buffer = buffer.slice(newline + 1);
|
|
793
|
-
newline = buffer.indexOf("\n");
|
|
794
|
-
if (line.length === 0 || line.startsWith(":")) continue;
|
|
795
|
-
const data = line.startsWith("data:") ? line.slice(5).trim() : line;
|
|
796
|
-
if (data === "[DONE]") continue;
|
|
797
|
-
let event;
|
|
798
|
-
try {
|
|
799
|
-
event = JSON.parse(data);
|
|
800
|
-
} catch {
|
|
801
|
-
continue;
|
|
802
|
-
}
|
|
803
|
-
if (typeof event.model === "string") model = event.model;
|
|
804
|
-
const choices = Array.isArray(event.choices) ? event.choices : [];
|
|
805
|
-
const first = choices[0];
|
|
806
|
-
const delta = typeof first?.delta?.content === "string" ? first.delta.content : "";
|
|
807
|
-
if (delta.length > 0) content += delta;
|
|
808
|
-
if (typeof first?.finish_reason === "string") finishReason = first.finish_reason;
|
|
809
|
-
onChunk({ delta, ...finishReason !== void 0 ? { finishReason } : {}, raw: event });
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
return { model, content, ...finishReason !== void 0 ? { finishReason } : {} };
|
|
813
|
-
}
|
|
814
|
-
function normalizeModalities(value) {
|
|
815
|
-
const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
|
|
816
|
-
if (!list) return void 0;
|
|
817
|
-
const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
|
|
818
|
-
const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
|
|
819
|
-
return modalities.length > 0 ? modalities : void 0;
|
|
820
|
-
}
|
|
821
|
-
function normalizeBaseUrl(baseUrl) {
|
|
822
|
-
return baseUrl.replace(/\/+$/, "");
|
|
823
|
-
}
|
|
824
|
-
function extractModelEntries(body, config) {
|
|
825
|
-
if (Array.isArray(body)) return body;
|
|
826
|
-
if (body !== null && typeof body === "object" && Array.isArray(body.data)) {
|
|
827
|
-
return body.data;
|
|
828
|
-
}
|
|
829
|
-
throw new ProviderError(
|
|
830
|
-
"PROVIDER_UNAVAILABLE",
|
|
831
|
-
`Provider "${config.name}" returned an unrecognized model list shape.`,
|
|
832
|
-
{ details: { endpoint: "models" } }
|
|
833
|
-
);
|
|
834
|
-
}
|
|
835
|
-
function unavailable(config, message, status) {
|
|
836
|
-
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
|
|
837
|
-
details: { status }
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
function inferHealth(error) {
|
|
841
|
-
const status = error instanceof ProviderError && typeof error.details?.status === "number" ? error.details.status : void 0;
|
|
842
|
-
return status !== void 0 && status >= 500 ? "degraded" : "unreachable";
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
// src/providers/omnihilbras/index.ts
|
|
846
|
-
var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
|
|
847
|
-
gateway;
|
|
848
|
-
async listModels() {
|
|
849
|
-
const raw = await this.rawModelsResponse();
|
|
850
|
-
await this.extractGatewayMetadata(raw);
|
|
851
|
-
const models = await super.listModels();
|
|
852
|
-
return this.gateway ? models.map((model) => ({ ...model, metadata: { ...model.metadata, gateway: this.gateway } })) : models;
|
|
853
|
-
}
|
|
854
|
-
/**
|
|
855
|
-
* Run the full connect flow. Throws on authentication or transport failure
|
|
856
|
-
* (use healthCheck() for a non-throwing probe).
|
|
857
|
-
*/
|
|
858
|
-
/**
|
|
859
|
-
* Gateway metadata reported alongside the model list (gateway version, tier,
|
|
860
|
-
* region…), kept separate from per-model metadata (§11 — provider metadata).
|
|
861
|
-
*/
|
|
862
|
-
gatewayMetadata() {
|
|
863
|
-
return this.gateway;
|
|
864
|
-
}
|
|
865
|
-
async extractGatewayMetadata(body) {
|
|
866
|
-
if (body !== null && typeof body === "object") {
|
|
867
|
-
const gateway = body.gateway;
|
|
868
|
-
if (gateway !== null && typeof gateway === "object") {
|
|
869
|
-
this.gateway = gateway;
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
async connectAndRegister(registry) {
|
|
874
|
-
const apiKey = resolveApiKey(this.config);
|
|
875
|
-
const discovered = await this.listModels();
|
|
876
|
-
const seen = /* @__PURE__ */ new Set();
|
|
877
|
-
const models = [];
|
|
878
|
-
const duplicates = [];
|
|
879
|
-
for (const model of discovered) {
|
|
880
|
-
if (seen.has(model.id)) {
|
|
881
|
-
duplicates.push(model.id);
|
|
882
|
-
continue;
|
|
883
|
-
}
|
|
884
|
-
seen.add(model.id);
|
|
885
|
-
models.push({
|
|
886
|
-
...model,
|
|
887
|
-
provider: this.config.name,
|
|
888
|
-
status: "available",
|
|
889
|
-
metadata: { ...model.metadata, gateway: "omnihilbras" }
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
if (registry) {
|
|
893
|
-
for (const model of models) registry.register(model);
|
|
894
|
-
}
|
|
895
|
-
const message = `Authenticated${apiKey ? " with API key" : " (no API key configured)"}; validated ${models.length} unique model(s)` + (duplicates.length > 0 ? `, skipped ${duplicates.length} duplicate(s)` : "") + ".";
|
|
896
|
-
const status = {
|
|
897
|
-
name: this.config.name,
|
|
898
|
-
type: this.config.type,
|
|
899
|
-
baseUrl: this.config.baseUrl,
|
|
900
|
-
health: "healthy",
|
|
901
|
-
connected: true,
|
|
902
|
-
modelCount: models.length,
|
|
903
|
-
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
904
|
-
message
|
|
905
|
-
};
|
|
906
|
-
return { status, models, duplicates, registered: registry ? models.length : 0 };
|
|
907
|
-
}
|
|
908
|
-
};
|
|
909
|
-
|
|
910
|
-
// src/providers/factory.ts
|
|
911
|
-
var OPENAI_COMPATIBLE_TYPES = [
|
|
912
|
-
"openai-compatible",
|
|
913
|
-
"openrouter",
|
|
914
|
-
"local",
|
|
915
|
-
"custom"
|
|
916
|
-
];
|
|
917
|
-
function createProvider(config) {
|
|
918
|
-
if (config.type === "omnihilbras") {
|
|
919
|
-
return new OmniHilbrasProvider(config);
|
|
920
|
-
}
|
|
921
|
-
if (OPENAI_COMPATIBLE_TYPES.includes(config.type)) {
|
|
922
|
-
return new OpenAICompatibleProvider(config);
|
|
923
|
-
}
|
|
924
|
-
throw new OmniNodeError("NOT_IMPLEMENTED", `Provider type "${config.type}" has no adapter yet.`);
|
|
925
|
-
}
|
|
926
|
-
|
|
927
360
|
// src/registry/index.ts
|
|
928
361
|
function registryKey(provider, id) {
|
|
929
362
|
return `${provider}:${id}`;
|
|
@@ -1008,183 +441,24 @@ function appendToConfigList(listKey, item) {
|
|
|
1008
441
|
return configPath;
|
|
1009
442
|
}
|
|
1010
443
|
|
|
1011
|
-
// src/
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
for (const provider2 of providers) {
|
|
1024
|
-
const key = provider2.apiKeyEnvVar ? `$${provider2.apiKeyEnvVar}` : "none";
|
|
1025
|
-
const enabled = provider2.enabled === false ? " (disabled)" : "";
|
|
1026
|
-
console.log(
|
|
1027
|
-
`${provider2.name} type=${provider2.type} base_url=${provider2.baseUrl} key=${key}${enabled}`
|
|
1028
|
-
);
|
|
1029
|
-
}
|
|
1030
|
-
});
|
|
1031
|
-
provider.command("add").description(
|
|
1032
|
-
"Add a provider to omninode.yaml. OpenAI-compatible gateways (including OpenRouter and local runtimes) are supported today; OmniHilbras gets its own adapter in Phase 2."
|
|
1033
|
-
).requiredOption("--name <name>", "Unique provider name used in configuration and the model registry.").requiredOption("--base-url <url>", "Provider base URL, e.g. https://example.com/v1").option(
|
|
1034
|
-
"--type <type>",
|
|
1035
|
-
"Provider type: openai-compatible, omnihilbras, openrouter, local or custom.",
|
|
1036
|
-
"openai-compatible"
|
|
1037
|
-
).option(
|
|
1038
|
-
"--api-key-env-var <envVar>",
|
|
1039
|
-
"Environment variable holding the API key. Keys are never written to the config file."
|
|
1040
|
-
).action(async (options) => addProvider(options));
|
|
1041
|
-
provider.command("test <name>").description("Test connectivity, authentication and model discovery for a configured provider.").option(
|
|
1042
|
-
"--connect",
|
|
1043
|
-
"Run the full provider connect flow (authenticate, fetch, validate, register) where the adapter supports it."
|
|
1044
|
-
).action(async (name, options) => testProvider(name, options));
|
|
1045
|
-
provider.command("remove <name>").description("Remove a provider (planned for Phase 1 follow-up).").action(() => {
|
|
1046
|
-
throw notImplemented("omninode provider remove", "a Phase 1 follow-up");
|
|
1047
|
-
});
|
|
444
|
+
// src/agents/process/index.ts
|
|
445
|
+
import { spawn } from "child_process";
|
|
446
|
+
|
|
447
|
+
// src/agent-protocol/version.ts
|
|
448
|
+
var PROTOCOL_V2 = "omninode-agent-protocol/2";
|
|
449
|
+
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
450
|
+
"omninode-agent-protocol/1",
|
|
451
|
+
PROTOCOL_V2
|
|
452
|
+
];
|
|
453
|
+
var MAX_MESSAGE_BYTES = 1048576;
|
|
454
|
+
function isSupportedProtocol(protocol) {
|
|
455
|
+
return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
|
|
1048
456
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
}
|
|
1054
|
-
const candidate = providerConfigSchema.safeParse({
|
|
1055
|
-
name: options.name,
|
|
1056
|
-
type: options.type,
|
|
1057
|
-
base_url: options.baseUrl,
|
|
1058
|
-
...options.apiKeyEnvVar !== void 0 ? { api_key_env_var: options.apiKeyEnvVar } : {}
|
|
1059
|
-
});
|
|
1060
|
-
if (!candidate.success) {
|
|
1061
|
-
const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
1062
|
-
throw new OmniNodeError("CLI_USAGE", `Invalid provider options: ${issues}`);
|
|
1063
|
-
}
|
|
1064
|
-
const newProvider = candidate.data;
|
|
1065
|
-
const existing = loadConfig();
|
|
1066
|
-
if (existing.project.providers.some((p) => p.name === newProvider.name)) {
|
|
1067
|
-
throw new OmniNodeError(
|
|
1068
|
-
"CLI_USAGE",
|
|
1069
|
-
`Provider "${newProvider.name}" already exists in ${findConfigFile() ?? "omninode.yaml"}.`
|
|
1070
|
-
);
|
|
1071
|
-
}
|
|
1072
|
-
const writtenPath = appendToConfigList("providers", newProvider);
|
|
1073
|
-
console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${writtenPath}.`);
|
|
1074
|
-
if (newProvider.api_key_env_var) {
|
|
1075
|
-
console.log(
|
|
1076
|
-
`Set $${newProvider.api_key_env_var} in your environment, then run: omninode provider test ${newProvider.name}`
|
|
1077
|
-
);
|
|
1078
|
-
} else {
|
|
1079
|
-
console.log(`Run \`omninode provider test ${newProvider.name}\` to verify connectivity.`);
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
async function testProvider(name, options = {}) {
|
|
1083
|
-
const config = loadConfig();
|
|
1084
|
-
const providerConfig = config.project.providers.find((p) => p.name === name);
|
|
1085
|
-
if (!providerConfig) {
|
|
1086
|
-
const available = config.project.providers.map((p) => p.name).join(", ") || "(none)";
|
|
1087
|
-
throw new ProviderError(
|
|
1088
|
-
"PROVIDER_NOT_FOUND",
|
|
1089
|
-
`Provider "${name}" is not configured. Configured providers: ${available}.`
|
|
1090
|
-
);
|
|
1091
|
-
}
|
|
1092
|
-
const provider = createProvider(providerConfig);
|
|
1093
|
-
console.log(`Testing provider "${name}" (${providerConfig.type}) at ${provider.config.baseUrl}`);
|
|
1094
|
-
if (options.connect && provider instanceof OmniHilbrasProvider) {
|
|
1095
|
-
const registry = new ModelRegistry();
|
|
1096
|
-
try {
|
|
1097
|
-
const result = await provider.connectAndRegister(registry);
|
|
1098
|
-
printProviderStatus(result.status);
|
|
1099
|
-
console.log(` registered: ${result.registered}`);
|
|
1100
|
-
} catch (error) {
|
|
1101
|
-
console.log(` connect failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1102
|
-
process.exitCode = 1;
|
|
1103
|
-
}
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
const health = await provider.healthCheck();
|
|
1107
|
-
printProviderStatus(health);
|
|
1108
|
-
if (!health.connected) process.exitCode = 1;
|
|
1109
|
-
}
|
|
1110
|
-
function printProviderStatus(status) {
|
|
1111
|
-
console.log(` health: ${status.health}`);
|
|
1112
|
-
console.log(` connected: ${status.connected}`);
|
|
1113
|
-
console.log(` models: ${status.modelCount}`);
|
|
1114
|
-
if (status.message) console.log(` detail: ${status.message}`);
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
// src/cli/commands/models.ts
|
|
1118
|
-
var CAPABILITIES = [
|
|
1119
|
-
"chat",
|
|
1120
|
-
"coding",
|
|
1121
|
-
"tools",
|
|
1122
|
-
"vision",
|
|
1123
|
-
"embeddings"
|
|
1124
|
-
];
|
|
1125
|
-
function registerModelsCommand(program) {
|
|
1126
|
-
program.command("models").description("Discover and list models from configured providers (model registry, \xA78).").argument("[provider]", "Only discover from this provider.").option("-c, --capability <capability>", `Filter by capability: ${CAPABILITIES.join(", ")}.`).action(async (providerName, options) => {
|
|
1127
|
-
const capability = options.capability;
|
|
1128
|
-
if (capability !== void 0 && !CAPABILITIES.includes(capability)) {
|
|
1129
|
-
throw new OmniNodeError(
|
|
1130
|
-
"CLI_USAGE",
|
|
1131
|
-
`Unknown capability "${options.capability}". Valid capabilities: ${CAPABILITIES.join(", ")}.`
|
|
1132
|
-
);
|
|
1133
|
-
}
|
|
1134
|
-
const config = loadConfig();
|
|
1135
|
-
const selected = config.project.providers.filter(
|
|
1136
|
-
(p) => providerName ? p.name === providerName : true
|
|
1137
|
-
);
|
|
1138
|
-
if (selected.length === 0) {
|
|
1139
|
-
throw new OmniNodeError(
|
|
1140
|
-
"PROVIDER_NOT_FOUND",
|
|
1141
|
-
providerName ? `Provider "${providerName}" is not configured.` : "No providers configured. Add one with `omninode provider add`."
|
|
1142
|
-
);
|
|
1143
|
-
}
|
|
1144
|
-
const registry = new ModelRegistry();
|
|
1145
|
-
for (const providerConfig of selected) {
|
|
1146
|
-
try {
|
|
1147
|
-
const provider = createProvider(providerConfig);
|
|
1148
|
-
const discovered = await registry.discoverFrom(provider);
|
|
1149
|
-
console.log(`${providerConfig.name}: ${discovered.length} model(s)`);
|
|
1150
|
-
} catch (error) {
|
|
1151
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1152
|
-
console.log(`${providerConfig.name}: discovery failed \u2014 ${message}`);
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
const models = registry.list({
|
|
1156
|
-
...providerName !== void 0 ? { provider: providerName } : {},
|
|
1157
|
-
...capability !== void 0 ? { capability } : {}
|
|
1158
|
-
});
|
|
1159
|
-
if (models.length === 0) {
|
|
1160
|
-
console.log("No models found.");
|
|
1161
|
-
return;
|
|
1162
|
-
}
|
|
1163
|
-
console.log("");
|
|
1164
|
-
for (const model of models) {
|
|
1165
|
-
console.log(`${model.provider}:${model.id}`);
|
|
1166
|
-
}
|
|
1167
|
-
});
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
// src/agents/process/index.ts
|
|
1171
|
-
import { spawn } from "child_process";
|
|
1172
|
-
|
|
1173
|
-
// src/agent-protocol/version.ts
|
|
1174
|
-
var PROTOCOL_V2 = "omninode-agent-protocol/2";
|
|
1175
|
-
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
1176
|
-
"omninode-agent-protocol/1",
|
|
1177
|
-
PROTOCOL_V2
|
|
1178
|
-
];
|
|
1179
|
-
var MAX_MESSAGE_BYTES = 1048576;
|
|
1180
|
-
function isSupportedProtocol(protocol) {
|
|
1181
|
-
return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
// src/agent-protocol/types.ts
|
|
1185
|
-
function newMessageId(prefix = "msg") {
|
|
1186
|
-
const random = Math.random().toString(36).slice(2, 10);
|
|
1187
|
-
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
457
|
+
|
|
458
|
+
// src/agent-protocol/types.ts
|
|
459
|
+
function newMessageId(prefix = "msg") {
|
|
460
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
461
|
+
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
1188
462
|
}
|
|
1189
463
|
function envelope(type, payload, ids = {}) {
|
|
1190
464
|
return {
|
|
@@ -1576,7 +850,7 @@ function buildChildEnv(config) {
|
|
|
1576
850
|
}
|
|
1577
851
|
|
|
1578
852
|
// src/agents/process/index.ts
|
|
1579
|
-
var
|
|
853
|
+
var DEFAULT_TIMEOUT_MS = 6e5;
|
|
1580
854
|
var KILL_GRACE_MS = 5e3;
|
|
1581
855
|
var DEFAULT_MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
|
|
1582
856
|
function composeTaskText(input) {
|
|
@@ -1639,7 +913,7 @@ var ProcessAgent = class {
|
|
|
1639
913
|
throw new AgentError("AGENT_NOT_FOUND", `Agent "${this.config.name}" has no command configured.`);
|
|
1640
914
|
}
|
|
1641
915
|
const mode = this.config.inputMode ?? "stdin";
|
|
1642
|
-
const timeoutMs = this.config.timeoutMs ??
|
|
916
|
+
const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1643
917
|
return new Promise((resolve2, reject) => {
|
|
1644
918
|
let cwd;
|
|
1645
919
|
try {
|
|
@@ -1998,138 +1272,57 @@ function buildAgentRegistry(config, log = logger) {
|
|
|
1998
1272
|
return registry;
|
|
1999
1273
|
}
|
|
2000
1274
|
|
|
2001
|
-
// src/
|
|
2002
|
-
var
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
const config = loadConfig();
|
|
2007
|
-
const agents = config.project.agents;
|
|
2008
|
-
if (agents.length === 0) {
|
|
2009
|
-
console.log("No agents configured. Add one with `omninode agent add` or edit omninode.yaml.");
|
|
2010
|
-
return;
|
|
2011
|
-
}
|
|
2012
|
-
for (const agent2 of agents) {
|
|
2013
|
-
const command = agent2.command ?? "(no command)";
|
|
2014
|
-
console.log(
|
|
2015
|
-
`${agent2.name} integration=${agent2.integration} command=${command} input=${agent2.inputMode ?? "stdin"}`
|
|
2016
|
-
);
|
|
2017
|
-
}
|
|
2018
|
-
});
|
|
2019
|
-
agent.command("add").description(
|
|
2020
|
-
"Register a CLI agent in omninode.yaml. Agents run as child processes (stdin/stdout); native adapters come in a later phase."
|
|
2021
|
-
).requiredOption("--name <name>", "Unique agent name.").requiredOption("--command <command>", "Executable command, e.g. opencode").option("--arg <value>", "Additional command argument (repeatable).", collectArgs, []).option("--cwd <dir>", "Working directory for the agent process.").option(
|
|
2022
|
-
"--input-mode <mode>",
|
|
2023
|
-
"How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
|
|
2024
|
-
"stdin"
|
|
2025
|
-
).option("--env-policy <policy>", "Environment policy: inherit, allowlist, denylist or explicit.").option("--allow-env <name>", "Variable to forward under the allowlist policy (repeatable).", collectArgs).option("--deny-env <name>", "Variable to remove under the denylist policy (repeatable).", collectArgs).option("--max-output-bytes <n>", "Per-stream output budget in bytes (default 5 MiB).", (value) => Number(value)).option("--allow-external-cwd", "Allow a working directory outside the project root.").option(
|
|
2026
|
-
"--timeout-ms <ms>",
|
|
2027
|
-
"Per-task timeout in milliseconds.",
|
|
2028
|
-
(value) => Number(value)
|
|
2029
|
-
).action(async (options) => addAgent(options));
|
|
2030
|
-
agent.command("test <name>").description("Verify the OmniNode \u2194 agent connection by sending a trivial task.").action(async (name) => testAgent(name));
|
|
2031
|
-
}
|
|
2032
|
-
async function addAgent(options) {
|
|
2033
|
-
const candidate = agentConfigSchema.safeParse({
|
|
2034
|
-
name: options.name,
|
|
2035
|
-
type: "cli",
|
|
2036
|
-
command: options.command,
|
|
2037
|
-
...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
|
|
2038
|
-
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
2039
|
-
input_mode: options.inputMode,
|
|
2040
|
-
...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {},
|
|
2041
|
-
...options.envPolicy !== void 0 ? { env_policy: options.envPolicy } : {},
|
|
2042
|
-
...options.allowEnv !== void 0 && options.allowEnv.length > 0 ? { env_allowlist: options.allowEnv } : {},
|
|
2043
|
-
...options.denyEnv !== void 0 && options.denyEnv.length > 0 ? { env_denylist: options.denyEnv } : {},
|
|
2044
|
-
...options.maxOutputBytes !== void 0 ? { max_output_bytes: options.maxOutputBytes } : {},
|
|
2045
|
-
...options.allowExternalCwd !== void 0 ? { allow_external_cwd: options.allowExternalCwd } : {}
|
|
2046
|
-
});
|
|
2047
|
-
if (!candidate.success) {
|
|
2048
|
-
const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
2049
|
-
throw new OmniNodeError("CLI_USAGE", `Invalid agent options: ${issues}`);
|
|
1275
|
+
// src/roles/index.ts
|
|
1276
|
+
var RoleRegistry = class {
|
|
1277
|
+
roles = /* @__PURE__ */ new Map();
|
|
1278
|
+
register(role) {
|
|
1279
|
+
this.roles.set(role.id, role);
|
|
2050
1280
|
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
if (existing.project.agents.some((a) => a.name === newAgent.name)) {
|
|
2054
|
-
throw new OmniNodeError(
|
|
2055
|
-
"CLI_USAGE",
|
|
2056
|
-
`Agent "${newAgent.name}" already exists in omninode.yaml. Remove it first or pick another name.`
|
|
2057
|
-
);
|
|
1281
|
+
registerAll(roles) {
|
|
1282
|
+
for (const role of roles) this.register(role);
|
|
2058
1283
|
}
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
console.log(`Run \`omninode agent test ${newAgent.name}\` to verify the connection.`);
|
|
2062
|
-
}
|
|
2063
|
-
async function testAgent(name) {
|
|
2064
|
-
const config = loadConfig();
|
|
2065
|
-
const agentConfig = config.project.agents.find((a) => a.name === name);
|
|
2066
|
-
if (!agentConfig) {
|
|
2067
|
-
const available = config.project.agents.map((a) => a.name).join(", ") || "(none)";
|
|
2068
|
-
throw new AgentError(
|
|
2069
|
-
"AGENT_NOT_FOUND",
|
|
2070
|
-
`Agent "${name}" is not configured. Configured agents: ${available}.`
|
|
2071
|
-
);
|
|
1284
|
+
get(id) {
|
|
1285
|
+
return this.roles.get(id);
|
|
2072
1286
|
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
`Testing agent "${name}" (${agent.info.integration}, input=${agentConfig.inputMode ?? "stdin"}) \u2014 sending a trivial task.`
|
|
2076
|
-
);
|
|
2077
|
-
const result = await agent.run({
|
|
2078
|
-
taskId: `test-${Date.now()}`,
|
|
2079
|
-
objective: "Reply with the single word: OK"
|
|
2080
|
-
});
|
|
2081
|
-
console.log(` status: ${result.status}`);
|
|
2082
|
-
if (result.exitCode !== void 0) console.log(` exit: ${result.exitCode}`);
|
|
2083
|
-
if (result.summary) console.log(` output: ${result.summary}`);
|
|
2084
|
-
if (result.rawOutput && result.rawOutput !== result.summary) {
|
|
2085
|
-
console.log(` raw: ${result.rawOutput.slice(0, 200).replace(/\n/g, " ")}`);
|
|
1287
|
+
list() {
|
|
1288
|
+
return [...this.roles.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
2086
1289
|
}
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
function registerRoleCommands(program) {
|
|
2093
|
-
const role = program.command("role").description("Inspect roles defined in omninode.yaml.");
|
|
2094
|
-
role.command("list").description("List configured roles.").action(async () => {
|
|
2095
|
-
const config = loadConfig();
|
|
2096
|
-
const roles = config.project.roles;
|
|
2097
|
-
if (roles.length === 0) {
|
|
2098
|
-
console.log("No roles configured. Add one under `project.roles` in omninode.yaml.");
|
|
2099
|
-
return;
|
|
2100
|
-
}
|
|
2101
|
-
for (const role2 of roles) {
|
|
2102
|
-
console.log(`${role2.id} ${role2.name}`);
|
|
2103
|
-
for (const responsibility of role2.responsibilities) {
|
|
2104
|
-
console.log(` - ${responsibility}`);
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
});
|
|
1290
|
+
};
|
|
1291
|
+
function buildRoleRegistry(config) {
|
|
1292
|
+
const registry = new RoleRegistry();
|
|
1293
|
+
registry.registerAll(config.project.roles);
|
|
1294
|
+
return registry;
|
|
2108
1295
|
}
|
|
2109
1296
|
|
|
2110
1297
|
// src/persistence/json-file-store.ts
|
|
2111
|
-
import { open, mkdir, readFile, rename, stat } from "fs/promises";
|
|
1298
|
+
import { open, mkdir, readFile, rename, stat, writeFile } from "fs/promises";
|
|
2112
1299
|
import { dirname } from "path";
|
|
2113
1300
|
var SCHEMA_VERSION = 2;
|
|
2114
|
-
async function writeFileAtomic(target, contents) {
|
|
1301
|
+
async function writeFileAtomic(target, contents, durability = "rename") {
|
|
2115
1302
|
const tmp = `${target}.tmp`;
|
|
2116
1303
|
await mkdir(dirname(target), { recursive: true });
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
1304
|
+
if (durability === "fsync") {
|
|
1305
|
+
const handle = await open(tmp, "w");
|
|
1306
|
+
try {
|
|
1307
|
+
await handle.writeFile(contents, "utf8");
|
|
1308
|
+
await handle.sync();
|
|
1309
|
+
} finally {
|
|
1310
|
+
await handle.close();
|
|
1311
|
+
}
|
|
1312
|
+
} else {
|
|
1313
|
+
await writeFile(tmp, contents, "utf8");
|
|
2123
1314
|
}
|
|
2124
1315
|
await rename(tmp, target);
|
|
2125
|
-
|
|
2126
|
-
const dir = await open(dirname(target), "r");
|
|
1316
|
+
if (durability === "fsync") {
|
|
2127
1317
|
try {
|
|
2128
|
-
await
|
|
2129
|
-
|
|
2130
|
-
|
|
1318
|
+
const dir = await open(dirname(target), "r");
|
|
1319
|
+
try {
|
|
1320
|
+
await dir.sync();
|
|
1321
|
+
} finally {
|
|
1322
|
+
await dir.close();
|
|
1323
|
+
}
|
|
1324
|
+
} catch {
|
|
2131
1325
|
}
|
|
2132
|
-
} catch {
|
|
2133
1326
|
}
|
|
2134
1327
|
}
|
|
2135
1328
|
async function inspectDocument(path) {
|
|
@@ -2199,6 +1392,7 @@ async function readDocument(path, onCorrupt, quarantine = true) {
|
|
|
2199
1392
|
var JsonFileStore = class {
|
|
2200
1393
|
filePath;
|
|
2201
1394
|
schemaVersion;
|
|
1395
|
+
durability;
|
|
2202
1396
|
onCorrupt;
|
|
2203
1397
|
quarantine;
|
|
2204
1398
|
log;
|
|
@@ -2206,6 +1400,7 @@ var JsonFileStore = class {
|
|
|
2206
1400
|
constructor(options) {
|
|
2207
1401
|
this.filePath = `${options.directory ?? `${process.cwd()}/.omninode`}/${options.fileName}`;
|
|
2208
1402
|
this.schemaVersion = options.schemaVersion ?? SCHEMA_VERSION;
|
|
1403
|
+
this.durability = options.durability ?? "rename";
|
|
2209
1404
|
this.onCorrupt = options.onCorrupt;
|
|
2210
1405
|
this.quarantine = options.quarantine ?? true;
|
|
2211
1406
|
this.log = logger.child({ component: "persistence", file: options.fileName });
|
|
@@ -2220,7 +1415,7 @@ var JsonFileStore = class {
|
|
|
2220
1415
|
async writeAll(items) {
|
|
2221
1416
|
const document = { schemaVersion: this.schemaVersion, items };
|
|
2222
1417
|
await writeFileAtomic(this.filePath, `${JSON.stringify(document, null, 2)}
|
|
2223
|
-
|
|
1418
|
+
`, this.durability);
|
|
2224
1419
|
}
|
|
2225
1420
|
/** Serializes an async operation against this file. */
|
|
2226
1421
|
synchronized(operation) {
|
|
@@ -2242,711 +1437,1137 @@ var JsonFileStore = class {
|
|
|
2242
1437
|
}
|
|
2243
1438
|
};
|
|
2244
1439
|
|
|
2245
|
-
// src/
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
1440
|
+
// src/memory/base.ts
|
|
1441
|
+
function defaultSearch(provider, text, options = {}) {
|
|
1442
|
+
return provider.retrieve({
|
|
1443
|
+
text,
|
|
1444
|
+
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
1445
|
+
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
function defaultMetadata(name, options = {}) {
|
|
1449
|
+
return {
|
|
1450
|
+
name,
|
|
1451
|
+
capabilities: options.capabilities ?? ["retrieve", "store", "search"],
|
|
1452
|
+
relevanceRanking: options.relevanceRanking ?? false,
|
|
1453
|
+
...options.backend !== void 0 ? { backend: options.backend } : {}
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// src/memory/local.ts
|
|
1458
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
1459
|
+
"the",
|
|
1460
|
+
"and",
|
|
1461
|
+
"for",
|
|
1462
|
+
"with",
|
|
1463
|
+
"this",
|
|
1464
|
+
"that",
|
|
1465
|
+
"from",
|
|
1466
|
+
"into",
|
|
1467
|
+
"was",
|
|
1468
|
+
"were",
|
|
1469
|
+
"have",
|
|
1470
|
+
"has",
|
|
1471
|
+
"had",
|
|
1472
|
+
"are",
|
|
1473
|
+
"but",
|
|
1474
|
+
"not",
|
|
1475
|
+
"you",
|
|
1476
|
+
"his",
|
|
1477
|
+
"her",
|
|
1478
|
+
"its",
|
|
1479
|
+
"their",
|
|
1480
|
+
"them",
|
|
1481
|
+
"then",
|
|
1482
|
+
"than",
|
|
1483
|
+
"when",
|
|
1484
|
+
"what",
|
|
1485
|
+
"which",
|
|
1486
|
+
"will",
|
|
1487
|
+
"would",
|
|
1488
|
+
"been",
|
|
1489
|
+
"were",
|
|
1490
|
+
"also",
|
|
1491
|
+
"because",
|
|
1492
|
+
"about",
|
|
1493
|
+
"after",
|
|
1494
|
+
"before"
|
|
1495
|
+
]);
|
|
1496
|
+
function keywords(text) {
|
|
1497
|
+
return new Set(
|
|
1498
|
+
(text ?? "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 4 && !STOPWORDS.has(t))
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1501
|
+
function relevanceScore(queryKeywords, entry2) {
|
|
1502
|
+
if (queryKeywords.size === 0) return 0;
|
|
1503
|
+
const entryKeywords = keywords(`${entry2.content} ${entry2.key}`);
|
|
1504
|
+
let overlap = 0;
|
|
1505
|
+
for (const keyword of queryKeywords) {
|
|
1506
|
+
if (entryKeywords.has(keyword)) overlap += 1;
|
|
1507
|
+
}
|
|
1508
|
+
let tagMatches = 0;
|
|
1509
|
+
for (const tag of entry2.tags ?? []) {
|
|
1510
|
+
if (queryKeywords.has(tag.toLowerCase())) tagMatches += 1;
|
|
1511
|
+
}
|
|
1512
|
+
return overlap * 2 + tagMatches * 1.5;
|
|
1513
|
+
}
|
|
1514
|
+
var LocalMemoryProvider = class extends JsonFileStore {
|
|
1515
|
+
name = "local";
|
|
1516
|
+
constructor(directory) {
|
|
1517
|
+
super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
|
|
1518
|
+
}
|
|
1519
|
+
metadata() {
|
|
1520
|
+
return defaultMetadata(this.name, {
|
|
1521
|
+
capabilities: ["retrieve", "store", "search", "categories", "tags"],
|
|
1522
|
+
relevanceRanking: true,
|
|
1523
|
+
backend: { kind: "json-file", deterministicScoring: true }
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
search(text, options = {}) {
|
|
1527
|
+
return defaultSearch(this, text, options);
|
|
1528
|
+
}
|
|
1529
|
+
/** @deprecated v1 alias of {@link retrieve}. */
|
|
1530
|
+
query(query) {
|
|
1531
|
+
return this.retrieve(query);
|
|
1532
|
+
}
|
|
1533
|
+
/** @deprecated v1 alias of {@link store}. */
|
|
1534
|
+
write(entry2) {
|
|
1535
|
+
return this.store(entry2);
|
|
1536
|
+
}
|
|
1537
|
+
async retrieve(query) {
|
|
1538
|
+
const entries = await this.readAll();
|
|
1539
|
+
const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter((entry2) => query.category ? entry2.category === query.category : true).filter(
|
|
1540
|
+
(entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
|
|
1541
|
+
);
|
|
1542
|
+
if (!query.text || query.text.trim().length === 0) {
|
|
1543
|
+
return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
|
|
1544
|
+
}
|
|
1545
|
+
const queryKeywords = keywords(query.text);
|
|
1546
|
+
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);
|
|
1547
|
+
}
|
|
1548
|
+
store(entry2) {
|
|
1549
|
+
return this.mutate((entries) => {
|
|
1550
|
+
const index = entries.findIndex((e) => e.key === entry2.key);
|
|
1551
|
+
const stamped = {
|
|
1552
|
+
...entry2,
|
|
1553
|
+
createdAt: entry2.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1554
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1555
|
+
};
|
|
1556
|
+
if (index >= 0) entries[index] = stamped;
|
|
1557
|
+
else entries.push(stamped);
|
|
1558
|
+
return [entries, void 0];
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
async count() {
|
|
1562
|
+
return (await this.readAll()).length;
|
|
2265
1563
|
}
|
|
2266
1564
|
};
|
|
2267
1565
|
|
|
2268
|
-
// src/
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
1566
|
+
// src/memory/remembera.ts
|
|
1567
|
+
var RememberaMemoryProvider = class {
|
|
1568
|
+
name = "remembera";
|
|
1569
|
+
baseUrl;
|
|
1570
|
+
apiKeyEnvVar;
|
|
1571
|
+
env;
|
|
1572
|
+
log;
|
|
1573
|
+
constructor(config, log = logger) {
|
|
1574
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
1575
|
+
this.apiKeyEnvVar = config.apiKeyEnvVar;
|
|
1576
|
+
this.env = config.env ?? process.env;
|
|
1577
|
+
this.log = log.child({ component: "remembera" });
|
|
1578
|
+
}
|
|
1579
|
+
get authenticationConfigured() {
|
|
1580
|
+
return this.apiKeyEnvVar === void 0 || Boolean(this.env[this.apiKeyEnvVar]);
|
|
1581
|
+
}
|
|
1582
|
+
headers() {
|
|
1583
|
+
const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
|
|
1584
|
+
return key ? { authorization: `Bearer ${key}` } : {};
|
|
1585
|
+
}
|
|
1586
|
+
/** Search delegates to the gateway's relevance query. */
|
|
1587
|
+
search(text, options = {}) {
|
|
1588
|
+
return defaultSearch(
|
|
1589
|
+
this,
|
|
1590
|
+
text,
|
|
1591
|
+
{
|
|
1592
|
+
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
1593
|
+
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
1594
|
+
}
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
metadata() {
|
|
1598
|
+
return defaultMetadata(this.name, {
|
|
1599
|
+
capabilities: ["retrieve", "store", "search", "categories", "tags"],
|
|
1600
|
+
relevanceRanking: true,
|
|
1601
|
+
backend: { kind: "http", baseUrl: this.baseUrl, authenticated: this.authenticationConfigured }
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
/** @deprecated v1 alias of {@link retrieve}. */
|
|
1605
|
+
query(query) {
|
|
1606
|
+
return this.retrieve(query);
|
|
1607
|
+
}
|
|
1608
|
+
/** @deprecated v1 alias of {@link store}. */
|
|
1609
|
+
write(entry2) {
|
|
1610
|
+
return this.store(entry2);
|
|
1611
|
+
}
|
|
1612
|
+
async retrieve(query) {
|
|
1613
|
+
const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
|
|
1614
|
+
if (status !== 200) {
|
|
1615
|
+
throw new MemoryError(`Remembera query returned HTTP ${status}.`);
|
|
1616
|
+
}
|
|
1617
|
+
const entries = body.entries;
|
|
1618
|
+
if (!Array.isArray(entries)) {
|
|
1619
|
+
throw new MemoryError("Remembera query response had no entries array.");
|
|
1620
|
+
}
|
|
1621
|
+
return entries.map((entry2) => this.toEntry(entry2)).filter((entry2) => entry2 !== void 0);
|
|
1622
|
+
}
|
|
1623
|
+
async store(entry2) {
|
|
1624
|
+
const { status } = await this.request(`${this.baseUrl}/api/memory`, entry2);
|
|
1625
|
+
if (status !== 200 && status !== 201) {
|
|
1626
|
+
throw new MemoryError(`Remembera write returned HTTP ${status}.`);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
async request(url, body) {
|
|
1630
|
+
const headers = this.headers();
|
|
1631
|
+
try {
|
|
1632
|
+
return await requestJson(url, {
|
|
1633
|
+
method: "POST",
|
|
1634
|
+
headers,
|
|
1635
|
+
body
|
|
1636
|
+
});
|
|
1637
|
+
} catch (error) {
|
|
1638
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1639
|
+
throw new MemoryError(`Remembera request to ${url} failed: ${message}`, { cause: error });
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
toEntry(raw) {
|
|
1643
|
+
if (raw === null || typeof raw !== "object") {
|
|
1644
|
+
this.log.warn("Skipping malformed Remembera memory entry.");
|
|
1645
|
+
return void 0;
|
|
1646
|
+
}
|
|
1647
|
+
const entry2 = raw;
|
|
1648
|
+
if (typeof entry2.key !== "string" || typeof entry2.content !== "string") {
|
|
1649
|
+
this.log.warn("Skipping Remembera memory entry without key/content.");
|
|
1650
|
+
return void 0;
|
|
1651
|
+
}
|
|
1652
|
+
const scope = entry2.scope;
|
|
1653
|
+
return {
|
|
1654
|
+
key: entry2.key,
|
|
1655
|
+
content: entry2.content,
|
|
1656
|
+
scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
|
|
1657
|
+
...Array.isArray(entry2.tags) ? { tags: entry2.tags.filter((t) => typeof t === "string") } : {},
|
|
1658
|
+
...typeof entry2.category === "string" ? { category: entry2.category } : {},
|
|
1659
|
+
...typeof entry2.createdAt === "string" ? { createdAt: entry2.createdAt } : {},
|
|
1660
|
+
...typeof entry2.updatedAt === "string" ? { updatedAt: entry2.updatedAt } : {}
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
2282
1663
|
};
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
1664
|
+
|
|
1665
|
+
// src/memory/factory.ts
|
|
1666
|
+
function createMemoryProvider(config) {
|
|
1667
|
+
if (!config || config.provider === "local") {
|
|
1668
|
+
return new LocalMemoryProvider();
|
|
1669
|
+
}
|
|
1670
|
+
if (config.provider === "remembera") {
|
|
1671
|
+
if (!config.baseUrl) {
|
|
1672
|
+
throw new ConfigError(
|
|
1673
|
+
"CONFIG_INVALID",
|
|
1674
|
+
`Memory provider "remembera" needs a base_url in omninode.yaml.`
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
return new RememberaMemoryProvider({
|
|
1678
|
+
baseUrl: config.baseUrl,
|
|
1679
|
+
apiKeyEnvVar: config.apiKeyEnvVar
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
throw new ConfigError(
|
|
1683
|
+
"CONFIG_INVALID",
|
|
1684
|
+
`Unknown memory provider "${config.provider}". Available: local, remembera.`
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// src/memory/service.ts
|
|
1689
|
+
var DEFAULT_ENTRY_LIMIT = 8;
|
|
1690
|
+
var MAX_CONTEXT_CHARS = 4e3;
|
|
1691
|
+
var MemoryService = class {
|
|
1692
|
+
constructor(provider, log = logger, options = {}) {
|
|
1693
|
+
this.provider = provider;
|
|
1694
|
+
this.log = log.child({ component: "memory", provider: provider.name });
|
|
1695
|
+
this.required = options.required ?? false;
|
|
1696
|
+
this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
|
|
1697
|
+
}
|
|
1698
|
+
provider;
|
|
1699
|
+
log;
|
|
1700
|
+
required;
|
|
1701
|
+
maxContextChars;
|
|
1702
|
+
get providerRef() {
|
|
1703
|
+
return this.provider;
|
|
2289
1704
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
get store() {
|
|
2293
|
-
return this.options.store;
|
|
1705
|
+
get providerName() {
|
|
1706
|
+
return this.provider.name;
|
|
2294
1707
|
}
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
);
|
|
1708
|
+
get isRequired() {
|
|
1709
|
+
return this.required;
|
|
1710
|
+
}
|
|
1711
|
+
/** Structured, budgeted context for a task (§12 — avoid loading all memory). */
|
|
1712
|
+
async contextFor(query) {
|
|
1713
|
+
let entries;
|
|
1714
|
+
try {
|
|
1715
|
+
entries = await this.provider.retrieve({
|
|
1716
|
+
text: query.objective,
|
|
1717
|
+
...query.role ? { tags: [query.role] } : {},
|
|
1718
|
+
limit: query.limit ?? DEFAULT_ENTRY_LIMIT
|
|
1719
|
+
});
|
|
1720
|
+
} catch (error) {
|
|
1721
|
+
return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
|
|
2310
1722
|
}
|
|
2311
|
-
|
|
2312
|
-
const task = {
|
|
2313
|
-
id: generateTaskId(),
|
|
2314
|
-
objective: input.objective.trim(),
|
|
2315
|
-
status: "created",
|
|
2316
|
-
attempt: 0,
|
|
2317
|
-
createdAt: now,
|
|
2318
|
-
updatedAt: now,
|
|
2319
|
-
...input.project !== void 0 ? { project: input.project } : {},
|
|
2320
|
-
...input.role !== void 0 ? { role: input.role } : {},
|
|
2321
|
-
...input.agent !== void 0 ? { agent: input.agent } : {},
|
|
2322
|
-
...input.context !== void 0 ? { context: input.context } : {}
|
|
2323
|
-
};
|
|
2324
|
-
await this.options.store.save(task);
|
|
2325
|
-
this.log.info(`Created task ${task.id}.`);
|
|
2326
|
-
await this.audit("task.created", task.id, { objective: task.objective, project: task.project });
|
|
2327
|
-
return task;
|
|
1723
|
+
return this.format(entries);
|
|
2328
1724
|
}
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
return this.
|
|
1725
|
+
/** Prompt-ready context text (v1 convenience wrapper). */
|
|
1726
|
+
async gatherContext(query) {
|
|
1727
|
+
return (await this.contextFor(query)).text;
|
|
2332
1728
|
}
|
|
2333
|
-
/**
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
|
|
2347
|
-
}
|
|
2348
|
-
const role = task.role ? this.options.roles.get(task.role) : void 0;
|
|
2349
|
-
this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
|
|
2350
|
-
let background = task.context?.background;
|
|
2351
|
-
if (this.options.memory) {
|
|
2352
|
-
const memoryContext = await this.options.memory.gatherContext({
|
|
2353
|
-
objective: task.objective,
|
|
2354
|
-
...task.role ? { role: task.role } : {},
|
|
2355
|
-
...task.project ? { project: task.project } : {}
|
|
1729
|
+
/** Records a finished task's outcome — task history plus "known problems" (§12). */
|
|
1730
|
+
async recordTaskOutcome(task) {
|
|
1731
|
+
const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
|
|
1732
|
+
try {
|
|
1733
|
+
await this.provider.store({
|
|
1734
|
+
key: `task:${task.id}`,
|
|
1735
|
+
scope: "task",
|
|
1736
|
+
category: "tasks",
|
|
1737
|
+
content: `Objective: ${task.objective}
|
|
1738
|
+
Status: ${task.status}
|
|
1739
|
+
Outcome: ${outcome}`,
|
|
1740
|
+
tags: ["task", ...task.agent ? [task.agent] : [], ...task.role ? [task.role] : []],
|
|
1741
|
+
createdAt: task.createdAt
|
|
2356
1742
|
});
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2368
|
-
current = await this.transition(current, "running", { attempt });
|
|
2369
|
-
let output;
|
|
2370
|
-
let thrown;
|
|
2371
|
-
try {
|
|
2372
|
-
output = await agent.run({
|
|
2373
|
-
taskId: current.id,
|
|
2374
|
-
objective: current.objective,
|
|
2375
|
-
...role ? { role } : {},
|
|
2376
|
-
...background !== void 0 ? { context: background } : {}
|
|
2377
|
-
});
|
|
2378
|
-
} catch (error) {
|
|
2379
|
-
thrown = error;
|
|
2380
|
-
}
|
|
2381
|
-
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2382
|
-
if (thrown !== void 0 || output === void 0) {
|
|
2383
|
-
const outcome = classifyThrown(thrown);
|
|
2384
|
-
const message = thrown instanceof Error ? thrown.message : String(thrown);
|
|
2385
|
-
const retryable = outcome === "failed" && RETRYABLE_ERROR_CODES.includes(errorCode(thrown)) && i < maxAttempts - 1;
|
|
2386
|
-
current = await this.transition(current, outcome, {
|
|
2387
|
-
attempt,
|
|
2388
|
-
lastError: message,
|
|
2389
|
-
execution: {
|
|
2390
|
-
executionId,
|
|
2391
|
-
attempt,
|
|
2392
|
-
startedAt,
|
|
2393
|
-
finishedAt,
|
|
2394
|
-
outcome,
|
|
2395
|
-
error: message
|
|
2396
|
-
}
|
|
1743
|
+
const significant = (task.result?.reports ?? []).flatMap(
|
|
1744
|
+
(report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
|
|
1745
|
+
);
|
|
1746
|
+
for (const { report, finding } of significant) {
|
|
1747
|
+
await this.provider.store({
|
|
1748
|
+
key: `finding:${report.id}:${finding.id}`,
|
|
1749
|
+
scope: "project",
|
|
1750
|
+
category: "problems",
|
|
1751
|
+
content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
|
|
1752
|
+
tags: ["known-problem", report.agent]
|
|
2397
1753
|
});
|
|
2398
|
-
this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
|
|
2399
|
-
if (retryable) {
|
|
2400
|
-
if (backoffMs > 0) await sleep(backoffMs * attempt);
|
|
2401
|
-
continue;
|
|
2402
|
-
}
|
|
2403
|
-
if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
|
|
2404
|
-
return current;
|
|
2405
|
-
}
|
|
2406
|
-
const currentState = await this.options.store.get(current.id);
|
|
2407
|
-
if (currentState?.status === "cancelled") {
|
|
2408
|
-
current = currentState;
|
|
2409
|
-
break;
|
|
2410
1754
|
}
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
2414
|
-
// Keep the verbatim output (bounded) for report extraction and audit.
|
|
2415
|
-
...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
|
|
2416
|
-
...output.error !== void 0 ? { error: output.error } : {},
|
|
2417
|
-
finishedAt
|
|
2418
|
-
};
|
|
2419
|
-
const finalStatus = output.status === "completed" ? "completed" : "failed";
|
|
2420
|
-
current = await this.transition(current, finalStatus, {
|
|
2421
|
-
attempt,
|
|
2422
|
-
result,
|
|
2423
|
-
execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
|
|
2424
|
-
});
|
|
2425
|
-
this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
|
|
2426
|
-
break;
|
|
2427
|
-
}
|
|
2428
|
-
if (this.options.memory) {
|
|
2429
|
-
await this.options.memory.recordTaskOutcome(current);
|
|
1755
|
+
} catch (error) {
|
|
1756
|
+
this.handleFailure("store", error, void 0);
|
|
2430
1757
|
}
|
|
2431
|
-
return current;
|
|
2432
1758
|
}
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
1759
|
+
format(entries) {
|
|
1760
|
+
if (entries.length === 0) return { text: "", entries, truncated: false };
|
|
1761
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
1762
|
+
for (const entry2 of entries) {
|
|
1763
|
+
byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
|
|
2437
1764
|
}
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
1765
|
+
const parts = [];
|
|
1766
|
+
for (const scope of ["project", "task", "role", "knowledge"]) {
|
|
1767
|
+
const scopeEntries = byScope.get(scope) ?? [];
|
|
1768
|
+
if (scopeEntries.length === 0) continue;
|
|
1769
|
+
const lines = scopeEntries.map((entry2) => {
|
|
1770
|
+
const category = entry2.category ? `[${entry2.category}] ` : "";
|
|
1771
|
+
return `- ${category}${entry2.content}`;
|
|
1772
|
+
}).join("\n");
|
|
1773
|
+
parts.push(`### Relevant memory (${scope})
|
|
1774
|
+
${lines}`);
|
|
2441
1775
|
}
|
|
2442
|
-
|
|
1776
|
+
let text = parts.join("\n\n");
|
|
1777
|
+
const truncated = text.length > this.maxContextChars;
|
|
1778
|
+
if (truncated) text = `${text.slice(0, this.maxContextChars - 1)}\u2026`;
|
|
1779
|
+
return { text, entries, truncated };
|
|
2443
1780
|
}
|
|
2444
|
-
/**
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
`
|
|
1781
|
+
/**
|
|
1782
|
+
* Failure policy (§12): continue unless memory is explicitly required.
|
|
1783
|
+
*/
|
|
1784
|
+
handleFailure(operation, error, fallback) {
|
|
1785
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1786
|
+
if (this.required) {
|
|
1787
|
+
throw new MemoryError(
|
|
1788
|
+
`Memory is marked required, but ${operation} failed: ${message}`,
|
|
1789
|
+
{ cause: error }
|
|
2452
1790
|
);
|
|
2453
1791
|
}
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
1792
|
+
this.log.warn(`Memory ${operation} failed (continuing without memory): ${message}`);
|
|
1793
|
+
return fallback ?? { text: "", entries: [], truncated: false };
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1797
|
+
// src/pipelines/store.ts
|
|
1798
|
+
var FilePipelineRunStore = class extends JsonFileStore {
|
|
1799
|
+
constructor(directory) {
|
|
1800
|
+
super({ ...directory !== void 0 ? { directory } : {}, fileName: "pipelines.json" });
|
|
1801
|
+
}
|
|
1802
|
+
save(run2) {
|
|
1803
|
+
return this.mutate((runs) => {
|
|
1804
|
+
const index = runs.findIndex((r) => r.id === run2.id);
|
|
1805
|
+
if (index >= 0) runs[index] = run2;
|
|
1806
|
+
else runs.push(run2);
|
|
1807
|
+
return [runs, void 0];
|
|
1808
|
+
});
|
|
2464
1809
|
}
|
|
2465
1810
|
async get(id) {
|
|
2466
|
-
return this.
|
|
1811
|
+
return (await this.readAll()).find((r) => r.id === id);
|
|
2467
1812
|
}
|
|
2468
|
-
async list(filter) {
|
|
2469
|
-
return this.
|
|
1813
|
+
async list(filter = {}) {
|
|
1814
|
+
return (await this.readAll()).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 ?? ""));
|
|
2470
1815
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
1816
|
+
};
|
|
1817
|
+
|
|
1818
|
+
// src/tasks/store.ts
|
|
1819
|
+
var FileTaskStore = class extends JsonFileStore {
|
|
1820
|
+
constructor(directory) {
|
|
1821
|
+
super({ ...directory !== void 0 ? { directory } : {}, fileName: "tasks.json" });
|
|
2477
1822
|
}
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
const updated = {
|
|
2485
|
-
...task,
|
|
2486
|
-
status: to,
|
|
2487
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2488
|
-
...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
|
|
2489
|
-
...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
|
|
2490
|
-
...executions !== void 0 ? { executions } : {},
|
|
2491
|
-
...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
|
|
2492
|
-
};
|
|
2493
|
-
await this.options.store.save(updated);
|
|
2494
|
-
await this.audit(`task.${to === "queued" ? "queued" : to}`, updated.id, {
|
|
2495
|
-
from: task.status
|
|
1823
|
+
save(task) {
|
|
1824
|
+
return this.mutate((tasks) => {
|
|
1825
|
+
const index = tasks.findIndex((t) => t.id === task.id);
|
|
1826
|
+
if (index >= 0) tasks[index] = task;
|
|
1827
|
+
else tasks.push(task);
|
|
1828
|
+
return [tasks, void 0];
|
|
2496
1829
|
});
|
|
2497
|
-
return updated;
|
|
2498
1830
|
}
|
|
2499
|
-
async
|
|
2500
|
-
await this.
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
1831
|
+
async get(id) {
|
|
1832
|
+
const tasks = await this.readAll();
|
|
1833
|
+
return tasks.find((t) => t.id === id);
|
|
1834
|
+
}
|
|
1835
|
+
async list(filter = {}) {
|
|
1836
|
+
const tasks = await this.readAll();
|
|
1837
|
+
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));
|
|
2506
1838
|
}
|
|
2507
1839
|
};
|
|
2508
|
-
|
|
2509
|
-
|
|
1840
|
+
|
|
1841
|
+
// src/cli/commands/inspect.ts
|
|
1842
|
+
function emit(json, payload, render) {
|
|
1843
|
+
if (json) {
|
|
1844
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
render();
|
|
1848
|
+
}
|
|
1849
|
+
function jsonOption(command) {
|
|
1850
|
+
return command.option("--json", "Emit the raw record as JSON.");
|
|
1851
|
+
}
|
|
1852
|
+
function registerTaskInspect(taskGroup) {
|
|
1853
|
+
jsonOption(
|
|
1854
|
+
taskGroup.command("inspect <id>").description("Show a task with its execution history, result and report references.").action(async (id, options) => {
|
|
1855
|
+
const tasks = await new FileTaskStore().list();
|
|
1856
|
+
const task = tasks.find((t) => t.id === id);
|
|
1857
|
+
if (!task) throw new OmniNodeError("TASK_NOT_FOUND", `No task "${id}" in this project's store.`);
|
|
1858
|
+
emit(options.json ?? false, task, () => {
|
|
1859
|
+
console.log(`Task ${task.id}`);
|
|
1860
|
+
console.log(` status: ${task.status}${task.attempt > 0 ? ` (attempt ${task.attempt})` : ""}`);
|
|
1861
|
+
if (task.project) console.log(` project: ${task.project}`);
|
|
1862
|
+
if (task.pipelineId) console.log(` pipeline: ${task.pipelineId}`);
|
|
1863
|
+
console.log(` objective: ${task.objective}`);
|
|
1864
|
+
if (task.role) console.log(` role: ${task.role}`);
|
|
1865
|
+
if (task.agent) console.log(` agent: ${task.agent}`);
|
|
1866
|
+
console.log(` created: ${task.createdAt}`);
|
|
1867
|
+
console.log(` updated: ${task.updatedAt}`);
|
|
1868
|
+
if (task.lastError) console.log(` lastError: ${task.lastError}`);
|
|
1869
|
+
if (task.executions && task.executions.length > 0) {
|
|
1870
|
+
console.log(" executions:");
|
|
1871
|
+
for (const execution of task.executions) {
|
|
1872
|
+
console.log(
|
|
1873
|
+
` #${execution.attempt} ${execution.outcome} (${execution.executionId})${execution.error ? ` \u2014 ${execution.error}` : ""}`
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
if (task.result?.summary) console.log(` summary: ${task.result.summary}`);
|
|
1878
|
+
if (task.result?.reports && task.result.reports.length > 0) {
|
|
1879
|
+
console.log(` reports: ${task.result.reports.length}`);
|
|
1880
|
+
for (const report of task.result.reports) {
|
|
1881
|
+
console.log(` - [${report.agent}] ${report.summary}`);
|
|
1882
|
+
for (const finding of report.findings) {
|
|
1883
|
+
console.log(` \xB7 [${finding.severity ?? "info"}] ${finding.title}`);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
})
|
|
1889
|
+
);
|
|
2510
1890
|
}
|
|
2511
|
-
function
|
|
2512
|
-
|
|
1891
|
+
function registerPipelineInspect(pipelineGroup) {
|
|
1892
|
+
jsonOption(
|
|
1893
|
+
pipelineGroup.command("inspect <run-id>").description("Show a pipeline run with per-step status and artifact references.").action(async (runId, options) => {
|
|
1894
|
+
const run2 = await new FilePipelineRunStore().get(runId);
|
|
1895
|
+
if (!run2) throw new OmniNodeError("PIPELINE_NOT_FOUND", `No pipeline run "${runId}" in this project's store.`);
|
|
1896
|
+
emit(options.json ?? false, run2, () => {
|
|
1897
|
+
console.log(`Pipeline run ${run2.id} (${run2.pipelineId})`);
|
|
1898
|
+
console.log(` status: ${run2.status}${run2.attempt > 1 ? ` (attempt ${run2.attempt})` : ""}`);
|
|
1899
|
+
if (run2.objective) console.log(` objective: ${run2.objective}`);
|
|
1900
|
+
console.log(` started: ${run2.startedAt ?? "?"}`);
|
|
1901
|
+
if (run2.finishedAt) console.log(` finished: ${run2.finishedAt}`);
|
|
1902
|
+
if (run2.planId) console.log(` plan: ${run2.planId}`);
|
|
1903
|
+
if (run2.combinedReportId) console.log(` combined: ${run2.combinedReportId}`);
|
|
1904
|
+
if (run2.resultSummary) console.log(` result: ${run2.resultSummary}`);
|
|
1905
|
+
console.log(" steps:");
|
|
1906
|
+
for (const step of run2.stepRuns) {
|
|
1907
|
+
const stepTaskIds = step.taskIds ?? [];
|
|
1908
|
+
const tasks = stepTaskIds.length > 0 ? ` tasks=${stepTaskIds.join(",")}` : "";
|
|
1909
|
+
console.log(` ${step.stepId.padEnd(14)} ${step.status}${tasks}${step.error ? ` \u2014 ${step.error}` : ""}`);
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
})
|
|
1913
|
+
);
|
|
2513
1914
|
}
|
|
2514
|
-
function
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
1915
|
+
function registerAgentInspect(agentGroup) {
|
|
1916
|
+
jsonOption(
|
|
1917
|
+
agentGroup.command("inspect <name>").description("Show an agent's effective configuration and recent tasks.").action(async (name, options) => {
|
|
1918
|
+
const config = loadConfig();
|
|
1919
|
+
const agentConfig = config.project.agents.find((a) => a.name === name);
|
|
1920
|
+
if (!agentConfig) throw new OmniNodeError("AGENT_NOT_FOUND", `No agent "${name}" is configured.`);
|
|
1921
|
+
const registry = buildAgentRegistry(config);
|
|
1922
|
+
const agent = registry.get(name);
|
|
1923
|
+
const recent = (await new FileTaskStore().list()).filter((task) => task.agent === name).slice(-5).map((task) => ({ id: task.id, status: task.status, updatedAt: task.updatedAt }));
|
|
1924
|
+
const payload = {
|
|
1925
|
+
config: agentConfig,
|
|
1926
|
+
effective: agent?.info,
|
|
1927
|
+
envPolicy: resolveEnvPolicy(agentConfig),
|
|
1928
|
+
recentTasks: recent
|
|
1929
|
+
};
|
|
1930
|
+
emit(options.json ?? false, payload, () => {
|
|
1931
|
+
console.log(`Agent ${name}`);
|
|
1932
|
+
console.log(` integration: ${agentConfig.integration}`);
|
|
1933
|
+
console.log(` command: ${agentConfig.command ?? "(none)"}`);
|
|
1934
|
+
console.log(` input mode: ${agentConfig.inputMode ?? "stdin"}`);
|
|
1935
|
+
console.log(` env policy: ${resolveEnvPolicy(agentConfig)}`);
|
|
1936
|
+
if (agentConfig.cwd) console.log(` working dir: ${agentConfig.cwd}`);
|
|
1937
|
+
if (agentConfig.timeoutMs) console.log(` timeout: ${agentConfig.timeoutMs}ms`);
|
|
1938
|
+
if (agentConfig.maxOutputBytes) console.log(` max output: ${agentConfig.maxOutputBytes} bytes/stream`);
|
|
1939
|
+
if (agentConfig.allowExternalCwd) console.log(` external cwd allowed`);
|
|
1940
|
+
console.log(` recent tasks: ${recent.map((t) => `${t.id}:${t.status}`).join(", ") || "(none)"}`);
|
|
1941
|
+
});
|
|
1942
|
+
})
|
|
1943
|
+
);
|
|
2520
1944
|
}
|
|
2521
|
-
function
|
|
2522
|
-
|
|
1945
|
+
function registerProviderInspect(providerGroup) {
|
|
1946
|
+
jsonOption(
|
|
1947
|
+
providerGroup.command("inspect <name>").description("Show a provider's authentication status and capabilities.").action(async (name, options) => {
|
|
1948
|
+
const config = loadConfig();
|
|
1949
|
+
const providerConfig = config.project.providers.find((p) => p.name === name);
|
|
1950
|
+
if (!providerConfig) throw new OmniNodeError("PROVIDER_NOT_FOUND", `No provider "${name}" is configured.`);
|
|
1951
|
+
const { createProvider: createProvider2 } = await import("./providers-OYXBAQTX.js");
|
|
1952
|
+
const provider = createProvider2(providerConfig);
|
|
1953
|
+
const payload = {
|
|
1954
|
+
config: providerConfig,
|
|
1955
|
+
providerId: provider.providerId,
|
|
1956
|
+
authentication: provider.authentication,
|
|
1957
|
+
capabilities: providerConfig.capabilities ?? [],
|
|
1958
|
+
memoryConfigured: config.project.memory ? { provider: config.project.memory.provider, required: config.project.memory.required ?? false } : void 0,
|
|
1959
|
+
memoryEntries: config.project.memory ? await new MemoryService(createMemoryProvider(config.project.memory)).providerRef.query({ limit: 1e4 }).then((e) => e.length) : void 0,
|
|
1960
|
+
roles: buildRoleRegistry(config).list().length
|
|
1961
|
+
};
|
|
1962
|
+
emit(options.json ?? false, payload, () => {
|
|
1963
|
+
console.log(`Provider ${name}`);
|
|
1964
|
+
console.log(` providerId: ${provider.providerId}`);
|
|
1965
|
+
console.log(` type: ${providerConfig.type}`);
|
|
1966
|
+
console.log(` base URL: ${providerConfig.baseUrl}`);
|
|
1967
|
+
console.log(` auth: ${provider.authentication.method}${provider.authentication.envVar ? ` ($${provider.authentication.envVar})` : ""}`);
|
|
1968
|
+
console.log(` credentials: ${provider.authentication.configured ? "configured" : "MISSING"}`);
|
|
1969
|
+
if (providerConfig.capabilities?.length) {
|
|
1970
|
+
console.log(` capabilities: ${providerConfig.capabilities.join(", ")}`);
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
})
|
|
1974
|
+
);
|
|
2523
1975
|
}
|
|
2524
1976
|
|
|
2525
|
-
// src/
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
}
|
|
2535
|
-
async record(event) {
|
|
2536
|
-
try {
|
|
2537
|
-
await mkdir2(dirname2(this.filePath), { recursive: true });
|
|
2538
|
-
const { open: open2 } = await import("fs/promises");
|
|
2539
|
-
const handle = await open2(this.filePath, "a");
|
|
2540
|
-
try {
|
|
2541
|
-
await handle.appendFile(`${JSON.stringify(event)}
|
|
2542
|
-
`, "utf8");
|
|
2543
|
-
await handle.sync();
|
|
2544
|
-
} finally {
|
|
2545
|
-
await handle.close();
|
|
2546
|
-
}
|
|
2547
|
-
} catch (error) {
|
|
2548
|
-
this.log.warn(
|
|
2549
|
-
`Audit write failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1977
|
+
// src/cli/commands/providers.ts
|
|
1978
|
+
function registerProviderCommands(program) {
|
|
1979
|
+
const provider = program.command("provider").description("Manage AI providers configured in omninode.yaml.");
|
|
1980
|
+
provider.command("list").description("List configured providers.").action(async () => {
|
|
1981
|
+
const config = loadConfig();
|
|
1982
|
+
const providers = config.project.providers;
|
|
1983
|
+
if (providers.length === 0) {
|
|
1984
|
+
console.log(
|
|
1985
|
+
"No providers configured. Add one with `omninode provider add` or edit omninode.yaml."
|
|
2550
1986
|
);
|
|
1987
|
+
return;
|
|
2551
1988
|
}
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
} catch {
|
|
2559
|
-
return [];
|
|
1989
|
+
for (const provider2 of providers) {
|
|
1990
|
+
const key = provider2.apiKeyEnvVar ? `$${provider2.apiKeyEnvVar}` : "none";
|
|
1991
|
+
const enabled = provider2.enabled === false ? " (disabled)" : "";
|
|
1992
|
+
console.log(
|
|
1993
|
+
`${provider2.name} type=${provider2.type} base_url=${provider2.baseUrl} key=${key}${enabled}`
|
|
1994
|
+
);
|
|
2560
1995
|
}
|
|
2561
|
-
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => {
|
|
2562
|
-
try {
|
|
2563
|
-
return JSON.parse(line);
|
|
2564
|
-
} catch {
|
|
2565
|
-
return void 0;
|
|
2566
|
-
}
|
|
2567
|
-
}).filter((event) => event !== void 0);
|
|
2568
|
-
return events.slice(-limit);
|
|
2569
|
-
}
|
|
2570
|
-
};
|
|
2571
|
-
|
|
2572
|
-
// src/memory/base.ts
|
|
2573
|
-
function defaultSearch(provider, text, options = {}) {
|
|
2574
|
-
return provider.retrieve({
|
|
2575
|
-
text,
|
|
2576
|
-
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
2577
|
-
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
2578
1996
|
});
|
|
1997
|
+
provider.command("add").description(
|
|
1998
|
+
"Add a provider to omninode.yaml. OpenAI-compatible gateways (including OpenRouter and local runtimes) are supported today; OmniHilbras gets its own adapter in Phase 2."
|
|
1999
|
+
).requiredOption("--name <name>", "Unique provider name used in configuration and the model registry.").requiredOption("--base-url <url>", "Provider base URL, e.g. https://example.com/v1").option(
|
|
2000
|
+
"--type <type>",
|
|
2001
|
+
"Provider type: openai-compatible, omnihilbras, openrouter, local or custom.",
|
|
2002
|
+
"openai-compatible"
|
|
2003
|
+
).option(
|
|
2004
|
+
"--api-key-env-var <envVar>",
|
|
2005
|
+
"Environment variable holding the API key. Keys are never written to the config file."
|
|
2006
|
+
).action(async (options) => addProvider(options));
|
|
2007
|
+
provider.command("test <name>").description("Test connectivity, authentication and model discovery for a configured provider.").option(
|
|
2008
|
+
"--connect",
|
|
2009
|
+
"Run the full provider connect flow (authenticate, fetch, validate, register) where the adapter supports it."
|
|
2010
|
+
).action(async (name, options) => testProvider(name, options));
|
|
2011
|
+
provider.command("remove <name>").description("Remove a provider (planned for Phase 1 follow-up).").action(() => {
|
|
2012
|
+
throw notImplemented("omninode provider remove", "a Phase 1 follow-up");
|
|
2013
|
+
});
|
|
2014
|
+
registerProviderInspect(provider);
|
|
2579
2015
|
}
|
|
2580
|
-
function
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
relevanceRanking: options.relevanceRanking ?? false,
|
|
2585
|
-
...options.backend !== void 0 ? { backend: options.backend } : {}
|
|
2586
|
-
};
|
|
2587
|
-
}
|
|
2588
|
-
|
|
2589
|
-
// src/memory/local.ts
|
|
2590
|
-
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
2591
|
-
"the",
|
|
2592
|
-
"and",
|
|
2593
|
-
"for",
|
|
2594
|
-
"with",
|
|
2595
|
-
"this",
|
|
2596
|
-
"that",
|
|
2597
|
-
"from",
|
|
2598
|
-
"into",
|
|
2599
|
-
"was",
|
|
2600
|
-
"were",
|
|
2601
|
-
"have",
|
|
2602
|
-
"has",
|
|
2603
|
-
"had",
|
|
2604
|
-
"are",
|
|
2605
|
-
"but",
|
|
2606
|
-
"not",
|
|
2607
|
-
"you",
|
|
2608
|
-
"his",
|
|
2609
|
-
"her",
|
|
2610
|
-
"its",
|
|
2611
|
-
"their",
|
|
2612
|
-
"them",
|
|
2613
|
-
"then",
|
|
2614
|
-
"than",
|
|
2615
|
-
"when",
|
|
2616
|
-
"what",
|
|
2617
|
-
"which",
|
|
2618
|
-
"will",
|
|
2619
|
-
"would",
|
|
2620
|
-
"been",
|
|
2621
|
-
"were",
|
|
2622
|
-
"also",
|
|
2623
|
-
"because",
|
|
2624
|
-
"about",
|
|
2625
|
-
"after",
|
|
2626
|
-
"before"
|
|
2627
|
-
]);
|
|
2628
|
-
function keywords(text) {
|
|
2629
|
-
return new Set(
|
|
2630
|
-
(text ?? "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 4 && !STOPWORDS.has(t))
|
|
2631
|
-
);
|
|
2632
|
-
}
|
|
2633
|
-
function relevanceScore(queryKeywords, entry2) {
|
|
2634
|
-
if (queryKeywords.size === 0) return 0;
|
|
2635
|
-
const entryKeywords = keywords(`${entry2.content} ${entry2.key}`);
|
|
2636
|
-
let overlap = 0;
|
|
2637
|
-
for (const keyword of queryKeywords) {
|
|
2638
|
-
if (entryKeywords.has(keyword)) overlap += 1;
|
|
2639
|
-
}
|
|
2640
|
-
let tagMatches = 0;
|
|
2641
|
-
for (const tag of entry2.tags ?? []) {
|
|
2642
|
-
if (queryKeywords.has(tag.toLowerCase())) tagMatches += 1;
|
|
2643
|
-
}
|
|
2644
|
-
return overlap * 2 + tagMatches * 1.5;
|
|
2645
|
-
}
|
|
2646
|
-
var LocalMemoryProvider = class extends JsonFileStore {
|
|
2647
|
-
name = "local";
|
|
2648
|
-
constructor(directory) {
|
|
2649
|
-
super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
|
|
2650
|
-
}
|
|
2651
|
-
metadata() {
|
|
2652
|
-
return defaultMetadata(this.name, {
|
|
2653
|
-
capabilities: ["retrieve", "store", "search", "categories", "tags"],
|
|
2654
|
-
relevanceRanking: true,
|
|
2655
|
-
backend: { kind: "json-file", deterministicScoring: true }
|
|
2656
|
-
});
|
|
2657
|
-
}
|
|
2658
|
-
search(text, options = {}) {
|
|
2659
|
-
return defaultSearch(this, text, options);
|
|
2016
|
+
async function addProvider(options) {
|
|
2017
|
+
const configPath = findConfigFile();
|
|
2018
|
+
if (!configPath) {
|
|
2019
|
+
throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
|
|
2660
2020
|
}
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2021
|
+
const candidate = providerConfigSchema.safeParse({
|
|
2022
|
+
name: options.name,
|
|
2023
|
+
type: options.type,
|
|
2024
|
+
base_url: options.baseUrl,
|
|
2025
|
+
...options.apiKeyEnvVar !== void 0 ? { api_key_env_var: options.apiKeyEnvVar } : {}
|
|
2026
|
+
});
|
|
2027
|
+
if (!candidate.success) {
|
|
2028
|
+
const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
2029
|
+
throw new OmniNodeError("CLI_USAGE", `Invalid provider options: ${issues}`);
|
|
2664
2030
|
}
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2031
|
+
const newProvider = candidate.data;
|
|
2032
|
+
const existing = loadConfig();
|
|
2033
|
+
if (existing.project.providers.some((p) => p.name === newProvider.name)) {
|
|
2034
|
+
throw new OmniNodeError(
|
|
2035
|
+
"CLI_USAGE",
|
|
2036
|
+
`Provider "${newProvider.name}" already exists in ${findConfigFile() ?? "omninode.yaml"}.`
|
|
2037
|
+
);
|
|
2668
2038
|
}
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2039
|
+
const writtenPath = appendToConfigList("providers", newProvider);
|
|
2040
|
+
console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${writtenPath}.`);
|
|
2041
|
+
if (newProvider.api_key_env_var) {
|
|
2042
|
+
console.log(
|
|
2043
|
+
`Set $${newProvider.api_key_env_var} in your environment, then run: omninode provider test ${newProvider.name}`
|
|
2673
2044
|
);
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
}
|
|
2677
|
-
const queryKeywords = keywords(query.text);
|
|
2678
|
-
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);
|
|
2045
|
+
} else {
|
|
2046
|
+
console.log(`Run \`omninode provider test ${newProvider.name}\` to verify connectivity.`);
|
|
2679
2047
|
}
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
return [entries, void 0];
|
|
2691
|
-
});
|
|
2048
|
+
}
|
|
2049
|
+
async function testProvider(name, options = {}) {
|
|
2050
|
+
const config = loadConfig();
|
|
2051
|
+
const providerConfig = config.project.providers.find((p) => p.name === name);
|
|
2052
|
+
if (!providerConfig) {
|
|
2053
|
+
const available = config.project.providers.map((p) => p.name).join(", ") || "(none)";
|
|
2054
|
+
throw new ProviderError(
|
|
2055
|
+
"PROVIDER_NOT_FOUND",
|
|
2056
|
+
`Provider "${name}" is not configured. Configured providers: ${available}.`
|
|
2057
|
+
);
|
|
2692
2058
|
}
|
|
2693
|
-
|
|
2694
|
-
|
|
2059
|
+
const provider = createProvider(providerConfig);
|
|
2060
|
+
console.log(`Testing provider "${name}" (${providerConfig.type}) at ${provider.config.baseUrl}`);
|
|
2061
|
+
if (options.connect && provider instanceof OmniHilbrasProvider) {
|
|
2062
|
+
const registry = new ModelRegistry();
|
|
2063
|
+
try {
|
|
2064
|
+
const result = await provider.connectAndRegister(registry);
|
|
2065
|
+
printProviderStatus(result.status);
|
|
2066
|
+
console.log(` registered: ${result.registered}`);
|
|
2067
|
+
} catch (error) {
|
|
2068
|
+
console.log(` connect failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2069
|
+
process.exitCode = 1;
|
|
2070
|
+
}
|
|
2071
|
+
return;
|
|
2695
2072
|
}
|
|
2696
|
-
|
|
2073
|
+
const health = await provider.healthCheck();
|
|
2074
|
+
printProviderStatus(health);
|
|
2075
|
+
if (!health.connected) process.exitCode = 1;
|
|
2076
|
+
}
|
|
2077
|
+
function printProviderStatus(status) {
|
|
2078
|
+
console.log(` health: ${status.health}`);
|
|
2079
|
+
console.log(` connected: ${status.connected}`);
|
|
2080
|
+
console.log(` models: ${status.modelCount}`);
|
|
2081
|
+
if (status.message) console.log(` detail: ${status.message}`);
|
|
2082
|
+
}
|
|
2697
2083
|
|
|
2698
|
-
// src/
|
|
2699
|
-
var
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
const
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
/** Search delegates to the gateway's relevance query. */
|
|
2719
|
-
search(text, options = {}) {
|
|
2720
|
-
return defaultSearch(
|
|
2721
|
-
this,
|
|
2722
|
-
text,
|
|
2723
|
-
{
|
|
2724
|
-
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
2725
|
-
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
2726
|
-
}
|
|
2084
|
+
// src/cli/commands/models.ts
|
|
2085
|
+
var CAPABILITIES = [
|
|
2086
|
+
"chat",
|
|
2087
|
+
"coding",
|
|
2088
|
+
"tools",
|
|
2089
|
+
"vision",
|
|
2090
|
+
"embeddings"
|
|
2091
|
+
];
|
|
2092
|
+
function registerModelsCommand(program) {
|
|
2093
|
+
program.command("models").description("Discover and list models from configured providers (model registry, \xA78).").argument("[provider]", "Only discover from this provider.").option("-c, --capability <capability>", `Filter by capability: ${CAPABILITIES.join(", ")}.`).action(async (providerName, options) => {
|
|
2094
|
+
const capability = options.capability;
|
|
2095
|
+
if (capability !== void 0 && !CAPABILITIES.includes(capability)) {
|
|
2096
|
+
throw new OmniNodeError(
|
|
2097
|
+
"CLI_USAGE",
|
|
2098
|
+
`Unknown capability "${options.capability}". Valid capabilities: ${CAPABILITIES.join(", ")}.`
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
const config = loadConfig();
|
|
2102
|
+
const selected = config.project.providers.filter(
|
|
2103
|
+
(p) => providerName ? p.name === providerName : true
|
|
2727
2104
|
);
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
backend: { kind: "http", baseUrl: this.baseUrl, authenticated: this.authenticationConfigured }
|
|
2734
|
-
});
|
|
2735
|
-
}
|
|
2736
|
-
/** @deprecated v1 alias of {@link retrieve}. */
|
|
2737
|
-
query(query) {
|
|
2738
|
-
return this.retrieve(query);
|
|
2739
|
-
}
|
|
2740
|
-
/** @deprecated v1 alias of {@link store}. */
|
|
2741
|
-
write(entry2) {
|
|
2742
|
-
return this.store(entry2);
|
|
2743
|
-
}
|
|
2744
|
-
async retrieve(query) {
|
|
2745
|
-
const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
|
|
2746
|
-
if (status !== 200) {
|
|
2747
|
-
throw new MemoryError(`Remembera query returned HTTP ${status}.`);
|
|
2105
|
+
if (selected.length === 0) {
|
|
2106
|
+
throw new OmniNodeError(
|
|
2107
|
+
"PROVIDER_NOT_FOUND",
|
|
2108
|
+
providerName ? `Provider "${providerName}" is not configured.` : "No providers configured. Add one with `omninode provider add`."
|
|
2109
|
+
);
|
|
2748
2110
|
}
|
|
2749
|
-
const
|
|
2750
|
-
|
|
2751
|
-
|
|
2111
|
+
const registry = new ModelRegistry();
|
|
2112
|
+
for (const providerConfig of selected) {
|
|
2113
|
+
try {
|
|
2114
|
+
const provider = createProvider(providerConfig);
|
|
2115
|
+
const discovered = await registry.discoverFrom(provider);
|
|
2116
|
+
console.log(`${providerConfig.name}: ${discovered.length} model(s)`);
|
|
2117
|
+
} catch (error) {
|
|
2118
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2119
|
+
console.log(`${providerConfig.name}: discovery failed \u2014 ${message}`);
|
|
2120
|
+
}
|
|
2752
2121
|
}
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
if (
|
|
2758
|
-
|
|
2122
|
+
const models = registry.list({
|
|
2123
|
+
...providerName !== void 0 ? { provider: providerName } : {},
|
|
2124
|
+
...capability !== void 0 ? { capability } : {}
|
|
2125
|
+
});
|
|
2126
|
+
if (models.length === 0) {
|
|
2127
|
+
console.log("No models found.");
|
|
2128
|
+
return;
|
|
2759
2129
|
}
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
try {
|
|
2764
|
-
return await requestJson(url, {
|
|
2765
|
-
method: "POST",
|
|
2766
|
-
headers,
|
|
2767
|
-
body
|
|
2768
|
-
});
|
|
2769
|
-
} catch (error) {
|
|
2770
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2771
|
-
throw new MemoryError(`Remembera request to ${url} failed: ${message}`, { cause: error });
|
|
2130
|
+
console.log("");
|
|
2131
|
+
for (const model of models) {
|
|
2132
|
+
console.log(`${model.provider}:${model.id}`);
|
|
2772
2133
|
}
|
|
2773
|
-
}
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// src/cli/commands/agents.ts
|
|
2138
|
+
var collectArgs = (value, previous) => [...previous, value];
|
|
2139
|
+
function registerAgentCommands(program) {
|
|
2140
|
+
const agent = program.command("agent").description("Manage CLI agents registered with OmniNode.");
|
|
2141
|
+
agent.command("list").description("List configured agents.").action(async () => {
|
|
2142
|
+
const config = loadConfig();
|
|
2143
|
+
const agents = config.project.agents;
|
|
2144
|
+
if (agents.length === 0) {
|
|
2145
|
+
console.log("No agents configured. Add one with `omninode agent add` or edit omninode.yaml.");
|
|
2146
|
+
return;
|
|
2778
2147
|
}
|
|
2779
|
-
const
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2148
|
+
for (const agent2 of agents) {
|
|
2149
|
+
const command = agent2.command ?? "(no command)";
|
|
2150
|
+
console.log(
|
|
2151
|
+
`${agent2.name} integration=${agent2.integration} command=${command} input=${agent2.inputMode ?? "stdin"}`
|
|
2152
|
+
);
|
|
2783
2153
|
}
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2154
|
+
});
|
|
2155
|
+
agent.command("add").description(
|
|
2156
|
+
"Register a CLI agent in omninode.yaml. Agents run as child processes (stdin/stdout); native adapters come in a later phase."
|
|
2157
|
+
).requiredOption("--name <name>", "Unique agent name.").requiredOption("--command <command>", "Executable command, e.g. opencode").option("--arg <value>", "Additional command argument (repeatable).", collectArgs, []).option("--cwd <dir>", "Working directory for the agent process.").option(
|
|
2158
|
+
"--input-mode <mode>",
|
|
2159
|
+
"How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
|
|
2160
|
+
"stdin"
|
|
2161
|
+
).option("--env-policy <policy>", "Environment policy: inherit, allowlist, denylist or explicit.").option("--allow-env <name>", "Variable to forward under the allowlist policy (repeatable).", collectArgs).option("--deny-env <name>", "Variable to remove under the denylist policy (repeatable).", collectArgs).option("--max-output-bytes <n>", "Per-stream output budget in bytes (default 5 MiB).", (value) => Number(value)).option("--allow-external-cwd", "Allow a working directory outside the project root.").option(
|
|
2162
|
+
"--timeout-ms <ms>",
|
|
2163
|
+
"Per-task timeout in milliseconds.",
|
|
2164
|
+
(value) => Number(value)
|
|
2165
|
+
).action(async (options) => addAgent(options));
|
|
2166
|
+
agent.command("test <name>").description("Verify the OmniNode \u2194 agent connection by sending a trivial task.").action(async (name) => testAgent(name));
|
|
2167
|
+
registerAgentInspect(agent);
|
|
2168
|
+
}
|
|
2169
|
+
async function addAgent(options) {
|
|
2170
|
+
const candidate = agentConfigSchema.safeParse({
|
|
2171
|
+
name: options.name,
|
|
2172
|
+
type: "cli",
|
|
2173
|
+
command: options.command,
|
|
2174
|
+
...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
|
|
2175
|
+
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
2176
|
+
input_mode: options.inputMode,
|
|
2177
|
+
...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {},
|
|
2178
|
+
...options.envPolicy !== void 0 ? { env_policy: options.envPolicy } : {},
|
|
2179
|
+
...options.allowEnv !== void 0 && options.allowEnv.length > 0 ? { env_allowlist: options.allowEnv } : {},
|
|
2180
|
+
...options.denyEnv !== void 0 && options.denyEnv.length > 0 ? { env_denylist: options.denyEnv } : {},
|
|
2181
|
+
...options.maxOutputBytes !== void 0 ? { max_output_bytes: options.maxOutputBytes } : {},
|
|
2182
|
+
...options.allowExternalCwd !== void 0 ? { allow_external_cwd: options.allowExternalCwd } : {}
|
|
2183
|
+
});
|
|
2184
|
+
if (!candidate.success) {
|
|
2185
|
+
const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
2186
|
+
throw new OmniNodeError("CLI_USAGE", `Invalid agent options: ${issues}`);
|
|
2794
2187
|
}
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2188
|
+
const newAgent = candidate.data;
|
|
2189
|
+
const existing = loadConfig();
|
|
2190
|
+
if (existing.project.agents.some((a) => a.name === newAgent.name)) {
|
|
2191
|
+
throw new OmniNodeError(
|
|
2192
|
+
"CLI_USAGE",
|
|
2193
|
+
`Agent "${newAgent.name}" already exists in omninode.yaml. Remove it first or pick another name.`
|
|
2194
|
+
);
|
|
2801
2195
|
}
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2196
|
+
const configPath = appendToConfigList("agents", newAgent);
|
|
2197
|
+
console.log(`Added agent "${newAgent.name}" (${newAgent.type}, input=${newAgent.input_mode}) to ${configPath}.`);
|
|
2198
|
+
console.log(`Run \`omninode agent test ${newAgent.name}\` to verify the connection.`);
|
|
2199
|
+
}
|
|
2200
|
+
async function testAgent(name) {
|
|
2201
|
+
const config = loadConfig();
|
|
2202
|
+
const agentConfig = config.project.agents.find((a) => a.name === name);
|
|
2203
|
+
if (!agentConfig) {
|
|
2204
|
+
const available = config.project.agents.map((a) => a.name).join(", ") || "(none)";
|
|
2205
|
+
throw new AgentError(
|
|
2206
|
+
"AGENT_NOT_FOUND",
|
|
2207
|
+
`Agent "${name}" is not configured. Configured agents: ${available}.`
|
|
2208
|
+
);
|
|
2813
2209
|
}
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
`
|
|
2210
|
+
const agent = createAgent(agentConfig);
|
|
2211
|
+
console.log(
|
|
2212
|
+
`Testing agent "${name}" (${agent.info.integration}, input=${agentConfig.inputMode ?? "stdin"}) \u2014 sending a trivial task.`
|
|
2817
2213
|
);
|
|
2214
|
+
const result = await agent.run({
|
|
2215
|
+
taskId: `test-${Date.now()}`,
|
|
2216
|
+
objective: "Reply with the single word: OK"
|
|
2217
|
+
});
|
|
2218
|
+
console.log(` status: ${result.status}`);
|
|
2219
|
+
if (result.exitCode !== void 0) console.log(` exit: ${result.exitCode}`);
|
|
2220
|
+
if (result.summary) console.log(` output: ${result.summary}`);
|
|
2221
|
+
if (result.rawOutput && result.rawOutput !== result.summary) {
|
|
2222
|
+
console.log(` raw: ${result.rawOutput.slice(0, 200).replace(/\n/g, " ")}`);
|
|
2223
|
+
}
|
|
2224
|
+
if (result.error) console.log(` error: ${result.error}`);
|
|
2225
|
+
if (result.status !== "completed") process.exitCode = 1;
|
|
2818
2226
|
}
|
|
2819
2227
|
|
|
2820
|
-
// src/
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2228
|
+
// src/cli/commands/roles.ts
|
|
2229
|
+
function registerRoleCommands(program) {
|
|
2230
|
+
const role = program.command("role").description("Inspect roles defined in omninode.yaml.");
|
|
2231
|
+
role.command("list").description("List configured roles.").action(async () => {
|
|
2232
|
+
const config = loadConfig();
|
|
2233
|
+
const roles = config.project.roles;
|
|
2234
|
+
if (roles.length === 0) {
|
|
2235
|
+
console.log("No roles configured. Add one under `project.roles` in omninode.yaml.");
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
for (const role2 of roles) {
|
|
2239
|
+
console.log(`${role2.id} ${role2.name}`);
|
|
2240
|
+
for (const responsibility of role2.responsibilities) {
|
|
2241
|
+
console.log(` - ${responsibility}`);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
// src/tasks/engine.ts
|
|
2248
|
+
import { randomBytes } from "crypto";
|
|
2249
|
+
var ALLOWED_TRANSITIONS = {
|
|
2250
|
+
created: ["queued", "running", "cancelled"],
|
|
2251
|
+
queued: ["running", "cancelled"],
|
|
2252
|
+
running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
|
|
2253
|
+
waiting: ["running", "cancelled"],
|
|
2254
|
+
completed: [],
|
|
2255
|
+
// failed → running is the only re-entry: bounded automatic retry (§6.4).
|
|
2256
|
+
failed: ["running"],
|
|
2257
|
+
timed_out: [],
|
|
2258
|
+
cancelled: [],
|
|
2259
|
+
unknown: [],
|
|
2260
|
+
partially_completed: []
|
|
2261
|
+
};
|
|
2262
|
+
var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
|
|
2263
|
+
var EXECUTION_HISTORY_LIMIT = 10;
|
|
2264
|
+
var TaskEngine = class {
|
|
2265
|
+
constructor(options) {
|
|
2266
|
+
this.options = options;
|
|
2267
|
+
this.log = (options.log ?? logger).child({ component: "tasks" });
|
|
2829
2268
|
}
|
|
2830
|
-
|
|
2269
|
+
options;
|
|
2831
2270
|
log;
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
get providerRef() {
|
|
2835
|
-
return this.provider;
|
|
2271
|
+
get store() {
|
|
2272
|
+
return this.options.store;
|
|
2836
2273
|
}
|
|
2837
|
-
|
|
2838
|
-
|
|
2274
|
+
async create(input) {
|
|
2275
|
+
if (!input.objective || input.objective.trim().length === 0) {
|
|
2276
|
+
throw new TaskError("TASK_INVALID", "Task objective must not be empty.");
|
|
2277
|
+
}
|
|
2278
|
+
if (input.role && !this.options.roles.get(input.role)) {
|
|
2279
|
+
throw new TaskError(
|
|
2280
|
+
"TASK_INVALID",
|
|
2281
|
+
`Role "${input.role}" is not defined. Add it under project.roles in omninode.yaml.`
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
if (input.agent && !this.options.agents.get(input.agent)) {
|
|
2285
|
+
throw new TaskError(
|
|
2286
|
+
"TASK_INVALID",
|
|
2287
|
+
`Agent "${input.agent}" is not registered. Add it with \`omninode agent add\`.`
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2291
|
+
const task = {
|
|
2292
|
+
id: generateTaskId(),
|
|
2293
|
+
objective: input.objective.trim(),
|
|
2294
|
+
status: "created",
|
|
2295
|
+
attempt: 0,
|
|
2296
|
+
createdAt: now,
|
|
2297
|
+
updatedAt: now,
|
|
2298
|
+
...input.project !== void 0 ? { project: input.project } : {},
|
|
2299
|
+
...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
|
|
2300
|
+
...input.role !== void 0 ? { role: input.role } : {},
|
|
2301
|
+
...input.agent !== void 0 ? { agent: input.agent } : {},
|
|
2302
|
+
...input.context !== void 0 ? { context: input.context } : {}
|
|
2303
|
+
};
|
|
2304
|
+
await this.options.store.save(task);
|
|
2305
|
+
this.log.info(`Created task ${task.id}.`);
|
|
2306
|
+
await this.audit("task.created", task, { objective: task.objective });
|
|
2307
|
+
return task;
|
|
2839
2308
|
}
|
|
2840
|
-
|
|
2841
|
-
|
|
2309
|
+
async queue(id) {
|
|
2310
|
+
const task = await this.mustGet(id);
|
|
2311
|
+
return this.transition(task, "queued");
|
|
2842
2312
|
}
|
|
2843
|
-
/**
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2313
|
+
/** Runs the task through its assigned agent and records the result.
|
|
2314
|
+
* `maxAttempts` enables bounded automatic retries of *retryable* errors
|
|
2315
|
+
* with linear backoff; unknown/timed_out outcomes are never auto-retried. */
|
|
2316
|
+
async run(id, options = {}) {
|
|
2317
|
+
const task = await this.mustGet(id);
|
|
2318
|
+
if (task.status !== "created" && task.status !== "queued") {
|
|
2319
|
+
throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
|
|
2320
|
+
}
|
|
2321
|
+
if (!task.agent) {
|
|
2322
|
+
throw new TaskError("TASK_INVALID", `Task ${id} has no agent assigned. Re-create it with --agent.`);
|
|
2323
|
+
}
|
|
2324
|
+
const agent = this.options.agents.get(task.agent);
|
|
2325
|
+
if (!agent) {
|
|
2326
|
+
throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
|
|
2327
|
+
}
|
|
2328
|
+
const role = task.role ? this.options.roles.get(task.role) : void 0;
|
|
2329
|
+
this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
|
|
2330
|
+
let background = task.context?.background;
|
|
2331
|
+
if (this.options.memory) {
|
|
2332
|
+
const memoryContext = await this.options.memory.gatherContext({
|
|
2333
|
+
objective: task.objective,
|
|
2334
|
+
...task.role ? { role: task.role } : {},
|
|
2335
|
+
...task.project ? { project: task.project } : {}
|
|
2851
2336
|
});
|
|
2852
|
-
|
|
2853
|
-
|
|
2337
|
+
if (memoryContext.length > 0) {
|
|
2338
|
+
background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
|
|
2339
|
+
}
|
|
2854
2340
|
}
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
tags: ["task", ...task.agent ? [task.agent] : [], ...task.role ? [task.role] : []],
|
|
2873
|
-
createdAt: task.createdAt
|
|
2341
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? 1);
|
|
2342
|
+
const backoffMs = options.retryBackoffMs ?? 0;
|
|
2343
|
+
let current = task;
|
|
2344
|
+
for (let i = 0; i < maxAttempts; i += 1) {
|
|
2345
|
+
const attempt = current.attempt + 1;
|
|
2346
|
+
const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
2347
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2348
|
+
current = await this.transition(current, "running", { attempt });
|
|
2349
|
+
await this.options.audit?.record({
|
|
2350
|
+
at: startedAt,
|
|
2351
|
+
action: "agent.started",
|
|
2352
|
+
id: current.id,
|
|
2353
|
+
taskId: current.id,
|
|
2354
|
+
...current.pipelineId !== void 0 ? { pipelineId: current.pipelineId } : {},
|
|
2355
|
+
agentId: task.agent,
|
|
2356
|
+
executionId,
|
|
2357
|
+
detail: { attempt }
|
|
2874
2358
|
});
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2359
|
+
let output;
|
|
2360
|
+
let thrown;
|
|
2361
|
+
try {
|
|
2362
|
+
output = await agent.run({
|
|
2363
|
+
taskId: current.id,
|
|
2364
|
+
objective: current.objective,
|
|
2365
|
+
executionId,
|
|
2366
|
+
...current.pipelineId !== void 0 ? { pipelineId: current.pipelineId } : {},
|
|
2367
|
+
...role ? { role } : {},
|
|
2368
|
+
...background !== void 0 ? { context: background } : {}
|
|
2369
|
+
});
|
|
2370
|
+
} catch (error) {
|
|
2371
|
+
thrown = error;
|
|
2372
|
+
}
|
|
2373
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2374
|
+
await this.options.audit?.record({
|
|
2375
|
+
at: finishedAt,
|
|
2376
|
+
action: "agent.completed",
|
|
2377
|
+
id: current.id,
|
|
2378
|
+
taskId: current.id,
|
|
2379
|
+
...current.pipelineId !== void 0 ? { pipelineId: current.pipelineId } : {},
|
|
2380
|
+
agentId: task.agent,
|
|
2381
|
+
executionId,
|
|
2382
|
+
detail: { attempt, outcome: thrown !== void 0 || output === void 0 ? "threw" : "returned" }
|
|
2383
|
+
});
|
|
2384
|
+
if (thrown !== void 0 || output === void 0) {
|
|
2385
|
+
const outcome = classifyThrown(thrown);
|
|
2386
|
+
const message = thrown instanceof Error ? thrown.message : String(thrown);
|
|
2387
|
+
const retryable = outcome === "failed" && RETRYABLE_ERROR_CODES.includes(errorCode(thrown)) && i < maxAttempts - 1;
|
|
2388
|
+
current = await this.transition(current, outcome, {
|
|
2389
|
+
attempt,
|
|
2390
|
+
lastError: message,
|
|
2391
|
+
execution: {
|
|
2392
|
+
executionId,
|
|
2393
|
+
attempt,
|
|
2394
|
+
startedAt,
|
|
2395
|
+
finishedAt,
|
|
2396
|
+
outcome,
|
|
2397
|
+
error: message
|
|
2398
|
+
}
|
|
2885
2399
|
});
|
|
2400
|
+
this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
|
|
2401
|
+
if (retryable) {
|
|
2402
|
+
if (backoffMs > 0) await sleep(backoffMs * attempt);
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2405
|
+
if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
|
|
2406
|
+
return current;
|
|
2886
2407
|
}
|
|
2887
|
-
|
|
2888
|
-
|
|
2408
|
+
const currentState = await this.options.store.get(current.id);
|
|
2409
|
+
if (currentState?.status === "cancelled") {
|
|
2410
|
+
current = currentState;
|
|
2411
|
+
break;
|
|
2412
|
+
}
|
|
2413
|
+
const result = {
|
|
2414
|
+
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
2415
|
+
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
2416
|
+
// Keep the verbatim output (bounded) for report extraction and audit.
|
|
2417
|
+
...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
|
|
2418
|
+
...output.error !== void 0 ? { error: output.error } : {},
|
|
2419
|
+
finishedAt
|
|
2420
|
+
};
|
|
2421
|
+
const finalStatus = output.status === "completed" ? "completed" : "failed";
|
|
2422
|
+
current = await this.transition(current, finalStatus, {
|
|
2423
|
+
attempt,
|
|
2424
|
+
result,
|
|
2425
|
+
execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
|
|
2426
|
+
});
|
|
2427
|
+
this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
|
|
2428
|
+
break;
|
|
2429
|
+
}
|
|
2430
|
+
if (this.options.memory) {
|
|
2431
|
+
await this.options.memory.recordTaskOutcome(current);
|
|
2889
2432
|
}
|
|
2433
|
+
return current;
|
|
2890
2434
|
}
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
|
|
2435
|
+
async cancel(id) {
|
|
2436
|
+
const task = await this.mustGet(id);
|
|
2437
|
+
if (!ALLOWED_TRANSITIONS[task.status].includes("cancelled")) {
|
|
2438
|
+
throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be cancelled.`);
|
|
2896
2439
|
}
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
if (scopeEntries.length === 0) continue;
|
|
2901
|
-
const lines = scopeEntries.map((entry2) => {
|
|
2902
|
-
const category = entry2.category ? `[${entry2.category}] ` : "";
|
|
2903
|
-
return `- ${category}${entry2.content}`;
|
|
2904
|
-
}).join("\n");
|
|
2905
|
-
parts.push(`### Relevant memory (${scope})
|
|
2906
|
-
${lines}`);
|
|
2440
|
+
if (task.agent) {
|
|
2441
|
+
const agent = this.options.agents.get(task.agent);
|
|
2442
|
+
await agent?.cancel?.(task.id);
|
|
2907
2443
|
}
|
|
2908
|
-
|
|
2909
|
-
const truncated = text.length > this.maxContextChars;
|
|
2910
|
-
if (truncated) text = `${text.slice(0, this.maxContextChars - 1)}\u2026`;
|
|
2911
|
-
return { text, entries, truncated };
|
|
2444
|
+
return this.transition(task, "cancelled");
|
|
2912
2445
|
}
|
|
2913
|
-
/**
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
`
|
|
2921
|
-
{ cause: error }
|
|
2446
|
+
/** Error recovery: reset a failed/cancelled/unknown/timed_out task so it can run again. */
|
|
2447
|
+
async retry(id) {
|
|
2448
|
+
const task = await this.mustGet(id);
|
|
2449
|
+
const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
|
|
2450
|
+
if (!retryableStates.includes(task.status)) {
|
|
2451
|
+
throw new TaskError(
|
|
2452
|
+
"TASK_INVALID",
|
|
2453
|
+
`Only failed, timed out, unknown or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
|
|
2922
2454
|
);
|
|
2923
2455
|
}
|
|
2924
|
-
|
|
2925
|
-
|
|
2456
|
+
const { result: _discarded, ...rest } = task;
|
|
2457
|
+
const reset = {
|
|
2458
|
+
...rest,
|
|
2459
|
+
status: "created",
|
|
2460
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2461
|
+
};
|
|
2462
|
+
await this.options.store.save(reset);
|
|
2463
|
+
await this.audit("task.retry", reset, { from: task.status });
|
|
2464
|
+
this.log.info(`Task ${id} reset for retry.`);
|
|
2465
|
+
return reset;
|
|
2926
2466
|
}
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
// src/roles/index.ts
|
|
2930
|
-
var RoleRegistry = class {
|
|
2931
|
-
roles = /* @__PURE__ */ new Map();
|
|
2932
|
-
register(role) {
|
|
2933
|
-
this.roles.set(role.id, role);
|
|
2467
|
+
async get(id) {
|
|
2468
|
+
return this.options.store.get(id);
|
|
2934
2469
|
}
|
|
2935
|
-
|
|
2936
|
-
|
|
2470
|
+
async list(filter) {
|
|
2471
|
+
return this.options.store.list(filter);
|
|
2937
2472
|
}
|
|
2938
|
-
|
|
2939
|
-
|
|
2473
|
+
async mustGet(id) {
|
|
2474
|
+
const task = await this.options.store.get(id);
|
|
2475
|
+
if (!task) {
|
|
2476
|
+
throw new TaskError("TASK_NOT_FOUND", `Task "${id}" does not exist in this project's task store.`);
|
|
2477
|
+
}
|
|
2478
|
+
return task;
|
|
2940
2479
|
}
|
|
2941
|
-
|
|
2942
|
-
|
|
2480
|
+
async transition(task, to, extra = {}) {
|
|
2481
|
+
const allowed = ALLOWED_TRANSITIONS[task.status] ?? [];
|
|
2482
|
+
if (!allowed.includes(to)) {
|
|
2483
|
+
throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
|
|
2484
|
+
}
|
|
2485
|
+
const executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
|
|
2486
|
+
const updated = {
|
|
2487
|
+
...task,
|
|
2488
|
+
status: to,
|
|
2489
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2490
|
+
...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
|
|
2491
|
+
...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
|
|
2492
|
+
...executions !== void 0 ? { executions } : {},
|
|
2493
|
+
...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
|
|
2494
|
+
};
|
|
2495
|
+
await this.options.store.save(updated);
|
|
2496
|
+
await this.audit(`task.${to === "queued" ? "queued" : to}`, updated, {
|
|
2497
|
+
from: task.status
|
|
2498
|
+
});
|
|
2499
|
+
return updated;
|
|
2500
|
+
}
|
|
2501
|
+
async audit(action, task, detail, executionId) {
|
|
2502
|
+
await this.options.audit?.record({
|
|
2503
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2504
|
+
action,
|
|
2505
|
+
id: task.id,
|
|
2506
|
+
taskId: task.id,
|
|
2507
|
+
...task.project !== void 0 ? { project: task.project } : {},
|
|
2508
|
+
...task.pipelineId !== void 0 ? { pipelineId: task.pipelineId } : {},
|
|
2509
|
+
...task.agent !== void 0 ? { agentId: task.agent } : {},
|
|
2510
|
+
...executionId !== void 0 ? { executionId } : {},
|
|
2511
|
+
...detail !== void 0 ? { detail } : {}
|
|
2512
|
+
});
|
|
2943
2513
|
}
|
|
2944
2514
|
};
|
|
2945
|
-
function
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2515
|
+
function generateTaskId() {
|
|
2516
|
+
return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
2517
|
+
}
|
|
2518
|
+
function errorCode(error) {
|
|
2519
|
+
return error?.code ?? "";
|
|
2520
|
+
}
|
|
2521
|
+
function classifyThrown(error) {
|
|
2522
|
+
if (error instanceof AgentError && error.code === "AGENT_TIMEOUT") {
|
|
2523
|
+
const dispatched = error.details?.dispatched;
|
|
2524
|
+
return dispatched === false ? "timed_out" : "unknown";
|
|
2525
|
+
}
|
|
2526
|
+
return "failed";
|
|
2949
2527
|
}
|
|
2528
|
+
function sleep(ms) {
|
|
2529
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// src/audit/index.ts
|
|
2533
|
+
import { appendFile, mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
|
|
2534
|
+
import { dirname as dirname2 } from "path";
|
|
2535
|
+
var FileAuditLog = class {
|
|
2536
|
+
filePath;
|
|
2537
|
+
log;
|
|
2538
|
+
constructor(directory = `${process.cwd()}/.omninode`, log = logger) {
|
|
2539
|
+
this.filePath = `${directory}/audit.jsonl`;
|
|
2540
|
+
this.log = log.child({ component: "audit" });
|
|
2541
|
+
}
|
|
2542
|
+
async record(event) {
|
|
2543
|
+
try {
|
|
2544
|
+
await mkdir2(dirname2(this.filePath), { recursive: true });
|
|
2545
|
+
await appendFile(this.filePath, `${JSON.stringify(event)}
|
|
2546
|
+
`, "utf8");
|
|
2547
|
+
} catch (error) {
|
|
2548
|
+
this.log.warn(
|
|
2549
|
+
`Audit write failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2550
|
+
);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
/** Recent events, oldest first; used by the CLI. */
|
|
2554
|
+
async recent(limit = 20) {
|
|
2555
|
+
let raw;
|
|
2556
|
+
try {
|
|
2557
|
+
raw = await readFile2(this.filePath, "utf8");
|
|
2558
|
+
} catch {
|
|
2559
|
+
return [];
|
|
2560
|
+
}
|
|
2561
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => {
|
|
2562
|
+
try {
|
|
2563
|
+
return JSON.parse(line);
|
|
2564
|
+
} catch {
|
|
2565
|
+
return void 0;
|
|
2566
|
+
}
|
|
2567
|
+
}).filter((event) => event !== void 0);
|
|
2568
|
+
return events.slice(-limit);
|
|
2569
|
+
}
|
|
2570
|
+
};
|
|
2950
2571
|
|
|
2951
2572
|
// src/tasks/engine-factory.ts
|
|
2952
2573
|
function createTaskEngine(config, log = logger, directory) {
|
|
@@ -3038,6 +2659,7 @@ function registerTaskCommands(program) {
|
|
|
3038
2659
|
printOutcome(finished);
|
|
3039
2660
|
if (finished.status !== "completed") process.exitCode = 1;
|
|
3040
2661
|
});
|
|
2662
|
+
registerTaskInspect(task);
|
|
3041
2663
|
}
|
|
3042
2664
|
function printOutcome(task, options = {}) {
|
|
3043
2665
|
console.log(` id: ${task.id}`);
|
|
@@ -3384,6 +3006,7 @@ function truncate4(text, max) {
|
|
|
3384
3006
|
}
|
|
3385
3007
|
|
|
3386
3008
|
// src/reports/service.ts
|
|
3009
|
+
var MAX_REPORT_BYTES = 256 * 1024;
|
|
3387
3010
|
var ReportService = class {
|
|
3388
3011
|
constructor(store, log = logger) {
|
|
3389
3012
|
this.store = store;
|
|
@@ -3418,6 +3041,16 @@ var ReportService = class {
|
|
|
3418
3041
|
const reports = [];
|
|
3419
3042
|
const rejected = [];
|
|
3420
3043
|
for (const candidate of candidates) {
|
|
3044
|
+
const size = Buffer.byteLength(JSON.stringify(candidate), "utf8");
|
|
3045
|
+
if (size > MAX_REPORT_BYTES) {
|
|
3046
|
+
rejected.push({
|
|
3047
|
+
key: candidate?.id ?? "(unknown)",
|
|
3048
|
+
reason: `report exceeds the ${MAX_REPORT_BYTES} byte limit (${size} bytes)`,
|
|
3049
|
+
issues: []
|
|
3050
|
+
});
|
|
3051
|
+
this.log.warn(`Rejected oversized report (${size} bytes).`);
|
|
3052
|
+
continue;
|
|
3053
|
+
}
|
|
3421
3054
|
const result = validateReport(candidate);
|
|
3422
3055
|
if (result.valid) {
|
|
3423
3056
|
reports.push(result.report);
|
|
@@ -4082,6 +3715,7 @@ var PipelineEngine = class {
|
|
|
4082
3715
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4083
3716
|
action: "pipeline.run.started",
|
|
4084
3717
|
id: run2.id,
|
|
3718
|
+
pipelineId: def.id,
|
|
4085
3719
|
detail: { pipeline: def.id, objective }
|
|
4086
3720
|
});
|
|
4087
3721
|
const deps = this.effectiveDependencies(def);
|
|
@@ -4122,7 +3756,9 @@ var PipelineEngine = class {
|
|
|
4122
3756
|
combinedContext: contextParts.join("\n\n"),
|
|
4123
3757
|
reports: [...allReports],
|
|
4124
3758
|
pipelineRunId: run2.id,
|
|
4125
|
-
runId: run2.id
|
|
3759
|
+
runId: run2.id,
|
|
3760
|
+
pipelineDefId: def.id,
|
|
3761
|
+
taskId: run2.id
|
|
4126
3762
|
};
|
|
4127
3763
|
return this.executeStep(step, ctx);
|
|
4128
3764
|
})
|
|
@@ -4199,8 +3835,9 @@ ${contribution}`);
|
|
|
4199
3835
|
this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
|
|
4200
3836
|
await this.options.audit?.record({
|
|
4201
3837
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4202
|
-
action: "pipeline.run.finished",
|
|
3838
|
+
action: cancelled ? "pipeline.run.cancelled" : finalStatus === "failed" ? "pipeline.run.failed" : "pipeline.run.finished",
|
|
4203
3839
|
id: run2.id,
|
|
3840
|
+
pipelineId: def.id,
|
|
4204
3841
|
detail: { pipeline: def.id, status: finalStatus }
|
|
4205
3842
|
});
|
|
4206
3843
|
return finished;
|
|
@@ -4281,6 +3918,7 @@ ${contribution}`);
|
|
|
4281
3918
|
agentNames.map(
|
|
4282
3919
|
(name) => this.options.tasks.create({
|
|
4283
3920
|
objective: ctx.objective,
|
|
3921
|
+
...ctx.pipelineRunId !== void 0 ? { pipelineId: ctx.pipelineRunId } : {},
|
|
4284
3922
|
agent: name,
|
|
4285
3923
|
...ctx.role ? { role: ctx.role.id } : {},
|
|
4286
3924
|
...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
|
|
@@ -4323,6 +3961,7 @@ ${contribution}`);
|
|
|
4323
3961
|
async runSingle(step, ctx, agentName) {
|
|
4324
3962
|
const task = await this.options.tasks.create({
|
|
4325
3963
|
objective: ctx.objective,
|
|
3964
|
+
...ctx.pipelineRunId !== void 0 ? { pipelineId: ctx.pipelineRunId } : {},
|
|
4326
3965
|
agent: agentName,
|
|
4327
3966
|
...ctx.role ? { role: ctx.role.id } : {},
|
|
4328
3967
|
...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
|
|
@@ -4393,7 +4032,13 @@ ${contribution}`);
|
|
|
4393
4032
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4394
4033
|
action: "plan.generated",
|
|
4395
4034
|
id: plan.id,
|
|
4396
|
-
|
|
4035
|
+
...ctx.pipelineDefId !== void 0 ? { pipelineId: ctx.pipelineDefId } : {},
|
|
4036
|
+
detail: {
|
|
4037
|
+
generatedBy: plan.generatedBy,
|
|
4038
|
+
steps: plan.steps.length,
|
|
4039
|
+
run: ctx.pipelineRunId,
|
|
4040
|
+
taskId: ctx.taskId
|
|
4041
|
+
}
|
|
4397
4042
|
});
|
|
4398
4043
|
return {
|
|
4399
4044
|
status: "completed",
|
|
@@ -4427,7 +4072,7 @@ function formatPlan(plan) {
|
|
|
4427
4072
|
}
|
|
4428
4073
|
return lines.join("\n");
|
|
4429
4074
|
}
|
|
4430
|
-
function createDefaultChatFn(providers) {
|
|
4075
|
+
function createDefaultChatFn(providers, options = {}) {
|
|
4431
4076
|
return async (modelRef, messages) => {
|
|
4432
4077
|
const separator = modelRef.indexOf(":");
|
|
4433
4078
|
if (separator <= 0 || separator === modelRef.length - 1) {
|
|
@@ -4445,33 +4090,15 @@ function createDefaultChatFn(providers) {
|
|
|
4445
4090
|
`Provider "${providerName}" (model "${modelRef}") is not configured.`
|
|
4446
4091
|
);
|
|
4447
4092
|
}
|
|
4448
|
-
const provider = createProvider(providerConfig
|
|
4093
|
+
const provider = createProvider(providerConfig, {
|
|
4094
|
+
...options.audit !== void 0 ? { audit: options.audit } : {},
|
|
4095
|
+
...options.env !== void 0 ? { env: options.env } : {}
|
|
4096
|
+
});
|
|
4449
4097
|
const response = await provider.chat({ model: modelId, messages });
|
|
4450
4098
|
return response.content;
|
|
4451
4099
|
};
|
|
4452
4100
|
}
|
|
4453
4101
|
|
|
4454
|
-
// src/pipelines/store.ts
|
|
4455
|
-
var FilePipelineRunStore = class extends JsonFileStore {
|
|
4456
|
-
constructor(directory) {
|
|
4457
|
-
super({ ...directory !== void 0 ? { directory } : {}, fileName: "pipelines.json" });
|
|
4458
|
-
}
|
|
4459
|
-
save(run2) {
|
|
4460
|
-
return this.mutate((runs) => {
|
|
4461
|
-
const index = runs.findIndex((r) => r.id === run2.id);
|
|
4462
|
-
if (index >= 0) runs[index] = run2;
|
|
4463
|
-
else runs.push(run2);
|
|
4464
|
-
return [runs, void 0];
|
|
4465
|
-
});
|
|
4466
|
-
}
|
|
4467
|
-
async get(id) {
|
|
4468
|
-
return (await this.readAll()).find((r) => r.id === id);
|
|
4469
|
-
}
|
|
4470
|
-
async list(filter = {}) {
|
|
4471
|
-
return (await this.readAll()).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 ?? ""));
|
|
4472
|
-
}
|
|
4473
|
-
};
|
|
4474
|
-
|
|
4475
4102
|
// src/planner/store.ts
|
|
4476
4103
|
var FilePlanStore = class extends JsonFileStore {
|
|
4477
4104
|
constructor(directory) {
|
|
@@ -4516,9 +4143,10 @@ function buildPlanner(plannerConfig, chat, log = logger) {
|
|
|
4516
4143
|
|
|
4517
4144
|
// src/pipelines/engine-factory.ts
|
|
4518
4145
|
function buildPipelineEngine(config, log = logger) {
|
|
4146
|
+
const auditLog = new FileAuditLog();
|
|
4519
4147
|
const agents = buildAgentRegistry(config, log);
|
|
4520
4148
|
const roles = buildRoleRegistry(config);
|
|
4521
|
-
const chat = createDefaultChatFn(config.project.providers);
|
|
4149
|
+
const chat = createDefaultChatFn(config.project.providers, { audit: auditLog });
|
|
4522
4150
|
const tasks = new TaskEngine({
|
|
4523
4151
|
agents,
|
|
4524
4152
|
roles,
|
|
@@ -4646,6 +4274,7 @@ function registerPipelineCommands(program) {
|
|
|
4646
4274
|
console.log(` reports: ${run2.reports.length}`);
|
|
4647
4275
|
}
|
|
4648
4276
|
});
|
|
4277
|
+
registerPipelineInspect(pipeline);
|
|
4649
4278
|
}
|
|
4650
4279
|
async function runPipelineAction(id, objective) {
|
|
4651
4280
|
const config = loadConfig();
|
|
@@ -4955,6 +4584,72 @@ function registerMigrateCommand(program) {
|
|
|
4955
4584
|
});
|
|
4956
4585
|
}
|
|
4957
4586
|
|
|
4587
|
+
// src/cli/commands/security.ts
|
|
4588
|
+
function registerSecurityCommand(program) {
|
|
4589
|
+
program.command("security").description("Inspect a project's security posture (advisory).").argument("[action]", "Action to perform.", "audit").action(async (action) => {
|
|
4590
|
+
if (action !== "audit") {
|
|
4591
|
+
console.log(`Unknown security action "${action}". Available: audit.`);
|
|
4592
|
+
return;
|
|
4593
|
+
}
|
|
4594
|
+
const config = loadConfig();
|
|
4595
|
+
const findings = [];
|
|
4596
|
+
for (const agent of config.project.agents) {
|
|
4597
|
+
const policy = resolveEnvPolicy(agent);
|
|
4598
|
+
if (policy === "inherit") {
|
|
4599
|
+
findings.push({
|
|
4600
|
+
severity: "warn",
|
|
4601
|
+
subject: `agent "${agent.name}"`,
|
|
4602
|
+
message: "inherits the full environment \u2014 provider keys in OmniNode's environment are visible to it. Consider env_policy: allowlist|denylist|explicit."
|
|
4603
|
+
});
|
|
4604
|
+
}
|
|
4605
|
+
if (agent.allowExternalCwd) {
|
|
4606
|
+
findings.push({
|
|
4607
|
+
severity: "info",
|
|
4608
|
+
subject: `agent "${agent.name}"`,
|
|
4609
|
+
message: "may run outside the project root (allow_external_cwd)."
|
|
4610
|
+
});
|
|
4611
|
+
}
|
|
4612
|
+
if (agent.env && Object.keys(agent.env).length > 0) {
|
|
4613
|
+
findings.push({
|
|
4614
|
+
severity: "info",
|
|
4615
|
+
subject: `agent "${agent.name}"`,
|
|
4616
|
+
message: `receives ${Object.keys(agent.env).length} explicit environment variable(s).`
|
|
4617
|
+
});
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4620
|
+
for (const provider of config.project.providers) {
|
|
4621
|
+
if (!provider.apiKeyEnvVar) {
|
|
4622
|
+
findings.push({
|
|
4623
|
+
severity: "info",
|
|
4624
|
+
subject: `provider "${provider.name}"`,
|
|
4625
|
+
message: "no api_key_env_var \u2014 fine for keyless local endpoints, otherwise credentials cannot be resolved."
|
|
4626
|
+
});
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
if (config.project.memory?.provider === "remembera" && !config.project.memory.apiKeyEnvVar) {
|
|
4630
|
+
findings.push({
|
|
4631
|
+
severity: "warn",
|
|
4632
|
+
subject: "memory",
|
|
4633
|
+
message: "Remembera is configured without api_key_env_var; requests will be unauthenticated."
|
|
4634
|
+
});
|
|
4635
|
+
}
|
|
4636
|
+
console.log(`OmniNode security audit \u2014 project "${config.project.name}"`);
|
|
4637
|
+
if (findings.length === 0) {
|
|
4638
|
+
console.log(" no findings.");
|
|
4639
|
+
}
|
|
4640
|
+
for (const finding of findings) {
|
|
4641
|
+
console.log(` [${finding.severity.toUpperCase()}] ${finding.subject}: ${finding.message}`);
|
|
4642
|
+
}
|
|
4643
|
+
console.log(
|
|
4644
|
+
`
|
|
4645
|
+
Trust model: agents execute with the permissions of the OS user running OmniNode.`
|
|
4646
|
+
);
|
|
4647
|
+
console.log(
|
|
4648
|
+
`OmniNode does not sandbox agents \u2014 run untrusted agents in a container or as a dedicated user.`
|
|
4649
|
+
);
|
|
4650
|
+
});
|
|
4651
|
+
}
|
|
4652
|
+
|
|
4958
4653
|
// src/cli/index.ts
|
|
4959
4654
|
function createProgram() {
|
|
4960
4655
|
const program = new Command();
|
|
@@ -4975,10 +4670,21 @@ function createProgram() {
|
|
|
4975
4670
|
registerStatusCommand(program);
|
|
4976
4671
|
registerAuditCommand(program);
|
|
4977
4672
|
registerMigrateCommand(program);
|
|
4673
|
+
registerSecurityCommand(program);
|
|
4978
4674
|
registerRunAlias(program);
|
|
4979
4675
|
return program;
|
|
4980
4676
|
}
|
|
4677
|
+
function applyLoggingConfig() {
|
|
4678
|
+
try {
|
|
4679
|
+
if (findConfigFile()) {
|
|
4680
|
+
const logging = loadConfig().logging;
|
|
4681
|
+
if (logging) configureLogger(logging);
|
|
4682
|
+
}
|
|
4683
|
+
} catch {
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4981
4686
|
async function run(argv) {
|
|
4687
|
+
applyLoggingConfig();
|
|
4982
4688
|
const program = createProgram();
|
|
4983
4689
|
try {
|
|
4984
4690
|
await program.parseAsync(argv);
|