@hilbras/omninode 2.0.0-alpha.13 → 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 +8 -1
- package/dist/cli/chunk-6Z6O63SS.js +708 -0
- package/dist/cli/chunk-6Z6O63SS.js.map +1 -0
- package/dist/cli/index.js +1163 -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 +80 -31
- package/dist/index.js +149 -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,7 +106,10 @@ 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
|
|
|
148
115
|
// src/config/secrets.ts
|
|
@@ -364,6 +331,7 @@ function defaultProjectConfigYaml(projectName = "My Project") {
|
|
|
364
331
|
}
|
|
365
332
|
|
|
366
333
|
// src/cli/commands/init.ts
|
|
334
|
+
import { existsSync as existsSync2, writeFileSync } from "fs";
|
|
367
335
|
async function runInit(options) {
|
|
368
336
|
const target = `${process.cwd()}/omninode.yaml`;
|
|
369
337
|
if (!options.force && existsSync2(target)) {
|
|
@@ -389,578 +357,6 @@ function registerProjectCommands(program) {
|
|
|
389
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));
|
|
390
358
|
}
|
|
391
359
|
|
|
392
|
-
// src/providers/auth.ts
|
|
393
|
-
function resolveApiKey(config, env = process.env) {
|
|
394
|
-
if (!config.apiKeyEnvVar) return void 0;
|
|
395
|
-
const value = env[config.apiKeyEnvVar];
|
|
396
|
-
if (!value) {
|
|
397
|
-
throw new ProviderError(
|
|
398
|
-
"PROVIDER_AUTH_FAILED",
|
|
399
|
-
`Environment variable "${config.apiKeyEnvVar}" (referenced by provider "${config.name}") is not set.`
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
return value;
|
|
403
|
-
}
|
|
404
|
-
function authHeaders(config, env = process.env) {
|
|
405
|
-
const key = resolveApiKey(config, env);
|
|
406
|
-
const base = { ...config.headers ?? {} };
|
|
407
|
-
return key ? { ...base, authorization: `Bearer ${key}` } : base;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// src/providers/http.ts
|
|
411
|
-
var HttpResponseError = class extends Error {
|
|
412
|
-
constructor(message, status, cause) {
|
|
413
|
-
super(message, { cause });
|
|
414
|
-
this.status = status;
|
|
415
|
-
this.name = "HttpResponseError";
|
|
416
|
-
}
|
|
417
|
-
status;
|
|
418
|
-
};
|
|
419
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
420
|
-
async function requestJson(url, options = {}) {
|
|
421
|
-
const { method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
|
|
422
|
-
const response = await fetch(url, {
|
|
423
|
-
method,
|
|
424
|
-
headers: {
|
|
425
|
-
accept: "application/json",
|
|
426
|
-
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
427
|
-
...headers
|
|
428
|
-
},
|
|
429
|
-
...body !== void 0 ? { body: JSON.stringify(body) } : {},
|
|
430
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
431
|
-
});
|
|
432
|
-
let parsed;
|
|
433
|
-
try {
|
|
434
|
-
parsed = await response.json();
|
|
435
|
-
} catch (error) {
|
|
436
|
-
throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
|
|
437
|
-
}
|
|
438
|
-
return { status: response.status, body: parsed };
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// src/logger/index.ts
|
|
442
|
-
var LEVEL_ORDER = {
|
|
443
|
-
debug: 10,
|
|
444
|
-
info: 20,
|
|
445
|
-
warn: 30,
|
|
446
|
-
error: 40
|
|
447
|
-
};
|
|
448
|
-
var ConsoleLogSink = class {
|
|
449
|
-
constructor(stream = process.stderr) {
|
|
450
|
-
this.stream = stream;
|
|
451
|
-
}
|
|
452
|
-
stream;
|
|
453
|
-
write(_level, line) {
|
|
454
|
-
this.stream.write(line + "\n");
|
|
455
|
-
}
|
|
456
|
-
};
|
|
457
|
-
var Logger = class _Logger {
|
|
458
|
-
level;
|
|
459
|
-
sink;
|
|
460
|
-
context;
|
|
461
|
-
constructor(options = {}) {
|
|
462
|
-
this.level = options.level ?? "info";
|
|
463
|
-
this.sink = options.sink ?? new ConsoleLogSink();
|
|
464
|
-
this.context = options.context ?? {};
|
|
465
|
-
}
|
|
466
|
-
child(context) {
|
|
467
|
-
return new _Logger({
|
|
468
|
-
level: this.level,
|
|
469
|
-
sink: this.sink,
|
|
470
|
-
context: { ...this.context, ...context }
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
debug(message, data) {
|
|
474
|
-
this.log("debug", message, data);
|
|
475
|
-
}
|
|
476
|
-
info(message, data) {
|
|
477
|
-
this.log("info", message, data);
|
|
478
|
-
}
|
|
479
|
-
warn(message, data) {
|
|
480
|
-
this.log("warn", message, data);
|
|
481
|
-
}
|
|
482
|
-
error(message, data) {
|
|
483
|
-
this.log("error", message, data);
|
|
484
|
-
}
|
|
485
|
-
log(level, message, data) {
|
|
486
|
-
if (LEVEL_ORDER[level] < LEVEL_ORDER[this.level]) return;
|
|
487
|
-
const parts = [(/* @__PURE__ */ new Date()).toISOString(), level.toUpperCase(), message];
|
|
488
|
-
const merged = { ...this.context, ...data };
|
|
489
|
-
if (Object.keys(merged).length > 0) {
|
|
490
|
-
parts.push(JSON.stringify(merged));
|
|
491
|
-
}
|
|
492
|
-
this.sink.write(level, parts.join(" "));
|
|
493
|
-
}
|
|
494
|
-
};
|
|
495
|
-
var logger = new Logger();
|
|
496
|
-
|
|
497
|
-
// src/providers/errors.ts
|
|
498
|
-
function kindToCode(kind) {
|
|
499
|
-
switch (kind) {
|
|
500
|
-
case "AUTHENTICATION_ERROR":
|
|
501
|
-
return "PROVIDER_AUTH_FAILED";
|
|
502
|
-
case "MODEL_NOT_FOUND":
|
|
503
|
-
return "MODEL_NOT_FOUND";
|
|
504
|
-
default:
|
|
505
|
-
return "PROVIDER_UNAVAILABLE";
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
function isRetryable(kind) {
|
|
509
|
-
return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
|
|
510
|
-
}
|
|
511
|
-
function kindFromStatus(status) {
|
|
512
|
-
if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
|
|
513
|
-
if (status === 429) return "RATE_LIMIT_ERROR";
|
|
514
|
-
if (status === 404) return "MODEL_NOT_FOUND";
|
|
515
|
-
if (status === 408 || status === 504) return "TIMEOUT";
|
|
516
|
-
if (status >= 500) return "SERVER_ERROR";
|
|
517
|
-
if (status >= 400) return "INVALID_REQUEST";
|
|
518
|
-
return "UNKNOWN_ERROR";
|
|
519
|
-
}
|
|
520
|
-
function extractMessage(body) {
|
|
521
|
-
if (body === null || typeof body !== "object") return void 0;
|
|
522
|
-
const root = body;
|
|
523
|
-
const error = root.error ?? root;
|
|
524
|
-
const message = error.message ?? error.detail ?? root.message;
|
|
525
|
-
return typeof message === "string" ? message : void 0;
|
|
526
|
-
}
|
|
527
|
-
function parseRetryAfter(value, now = Date.now()) {
|
|
528
|
-
if (!value) return void 0;
|
|
529
|
-
const seconds = Number(value);
|
|
530
|
-
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
531
|
-
const date = Date.parse(value);
|
|
532
|
-
return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
|
|
533
|
-
}
|
|
534
|
-
function isTimeoutError(error) {
|
|
535
|
-
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
|
|
536
|
-
}
|
|
537
|
-
function fallbackMessage(kind, context, status) {
|
|
538
|
-
const { provider, operation } = context;
|
|
539
|
-
switch (kind) {
|
|
540
|
-
case "AUTHENTICATION_ERROR":
|
|
541
|
-
return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
|
|
542
|
-
case "RATE_LIMIT_ERROR":
|
|
543
|
-
return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
|
|
544
|
-
case "MODEL_NOT_FOUND":
|
|
545
|
-
return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
|
|
546
|
-
case "TIMEOUT":
|
|
547
|
-
return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
|
|
548
|
-
case "SERVER_ERROR":
|
|
549
|
-
return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
|
|
550
|
-
case "INVALID_REQUEST":
|
|
551
|
-
return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
|
|
552
|
-
default:
|
|
553
|
-
return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
function providerHttpError(context) {
|
|
557
|
-
const status = context.status ?? 0;
|
|
558
|
-
const kind = kindFromStatus(status);
|
|
559
|
-
const detail = extractMessage(context.body);
|
|
560
|
-
const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
|
|
561
|
-
return new ProviderError(
|
|
562
|
-
context.code ?? kindToCode(kind),
|
|
563
|
-
detail ?? fallbackMessage(kind, context, status),
|
|
564
|
-
{
|
|
565
|
-
cause: context.cause,
|
|
566
|
-
details: {
|
|
567
|
-
kind,
|
|
568
|
-
provider: context.provider,
|
|
569
|
-
operation: context.operation,
|
|
570
|
-
status,
|
|
571
|
-
retryable: isRetryable(kind),
|
|
572
|
-
...retryAfterMs !== void 0 ? { retryAfterMs } : {},
|
|
573
|
-
...detail ? { providerMessage: detail } : {}
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
);
|
|
577
|
-
}
|
|
578
|
-
function providerTransportError(context) {
|
|
579
|
-
const cause = context.cause;
|
|
580
|
-
const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
|
|
581
|
-
const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
|
|
582
|
-
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
|
|
583
|
-
cause: context.cause,
|
|
584
|
-
details: {
|
|
585
|
-
kind,
|
|
586
|
-
provider: context.provider,
|
|
587
|
-
operation: context.operation,
|
|
588
|
-
retryable: isRetryable(kind)
|
|
589
|
-
}
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// src/providers/openai-compatible/index.ts
|
|
594
|
-
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
595
|
-
var OpenAICompatibleProvider = class {
|
|
596
|
-
config;
|
|
597
|
-
providerId;
|
|
598
|
-
authentication;
|
|
599
|
-
log;
|
|
600
|
-
/** Discovered models, so getModel() answers without another round-trip. */
|
|
601
|
-
discovered;
|
|
602
|
-
env;
|
|
603
|
-
constructor(config, log = logger, env = process.env) {
|
|
604
|
-
this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
|
|
605
|
-
this.env = env;
|
|
606
|
-
this.providerId = config.providerId ?? config.name;
|
|
607
|
-
this.authentication = {
|
|
608
|
-
method: config.apiKeyEnvVar ? "bearer" : "none",
|
|
609
|
-
...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
|
|
610
|
-
configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
|
|
611
|
-
};
|
|
612
|
-
this.log = log.child({ provider: config.name });
|
|
613
|
-
}
|
|
614
|
-
/** Provider-level request timeout (roadmap §6.6/§11) with the shared default. */
|
|
615
|
-
get requestTimeoutMs() {
|
|
616
|
-
return this.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
617
|
-
}
|
|
618
|
-
/** Authenticate + discover (roadmap §10, Model Discovery). */
|
|
619
|
-
async connect() {
|
|
620
|
-
authHeaders(this.config, this.env);
|
|
621
|
-
const models = await this.listModels();
|
|
622
|
-
this.discovered = models;
|
|
623
|
-
return models;
|
|
624
|
-
}
|
|
625
|
-
async getModel(modelId) {
|
|
626
|
-
const models = this.discovered ?? await this.connect();
|
|
627
|
-
return models.find((m) => m.id === modelId);
|
|
628
|
-
}
|
|
629
|
-
/** Raw /models body (protected so adapters can capture gateway-level metadata). */
|
|
630
|
-
async rawModelsResponse() {
|
|
631
|
-
const response = await this.modelsRequest();
|
|
632
|
-
return response.body;
|
|
633
|
-
}
|
|
634
|
-
async modelsRequest() {
|
|
635
|
-
let response;
|
|
636
|
-
try {
|
|
637
|
-
response = await requestJson(`${this.config.baseUrl}/models`, {
|
|
638
|
-
headers: authHeaders(this.config, this.env),
|
|
639
|
-
timeoutMs: this.requestTimeoutMs
|
|
640
|
-
});
|
|
641
|
-
} catch (error) {
|
|
642
|
-
if (error instanceof ProviderError) throw error;
|
|
643
|
-
throw providerTransportError({ provider: this.config.name, operation: "listModels", cause: error });
|
|
644
|
-
}
|
|
645
|
-
const { status, body } = response;
|
|
646
|
-
if (status !== 200) {
|
|
647
|
-
throw providerHttpError({
|
|
648
|
-
provider: this.config.name,
|
|
649
|
-
operation: "listModels",
|
|
650
|
-
status,
|
|
651
|
-
body
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
return response;
|
|
655
|
-
}
|
|
656
|
-
async listModels() {
|
|
657
|
-
const body = await this.rawModelsResponse();
|
|
658
|
-
const entries = extractModelEntries(body, this.config);
|
|
659
|
-
const models = [];
|
|
660
|
-
let skipped = 0;
|
|
661
|
-
for (const entry2 of entries) {
|
|
662
|
-
if (typeof entry2.id !== "string" || entry2.id.length === 0) {
|
|
663
|
-
skipped += 1;
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
models.push(this.toModelInfo(entry2));
|
|
667
|
-
}
|
|
668
|
-
if (skipped > 0) {
|
|
669
|
-
this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
|
|
670
|
-
}
|
|
671
|
-
return models;
|
|
672
|
-
}
|
|
673
|
-
/** Maps a raw model entry onto the normalized metadata shape (§10). */
|
|
674
|
-
toModelInfo(entry2) {
|
|
675
|
-
const parameters = Array.isArray(entry2.supported_parameters) ? entry2.supported_parameters.filter((p) => typeof p === "string") : void 0;
|
|
676
|
-
const contextWindow = typeof entry2.context_length === "number" ? entry2.context_length : typeof entry2.context_window === "number" ? entry2.context_window : void 0;
|
|
677
|
-
const inputTypes = normalizeModalities(entry2.input_modalities);
|
|
678
|
-
const outputTypes = normalizeModalities(entry2.output_modalities);
|
|
679
|
-
return {
|
|
680
|
-
provider: this.config.name,
|
|
681
|
-
id: entry2.id,
|
|
682
|
-
displayName: entry2.id,
|
|
683
|
-
...typeof entry2.name === "string" ? { name: entry2.name } : {},
|
|
684
|
-
status: "available",
|
|
685
|
-
...contextWindow !== void 0 ? { contextWindow } : {},
|
|
686
|
-
...inputTypes !== void 0 ? { inputTypes } : {},
|
|
687
|
-
...outputTypes !== void 0 ? { outputTypes } : {},
|
|
688
|
-
...parameters !== void 0 ? {
|
|
689
|
-
supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
|
|
690
|
-
supportsStructuredOutput: parameters.includes("response_format"),
|
|
691
|
-
supportsStreaming: parameters.includes("stream")
|
|
692
|
-
} : {},
|
|
693
|
-
metadata: {
|
|
694
|
-
...typeof entry2.owned_by === "string" ? { ownedBy: entry2.owned_by } : {},
|
|
695
|
-
...typeof entry2.created === "number" ? { createdAt: entry2.created } : {},
|
|
696
|
-
...parameters !== void 0 ? { supportedParameters: parameters } : {}
|
|
697
|
-
}
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
/** Streaming where the gateway supports it (§11). */
|
|
701
|
-
stream(request, onChunk) {
|
|
702
|
-
return streamChatCompletion(this.config, request, onChunk, this.env);
|
|
703
|
-
}
|
|
704
|
-
async healthCheck() {
|
|
705
|
-
const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
706
|
-
try {
|
|
707
|
-
const started = Date.now();
|
|
708
|
-
const models = await this.listModels();
|
|
709
|
-
return {
|
|
710
|
-
name: this.config.name,
|
|
711
|
-
type: this.config.type,
|
|
712
|
-
baseUrl: this.config.baseUrl,
|
|
713
|
-
health: "healthy",
|
|
714
|
-
connected: true,
|
|
715
|
-
modelCount: models.length,
|
|
716
|
-
lastCheckedAt,
|
|
717
|
-
message: `Discovered ${models.length} model(s) in ${Date.now() - started}ms.`
|
|
718
|
-
};
|
|
719
|
-
} catch (error) {
|
|
720
|
-
return {
|
|
721
|
-
name: this.config.name,
|
|
722
|
-
type: this.config.type,
|
|
723
|
-
baseUrl: this.config.baseUrl,
|
|
724
|
-
health: inferHealth(error),
|
|
725
|
-
connected: false,
|
|
726
|
-
modelCount: 0,
|
|
727
|
-
lastCheckedAt,
|
|
728
|
-
message: error instanceof Error ? error.message : String(error)
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
async chat(request) {
|
|
733
|
-
let response;
|
|
734
|
-
try {
|
|
735
|
-
response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
|
|
736
|
-
method: "POST",
|
|
737
|
-
headers: authHeaders(this.config, this.env),
|
|
738
|
-
timeoutMs: this.requestTimeoutMs,
|
|
739
|
-
body: {
|
|
740
|
-
model: request.model,
|
|
741
|
-
messages: request.messages,
|
|
742
|
-
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
743
|
-
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
|
|
744
|
-
...request.stop !== void 0 ? { stop: request.stop } : {}
|
|
745
|
-
}
|
|
746
|
-
});
|
|
747
|
-
} catch (error) {
|
|
748
|
-
if (error instanceof ProviderError) throw error;
|
|
749
|
-
throw providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
|
|
750
|
-
}
|
|
751
|
-
const { status, body } = response;
|
|
752
|
-
if (status !== 200) {
|
|
753
|
-
throw providerHttpError({
|
|
754
|
-
provider: this.config.name,
|
|
755
|
-
operation: "chat",
|
|
756
|
-
status,
|
|
757
|
-
body,
|
|
758
|
-
...status === 404 ? {
|
|
759
|
-
code: "MODEL_NOT_FOUND"
|
|
760
|
-
} : {}
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
const payload = body;
|
|
764
|
-
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
765
|
-
const first = choices[0];
|
|
766
|
-
if (!first || typeof first.message?.content !== "string") {
|
|
767
|
-
throw unavailable(this.config, "Chat completion response had no message content.", status);
|
|
768
|
-
}
|
|
769
|
-
const usage = payload.usage ?? {};
|
|
770
|
-
return {
|
|
771
|
-
model: typeof payload.model === "string" ? payload.model : request.model,
|
|
772
|
-
content: first.message.content,
|
|
773
|
-
finishReason: typeof first.finish_reason === "string" ? first.finish_reason : void 0,
|
|
774
|
-
usage: {
|
|
775
|
-
promptTokens: typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : void 0,
|
|
776
|
-
completionTokens: typeof usage.completion_tokens === "number" ? usage.completion_tokens : void 0,
|
|
777
|
-
totalTokens: typeof usage.total_tokens === "number" ? usage.total_tokens : void 0
|
|
778
|
-
},
|
|
779
|
-
raw: body
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
};
|
|
783
|
-
async function streamChatCompletion(config, request, onChunk, env = process.env) {
|
|
784
|
-
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
785
|
-
const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
786
|
-
let response;
|
|
787
|
-
try {
|
|
788
|
-
response = await fetch(`${baseUrl}/chat/completions`, {
|
|
789
|
-
method: "POST",
|
|
790
|
-
headers: {
|
|
791
|
-
accept: "text/event-stream",
|
|
792
|
-
"content-type": "application/json",
|
|
793
|
-
...authHeaders(config, env)
|
|
794
|
-
},
|
|
795
|
-
body: JSON.stringify({
|
|
796
|
-
model: request.model,
|
|
797
|
-
messages: request.messages,
|
|
798
|
-
stream: true,
|
|
799
|
-
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
800
|
-
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {}
|
|
801
|
-
}),
|
|
802
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
803
|
-
});
|
|
804
|
-
} catch (error) {
|
|
805
|
-
throw providerTransportError({ provider: config.name, operation: "stream", cause: error });
|
|
806
|
-
}
|
|
807
|
-
if (!response.ok) {
|
|
808
|
-
const body = await response.json().catch(() => void 0);
|
|
809
|
-
throw providerHttpError({ provider: config.name, operation: "stream", status: response.status, body });
|
|
810
|
-
}
|
|
811
|
-
if (!response.body) {
|
|
812
|
-
throw providerHttpError({
|
|
813
|
-
provider: config.name,
|
|
814
|
-
operation: "stream",
|
|
815
|
-
status: 500,
|
|
816
|
-
body: { error: { message: "streaming is not supported by this endpoint" } }
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
let content = "";
|
|
820
|
-
let model = request.model;
|
|
821
|
-
let finishReason;
|
|
822
|
-
const decoder = new TextDecoder();
|
|
823
|
-
let buffer = "";
|
|
824
|
-
for await (const piece of response.body) {
|
|
825
|
-
buffer += decoder.decode(piece, { stream: true });
|
|
826
|
-
let newline = buffer.indexOf("\n");
|
|
827
|
-
while (newline >= 0) {
|
|
828
|
-
const line = buffer.slice(0, newline).trim();
|
|
829
|
-
buffer = buffer.slice(newline + 1);
|
|
830
|
-
newline = buffer.indexOf("\n");
|
|
831
|
-
if (line.length === 0 || line.startsWith(":")) continue;
|
|
832
|
-
const data = line.startsWith("data:") ? line.slice(5).trim() : line;
|
|
833
|
-
if (data === "[DONE]") continue;
|
|
834
|
-
let event;
|
|
835
|
-
try {
|
|
836
|
-
event = JSON.parse(data);
|
|
837
|
-
} catch {
|
|
838
|
-
continue;
|
|
839
|
-
}
|
|
840
|
-
if (typeof event.model === "string") model = event.model;
|
|
841
|
-
const choices = Array.isArray(event.choices) ? event.choices : [];
|
|
842
|
-
const first = choices[0];
|
|
843
|
-
const delta = typeof first?.delta?.content === "string" ? first.delta.content : "";
|
|
844
|
-
if (delta.length > 0) content += delta;
|
|
845
|
-
if (typeof first?.finish_reason === "string") finishReason = first.finish_reason;
|
|
846
|
-
onChunk({ delta, ...finishReason !== void 0 ? { finishReason } : {}, raw: event });
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
return { model, content, ...finishReason !== void 0 ? { finishReason } : {} };
|
|
850
|
-
}
|
|
851
|
-
function normalizeModalities(value) {
|
|
852
|
-
const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
|
|
853
|
-
if (!list) return void 0;
|
|
854
|
-
const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
|
|
855
|
-
const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
|
|
856
|
-
return modalities.length > 0 ? modalities : void 0;
|
|
857
|
-
}
|
|
858
|
-
function normalizeBaseUrl(baseUrl) {
|
|
859
|
-
return baseUrl.replace(/\/+$/, "");
|
|
860
|
-
}
|
|
861
|
-
function extractModelEntries(body, config) {
|
|
862
|
-
if (Array.isArray(body)) return body;
|
|
863
|
-
if (body !== null && typeof body === "object" && Array.isArray(body.data)) {
|
|
864
|
-
return body.data;
|
|
865
|
-
}
|
|
866
|
-
throw new ProviderError(
|
|
867
|
-
"PROVIDER_UNAVAILABLE",
|
|
868
|
-
`Provider "${config.name}" returned an unrecognized model list shape.`,
|
|
869
|
-
{ details: { endpoint: "models" } }
|
|
870
|
-
);
|
|
871
|
-
}
|
|
872
|
-
function unavailable(config, message, status) {
|
|
873
|
-
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
|
|
874
|
-
details: { status }
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
function inferHealth(error) {
|
|
878
|
-
const status = error instanceof ProviderError && typeof error.details?.status === "number" ? error.details.status : void 0;
|
|
879
|
-
return status !== void 0 && status >= 500 ? "degraded" : "unreachable";
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
// src/providers/omnihilbras/index.ts
|
|
883
|
-
var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
|
|
884
|
-
gateway;
|
|
885
|
-
async listModels() {
|
|
886
|
-
const raw = await this.rawModelsResponse();
|
|
887
|
-
await this.extractGatewayMetadata(raw);
|
|
888
|
-
const models = await super.listModels();
|
|
889
|
-
return this.gateway ? models.map((model) => ({ ...model, metadata: { ...model.metadata, gateway: this.gateway } })) : models;
|
|
890
|
-
}
|
|
891
|
-
/**
|
|
892
|
-
* Run the full connect flow. Throws on authentication or transport failure
|
|
893
|
-
* (use healthCheck() for a non-throwing probe).
|
|
894
|
-
*/
|
|
895
|
-
/**
|
|
896
|
-
* Gateway metadata reported alongside the model list (gateway version, tier,
|
|
897
|
-
* region…), kept separate from per-model metadata (§11 — provider metadata).
|
|
898
|
-
*/
|
|
899
|
-
gatewayMetadata() {
|
|
900
|
-
return this.gateway;
|
|
901
|
-
}
|
|
902
|
-
async extractGatewayMetadata(body) {
|
|
903
|
-
if (body !== null && typeof body === "object") {
|
|
904
|
-
const gateway = body.gateway;
|
|
905
|
-
if (gateway !== null && typeof gateway === "object") {
|
|
906
|
-
this.gateway = gateway;
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
async connectAndRegister(registry) {
|
|
911
|
-
const apiKey = resolveApiKey(this.config);
|
|
912
|
-
const discovered = await this.listModels();
|
|
913
|
-
const seen = /* @__PURE__ */ new Set();
|
|
914
|
-
const models = [];
|
|
915
|
-
const duplicates = [];
|
|
916
|
-
for (const model of discovered) {
|
|
917
|
-
if (seen.has(model.id)) {
|
|
918
|
-
duplicates.push(model.id);
|
|
919
|
-
continue;
|
|
920
|
-
}
|
|
921
|
-
seen.add(model.id);
|
|
922
|
-
models.push({
|
|
923
|
-
...model,
|
|
924
|
-
provider: this.config.name,
|
|
925
|
-
status: "available",
|
|
926
|
-
metadata: { ...model.metadata, gateway: "omnihilbras" }
|
|
927
|
-
});
|
|
928
|
-
}
|
|
929
|
-
if (registry) {
|
|
930
|
-
for (const model of models) registry.register(model);
|
|
931
|
-
}
|
|
932
|
-
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)` : "") + ".";
|
|
933
|
-
const status = {
|
|
934
|
-
name: this.config.name,
|
|
935
|
-
type: this.config.type,
|
|
936
|
-
baseUrl: this.config.baseUrl,
|
|
937
|
-
health: "healthy",
|
|
938
|
-
connected: true,
|
|
939
|
-
modelCount: models.length,
|
|
940
|
-
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
941
|
-
message
|
|
942
|
-
};
|
|
943
|
-
return { status, models, duplicates, registered: registry ? models.length : 0 };
|
|
944
|
-
}
|
|
945
|
-
};
|
|
946
|
-
|
|
947
|
-
// src/providers/factory.ts
|
|
948
|
-
var OPENAI_COMPATIBLE_TYPES = [
|
|
949
|
-
"openai-compatible",
|
|
950
|
-
"openrouter",
|
|
951
|
-
"local",
|
|
952
|
-
"custom"
|
|
953
|
-
];
|
|
954
|
-
function createProvider(config) {
|
|
955
|
-
if (config.type === "omnihilbras") {
|
|
956
|
-
return new OmniHilbrasProvider(config);
|
|
957
|
-
}
|
|
958
|
-
if (OPENAI_COMPATIBLE_TYPES.includes(config.type)) {
|
|
959
|
-
return new OpenAICompatibleProvider(config);
|
|
960
|
-
}
|
|
961
|
-
throw new OmniNodeError("NOT_IMPLEMENTED", `Provider type "${config.type}" has no adapter yet.`);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
360
|
// src/registry/index.ts
|
|
965
361
|
function registryKey(provider, id) {
|
|
966
362
|
return `${provider}:${id}`;
|
|
@@ -1045,183 +441,24 @@ function appendToConfigList(listKey, item) {
|
|
|
1045
441
|
return configPath;
|
|
1046
442
|
}
|
|
1047
443
|
|
|
1048
|
-
// src/
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
for (const provider2 of providers) {
|
|
1061
|
-
const key = provider2.apiKeyEnvVar ? `$${provider2.apiKeyEnvVar}` : "none";
|
|
1062
|
-
const enabled = provider2.enabled === false ? " (disabled)" : "";
|
|
1063
|
-
console.log(
|
|
1064
|
-
`${provider2.name} type=${provider2.type} base_url=${provider2.baseUrl} key=${key}${enabled}`
|
|
1065
|
-
);
|
|
1066
|
-
}
|
|
1067
|
-
});
|
|
1068
|
-
provider.command("add").description(
|
|
1069
|
-
"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."
|
|
1070
|
-
).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(
|
|
1071
|
-
"--type <type>",
|
|
1072
|
-
"Provider type: openai-compatible, omnihilbras, openrouter, local or custom.",
|
|
1073
|
-
"openai-compatible"
|
|
1074
|
-
).option(
|
|
1075
|
-
"--api-key-env-var <envVar>",
|
|
1076
|
-
"Environment variable holding the API key. Keys are never written to the config file."
|
|
1077
|
-
).action(async (options) => addProvider(options));
|
|
1078
|
-
provider.command("test <name>").description("Test connectivity, authentication and model discovery for a configured provider.").option(
|
|
1079
|
-
"--connect",
|
|
1080
|
-
"Run the full provider connect flow (authenticate, fetch, validate, register) where the adapter supports it."
|
|
1081
|
-
).action(async (name, options) => testProvider(name, options));
|
|
1082
|
-
provider.command("remove <name>").description("Remove a provider (planned for Phase 1 follow-up).").action(() => {
|
|
1083
|
-
throw notImplemented("omninode provider remove", "a Phase 1 follow-up");
|
|
1084
|
-
});
|
|
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);
|
|
1085
456
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
}
|
|
1091
|
-
const candidate = providerConfigSchema.safeParse({
|
|
1092
|
-
name: options.name,
|
|
1093
|
-
type: options.type,
|
|
1094
|
-
base_url: options.baseUrl,
|
|
1095
|
-
...options.apiKeyEnvVar !== void 0 ? { api_key_env_var: options.apiKeyEnvVar } : {}
|
|
1096
|
-
});
|
|
1097
|
-
if (!candidate.success) {
|
|
1098
|
-
const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
1099
|
-
throw new OmniNodeError("CLI_USAGE", `Invalid provider options: ${issues}`);
|
|
1100
|
-
}
|
|
1101
|
-
const newProvider = candidate.data;
|
|
1102
|
-
const existing = loadConfig();
|
|
1103
|
-
if (existing.project.providers.some((p) => p.name === newProvider.name)) {
|
|
1104
|
-
throw new OmniNodeError(
|
|
1105
|
-
"CLI_USAGE",
|
|
1106
|
-
`Provider "${newProvider.name}" already exists in ${findConfigFile() ?? "omninode.yaml"}.`
|
|
1107
|
-
);
|
|
1108
|
-
}
|
|
1109
|
-
const writtenPath = appendToConfigList("providers", newProvider);
|
|
1110
|
-
console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${writtenPath}.`);
|
|
1111
|
-
if (newProvider.api_key_env_var) {
|
|
1112
|
-
console.log(
|
|
1113
|
-
`Set $${newProvider.api_key_env_var} in your environment, then run: omninode provider test ${newProvider.name}`
|
|
1114
|
-
);
|
|
1115
|
-
} else {
|
|
1116
|
-
console.log(`Run \`omninode provider test ${newProvider.name}\` to verify connectivity.`);
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
async function testProvider(name, options = {}) {
|
|
1120
|
-
const config = loadConfig();
|
|
1121
|
-
const providerConfig = config.project.providers.find((p) => p.name === name);
|
|
1122
|
-
if (!providerConfig) {
|
|
1123
|
-
const available = config.project.providers.map((p) => p.name).join(", ") || "(none)";
|
|
1124
|
-
throw new ProviderError(
|
|
1125
|
-
"PROVIDER_NOT_FOUND",
|
|
1126
|
-
`Provider "${name}" is not configured. Configured providers: ${available}.`
|
|
1127
|
-
);
|
|
1128
|
-
}
|
|
1129
|
-
const provider = createProvider(providerConfig);
|
|
1130
|
-
console.log(`Testing provider "${name}" (${providerConfig.type}) at ${provider.config.baseUrl}`);
|
|
1131
|
-
if (options.connect && provider instanceof OmniHilbrasProvider) {
|
|
1132
|
-
const registry = new ModelRegistry();
|
|
1133
|
-
try {
|
|
1134
|
-
const result = await provider.connectAndRegister(registry);
|
|
1135
|
-
printProviderStatus(result.status);
|
|
1136
|
-
console.log(` registered: ${result.registered}`);
|
|
1137
|
-
} catch (error) {
|
|
1138
|
-
console.log(` connect failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1139
|
-
process.exitCode = 1;
|
|
1140
|
-
}
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
const health = await provider.healthCheck();
|
|
1144
|
-
printProviderStatus(health);
|
|
1145
|
-
if (!health.connected) process.exitCode = 1;
|
|
1146
|
-
}
|
|
1147
|
-
function printProviderStatus(status) {
|
|
1148
|
-
console.log(` health: ${status.health}`);
|
|
1149
|
-
console.log(` connected: ${status.connected}`);
|
|
1150
|
-
console.log(` models: ${status.modelCount}`);
|
|
1151
|
-
if (status.message) console.log(` detail: ${status.message}`);
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
// src/cli/commands/models.ts
|
|
1155
|
-
var CAPABILITIES = [
|
|
1156
|
-
"chat",
|
|
1157
|
-
"coding",
|
|
1158
|
-
"tools",
|
|
1159
|
-
"vision",
|
|
1160
|
-
"embeddings"
|
|
1161
|
-
];
|
|
1162
|
-
function registerModelsCommand(program) {
|
|
1163
|
-
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) => {
|
|
1164
|
-
const capability = options.capability;
|
|
1165
|
-
if (capability !== void 0 && !CAPABILITIES.includes(capability)) {
|
|
1166
|
-
throw new OmniNodeError(
|
|
1167
|
-
"CLI_USAGE",
|
|
1168
|
-
`Unknown capability "${options.capability}". Valid capabilities: ${CAPABILITIES.join(", ")}.`
|
|
1169
|
-
);
|
|
1170
|
-
}
|
|
1171
|
-
const config = loadConfig();
|
|
1172
|
-
const selected = config.project.providers.filter(
|
|
1173
|
-
(p) => providerName ? p.name === providerName : true
|
|
1174
|
-
);
|
|
1175
|
-
if (selected.length === 0) {
|
|
1176
|
-
throw new OmniNodeError(
|
|
1177
|
-
"PROVIDER_NOT_FOUND",
|
|
1178
|
-
providerName ? `Provider "${providerName}" is not configured.` : "No providers configured. Add one with `omninode provider add`."
|
|
1179
|
-
);
|
|
1180
|
-
}
|
|
1181
|
-
const registry = new ModelRegistry();
|
|
1182
|
-
for (const providerConfig of selected) {
|
|
1183
|
-
try {
|
|
1184
|
-
const provider = createProvider(providerConfig);
|
|
1185
|
-
const discovered = await registry.discoverFrom(provider);
|
|
1186
|
-
console.log(`${providerConfig.name}: ${discovered.length} model(s)`);
|
|
1187
|
-
} catch (error) {
|
|
1188
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1189
|
-
console.log(`${providerConfig.name}: discovery failed \u2014 ${message}`);
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
const models = registry.list({
|
|
1193
|
-
...providerName !== void 0 ? { provider: providerName } : {},
|
|
1194
|
-
...capability !== void 0 ? { capability } : {}
|
|
1195
|
-
});
|
|
1196
|
-
if (models.length === 0) {
|
|
1197
|
-
console.log("No models found.");
|
|
1198
|
-
return;
|
|
1199
|
-
}
|
|
1200
|
-
console.log("");
|
|
1201
|
-
for (const model of models) {
|
|
1202
|
-
console.log(`${model.provider}:${model.id}`);
|
|
1203
|
-
}
|
|
1204
|
-
});
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
// src/agents/process/index.ts
|
|
1208
|
-
import { spawn } from "child_process";
|
|
1209
|
-
|
|
1210
|
-
// src/agent-protocol/version.ts
|
|
1211
|
-
var PROTOCOL_V2 = "omninode-agent-protocol/2";
|
|
1212
|
-
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
1213
|
-
"omninode-agent-protocol/1",
|
|
1214
|
-
PROTOCOL_V2
|
|
1215
|
-
];
|
|
1216
|
-
var MAX_MESSAGE_BYTES = 1048576;
|
|
1217
|
-
function isSupportedProtocol(protocol) {
|
|
1218
|
-
return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
// src/agent-protocol/types.ts
|
|
1222
|
-
function newMessageId(prefix = "msg") {
|
|
1223
|
-
const random = Math.random().toString(36).slice(2, 10);
|
|
1224
|
-
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}`;
|
|
1225
462
|
}
|
|
1226
463
|
function envelope(type, payload, ids = {}) {
|
|
1227
464
|
return {
|
|
@@ -1613,7 +850,7 @@ function buildChildEnv(config) {
|
|
|
1613
850
|
}
|
|
1614
851
|
|
|
1615
852
|
// src/agents/process/index.ts
|
|
1616
|
-
var
|
|
853
|
+
var DEFAULT_TIMEOUT_MS = 6e5;
|
|
1617
854
|
var KILL_GRACE_MS = 5e3;
|
|
1618
855
|
var DEFAULT_MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
|
|
1619
856
|
function composeTaskText(input) {
|
|
@@ -1676,7 +913,7 @@ var ProcessAgent = class {
|
|
|
1676
913
|
throw new AgentError("AGENT_NOT_FOUND", `Agent "${this.config.name}" has no command configured.`);
|
|
1677
914
|
}
|
|
1678
915
|
const mode = this.config.inputMode ?? "stdin";
|
|
1679
|
-
const timeoutMs = this.config.timeoutMs ??
|
|
916
|
+
const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1680
917
|
return new Promise((resolve2, reject) => {
|
|
1681
918
|
let cwd;
|
|
1682
919
|
try {
|
|
@@ -2035,138 +1272,57 @@ function buildAgentRegistry(config, log = logger) {
|
|
|
2035
1272
|
return registry;
|
|
2036
1273
|
}
|
|
2037
1274
|
|
|
2038
|
-
// src/
|
|
2039
|
-
var
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
const config = loadConfig();
|
|
2044
|
-
const agents = config.project.agents;
|
|
2045
|
-
if (agents.length === 0) {
|
|
2046
|
-
console.log("No agents configured. Add one with `omninode agent add` or edit omninode.yaml.");
|
|
2047
|
-
return;
|
|
2048
|
-
}
|
|
2049
|
-
for (const agent2 of agents) {
|
|
2050
|
-
const command = agent2.command ?? "(no command)";
|
|
2051
|
-
console.log(
|
|
2052
|
-
`${agent2.name} integration=${agent2.integration} command=${command} input=${agent2.inputMode ?? "stdin"}`
|
|
2053
|
-
);
|
|
2054
|
-
}
|
|
2055
|
-
});
|
|
2056
|
-
agent.command("add").description(
|
|
2057
|
-
"Register a CLI agent in omninode.yaml. Agents run as child processes (stdin/stdout); native adapters come in a later phase."
|
|
2058
|
-
).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(
|
|
2059
|
-
"--input-mode <mode>",
|
|
2060
|
-
"How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
|
|
2061
|
-
"stdin"
|
|
2062
|
-
).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(
|
|
2063
|
-
"--timeout-ms <ms>",
|
|
2064
|
-
"Per-task timeout in milliseconds.",
|
|
2065
|
-
(value) => Number(value)
|
|
2066
|
-
).action(async (options) => addAgent(options));
|
|
2067
|
-
agent.command("test <name>").description("Verify the OmniNode \u2194 agent connection by sending a trivial task.").action(async (name) => testAgent(name));
|
|
2068
|
-
}
|
|
2069
|
-
async function addAgent(options) {
|
|
2070
|
-
const candidate = agentConfigSchema.safeParse({
|
|
2071
|
-
name: options.name,
|
|
2072
|
-
type: "cli",
|
|
2073
|
-
command: options.command,
|
|
2074
|
-
...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
|
|
2075
|
-
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
2076
|
-
input_mode: options.inputMode,
|
|
2077
|
-
...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {},
|
|
2078
|
-
...options.envPolicy !== void 0 ? { env_policy: options.envPolicy } : {},
|
|
2079
|
-
...options.allowEnv !== void 0 && options.allowEnv.length > 0 ? { env_allowlist: options.allowEnv } : {},
|
|
2080
|
-
...options.denyEnv !== void 0 && options.denyEnv.length > 0 ? { env_denylist: options.denyEnv } : {},
|
|
2081
|
-
...options.maxOutputBytes !== void 0 ? { max_output_bytes: options.maxOutputBytes } : {},
|
|
2082
|
-
...options.allowExternalCwd !== void 0 ? { allow_external_cwd: options.allowExternalCwd } : {}
|
|
2083
|
-
});
|
|
2084
|
-
if (!candidate.success) {
|
|
2085
|
-
const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
2086
|
-
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);
|
|
2087
1280
|
}
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
if (existing.project.agents.some((a) => a.name === newAgent.name)) {
|
|
2091
|
-
throw new OmniNodeError(
|
|
2092
|
-
"CLI_USAGE",
|
|
2093
|
-
`Agent "${newAgent.name}" already exists in omninode.yaml. Remove it first or pick another name.`
|
|
2094
|
-
);
|
|
1281
|
+
registerAll(roles) {
|
|
1282
|
+
for (const role of roles) this.register(role);
|
|
2095
1283
|
}
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
console.log(`Run \`omninode agent test ${newAgent.name}\` to verify the connection.`);
|
|
2099
|
-
}
|
|
2100
|
-
async function testAgent(name) {
|
|
2101
|
-
const config = loadConfig();
|
|
2102
|
-
const agentConfig = config.project.agents.find((a) => a.name === name);
|
|
2103
|
-
if (!agentConfig) {
|
|
2104
|
-
const available = config.project.agents.map((a) => a.name).join(", ") || "(none)";
|
|
2105
|
-
throw new AgentError(
|
|
2106
|
-
"AGENT_NOT_FOUND",
|
|
2107
|
-
`Agent "${name}" is not configured. Configured agents: ${available}.`
|
|
2108
|
-
);
|
|
1284
|
+
get(id) {
|
|
1285
|
+
return this.roles.get(id);
|
|
2109
1286
|
}
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
`Testing agent "${name}" (${agent.info.integration}, input=${agentConfig.inputMode ?? "stdin"}) \u2014 sending a trivial task.`
|
|
2113
|
-
);
|
|
2114
|
-
const result = await agent.run({
|
|
2115
|
-
taskId: `test-${Date.now()}`,
|
|
2116
|
-
objective: "Reply with the single word: OK"
|
|
2117
|
-
});
|
|
2118
|
-
console.log(` status: ${result.status}`);
|
|
2119
|
-
if (result.exitCode !== void 0) console.log(` exit: ${result.exitCode}`);
|
|
2120
|
-
if (result.summary) console.log(` output: ${result.summary}`);
|
|
2121
|
-
if (result.rawOutput && result.rawOutput !== result.summary) {
|
|
2122
|
-
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));
|
|
2123
1289
|
}
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
function registerRoleCommands(program) {
|
|
2130
|
-
const role = program.command("role").description("Inspect roles defined in omninode.yaml.");
|
|
2131
|
-
role.command("list").description("List configured roles.").action(async () => {
|
|
2132
|
-
const config = loadConfig();
|
|
2133
|
-
const roles = config.project.roles;
|
|
2134
|
-
if (roles.length === 0) {
|
|
2135
|
-
console.log("No roles configured. Add one under `project.roles` in omninode.yaml.");
|
|
2136
|
-
return;
|
|
2137
|
-
}
|
|
2138
|
-
for (const role2 of roles) {
|
|
2139
|
-
console.log(`${role2.id} ${role2.name}`);
|
|
2140
|
-
for (const responsibility of role2.responsibilities) {
|
|
2141
|
-
console.log(` - ${responsibility}`);
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2144
|
-
});
|
|
1290
|
+
};
|
|
1291
|
+
function buildRoleRegistry(config) {
|
|
1292
|
+
const registry = new RoleRegistry();
|
|
1293
|
+
registry.registerAll(config.project.roles);
|
|
1294
|
+
return registry;
|
|
2145
1295
|
}
|
|
2146
1296
|
|
|
2147
1297
|
// src/persistence/json-file-store.ts
|
|
2148
|
-
import { open, mkdir, readFile, rename, stat } from "fs/promises";
|
|
1298
|
+
import { open, mkdir, readFile, rename, stat, writeFile } from "fs/promises";
|
|
2149
1299
|
import { dirname } from "path";
|
|
2150
1300
|
var SCHEMA_VERSION = 2;
|
|
2151
|
-
async function writeFileAtomic(target, contents) {
|
|
1301
|
+
async function writeFileAtomic(target, contents, durability = "rename") {
|
|
2152
1302
|
const tmp = `${target}.tmp`;
|
|
2153
1303
|
await mkdir(dirname(target), { recursive: true });
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
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");
|
|
2160
1314
|
}
|
|
2161
1315
|
await rename(tmp, target);
|
|
2162
|
-
|
|
2163
|
-
const dir = await open(dirname(target), "r");
|
|
1316
|
+
if (durability === "fsync") {
|
|
2164
1317
|
try {
|
|
2165
|
-
await
|
|
2166
|
-
|
|
2167
|
-
|
|
1318
|
+
const dir = await open(dirname(target), "r");
|
|
1319
|
+
try {
|
|
1320
|
+
await dir.sync();
|
|
1321
|
+
} finally {
|
|
1322
|
+
await dir.close();
|
|
1323
|
+
}
|
|
1324
|
+
} catch {
|
|
2168
1325
|
}
|
|
2169
|
-
} catch {
|
|
2170
1326
|
}
|
|
2171
1327
|
}
|
|
2172
1328
|
async function inspectDocument(path) {
|
|
@@ -2236,6 +1392,7 @@ async function readDocument(path, onCorrupt, quarantine = true) {
|
|
|
2236
1392
|
var JsonFileStore = class {
|
|
2237
1393
|
filePath;
|
|
2238
1394
|
schemaVersion;
|
|
1395
|
+
durability;
|
|
2239
1396
|
onCorrupt;
|
|
2240
1397
|
quarantine;
|
|
2241
1398
|
log;
|
|
@@ -2243,6 +1400,7 @@ var JsonFileStore = class {
|
|
|
2243
1400
|
constructor(options) {
|
|
2244
1401
|
this.filePath = `${options.directory ?? `${process.cwd()}/.omninode`}/${options.fileName}`;
|
|
2245
1402
|
this.schemaVersion = options.schemaVersion ?? SCHEMA_VERSION;
|
|
1403
|
+
this.durability = options.durability ?? "rename";
|
|
2246
1404
|
this.onCorrupt = options.onCorrupt;
|
|
2247
1405
|
this.quarantine = options.quarantine ?? true;
|
|
2248
1406
|
this.log = logger.child({ component: "persistence", file: options.fileName });
|
|
@@ -2257,7 +1415,7 @@ var JsonFileStore = class {
|
|
|
2257
1415
|
async writeAll(items) {
|
|
2258
1416
|
const document = { schemaVersion: this.schemaVersion, items };
|
|
2259
1417
|
await writeFileAtomic(this.filePath, `${JSON.stringify(document, null, 2)}
|
|
2260
|
-
|
|
1418
|
+
`, this.durability);
|
|
2261
1419
|
}
|
|
2262
1420
|
/** Serializes an async operation against this file. */
|
|
2263
1421
|
synchronized(operation) {
|
|
@@ -2279,711 +1437,1137 @@ var JsonFileStore = class {
|
|
|
2279
1437
|
}
|
|
2280
1438
|
};
|
|
2281
1439
|
|
|
2282
|
-
// src/
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
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;
|
|
2326
1507
|
}
|
|
2327
|
-
|
|
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;
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
|
|
1566
|
+
// src/memory/remembera.ts
|
|
1567
|
+
var RememberaMemoryProvider = class {
|
|
1568
|
+
name = "remembera";
|
|
1569
|
+
baseUrl;
|
|
1570
|
+
apiKeyEnvVar;
|
|
1571
|
+
env;
|
|
2328
1572
|
log;
|
|
2329
|
-
|
|
2330
|
-
|
|
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" });
|
|
2331
1578
|
}
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
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}.`);
|
|
2335
1616
|
}
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
`Role "${input.role}" is not defined. Add it under project.roles in omninode.yaml.`
|
|
2340
|
-
);
|
|
1617
|
+
const entries = body.entries;
|
|
1618
|
+
if (!Array.isArray(entries)) {
|
|
1619
|
+
throw new MemoryError("Remembera query response had no entries array.");
|
|
2341
1620
|
}
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
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}.`);
|
|
2347
1627
|
}
|
|
2348
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2349
|
-
const task = {
|
|
2350
|
-
id: generateTaskId(),
|
|
2351
|
-
objective: input.objective.trim(),
|
|
2352
|
-
status: "created",
|
|
2353
|
-
attempt: 0,
|
|
2354
|
-
createdAt: now,
|
|
2355
|
-
updatedAt: now,
|
|
2356
|
-
...input.project !== void 0 ? { project: input.project } : {},
|
|
2357
|
-
...input.role !== void 0 ? { role: input.role } : {},
|
|
2358
|
-
...input.agent !== void 0 ? { agent: input.agent } : {},
|
|
2359
|
-
...input.context !== void 0 ? { context: input.context } : {}
|
|
2360
|
-
};
|
|
2361
|
-
await this.options.store.save(task);
|
|
2362
|
-
this.log.info(`Created task ${task.id}.`);
|
|
2363
|
-
await this.audit("task.created", task.id, { objective: task.objective, project: task.project });
|
|
2364
|
-
return task;
|
|
2365
1628
|
}
|
|
2366
|
-
async
|
|
2367
|
-
const
|
|
2368
|
-
|
|
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
|
+
}
|
|
2369
1641
|
}
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
const task = await this.mustGet(id);
|
|
2375
|
-
if (task.status !== "created" && task.status !== "queued") {
|
|
2376
|
-
throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
|
|
1642
|
+
toEntry(raw) {
|
|
1643
|
+
if (raw === null || typeof raw !== "object") {
|
|
1644
|
+
this.log.warn("Skipping malformed Remembera memory entry.");
|
|
1645
|
+
return void 0;
|
|
2377
1646
|
}
|
|
2378
|
-
|
|
2379
|
-
|
|
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;
|
|
2380
1651
|
}
|
|
2381
|
-
const
|
|
2382
|
-
|
|
2383
|
-
|
|
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
|
+
}
|
|
1663
|
+
};
|
|
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
|
+
);
|
|
2384
1676
|
}
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
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;
|
|
1704
|
+
}
|
|
1705
|
+
get providerName() {
|
|
1706
|
+
return this.provider.name;
|
|
1707
|
+
}
|
|
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
|
|
2393
1719
|
});
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
}
|
|
1720
|
+
} catch (error) {
|
|
1721
|
+
return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
|
|
2397
1722
|
}
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
}
|
|
2418
|
-
const
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
attempt,
|
|
2429
|
-
startedAt,
|
|
2430
|
-
finishedAt,
|
|
2431
|
-
outcome,
|
|
2432
|
-
error: message
|
|
2433
|
-
}
|
|
1723
|
+
return this.format(entries);
|
|
1724
|
+
}
|
|
1725
|
+
/** Prompt-ready context text (v1 convenience wrapper). */
|
|
1726
|
+
async gatherContext(query) {
|
|
1727
|
+
return (await this.contextFor(query)).text;
|
|
1728
|
+
}
|
|
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
|
|
1742
|
+
});
|
|
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]
|
|
2434
1753
|
});
|
|
2435
|
-
this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
|
|
2436
|
-
if (retryable) {
|
|
2437
|
-
if (backoffMs > 0) await sleep(backoffMs * attempt);
|
|
2438
|
-
continue;
|
|
2439
|
-
}
|
|
2440
|
-
if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
|
|
2441
|
-
return current;
|
|
2442
1754
|
}
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
current = currentState;
|
|
2446
|
-
break;
|
|
2447
|
-
}
|
|
2448
|
-
const result = {
|
|
2449
|
-
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
2450
|
-
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
2451
|
-
// Keep the verbatim output (bounded) for report extraction and audit.
|
|
2452
|
-
...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
|
|
2453
|
-
...output.error !== void 0 ? { error: output.error } : {},
|
|
2454
|
-
finishedAt
|
|
2455
|
-
};
|
|
2456
|
-
const finalStatus = output.status === "completed" ? "completed" : "failed";
|
|
2457
|
-
current = await this.transition(current, finalStatus, {
|
|
2458
|
-
attempt,
|
|
2459
|
-
result,
|
|
2460
|
-
execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
|
|
2461
|
-
});
|
|
2462
|
-
this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
|
|
2463
|
-
break;
|
|
2464
|
-
}
|
|
2465
|
-
if (this.options.memory) {
|
|
2466
|
-
await this.options.memory.recordTaskOutcome(current);
|
|
1755
|
+
} catch (error) {
|
|
1756
|
+
this.handleFailure("store", error, void 0);
|
|
2467
1757
|
}
|
|
2468
|
-
return current;
|
|
2469
1758
|
}
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
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]);
|
|
2474
1764
|
}
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
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}`);
|
|
2478
1775
|
}
|
|
2479
|
-
|
|
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 };
|
|
2480
1780
|
}
|
|
2481
|
-
/**
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
`
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
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 }
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
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
|
+
});
|
|
2501
1809
|
}
|
|
2502
1810
|
async get(id) {
|
|
2503
|
-
return this.
|
|
1811
|
+
return (await this.readAll()).find((r) => r.id === id);
|
|
2504
1812
|
}
|
|
2505
|
-
async list(filter) {
|
|
2506
|
-
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 ?? ""));
|
|
2507
1815
|
}
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
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" });
|
|
2514
1822
|
}
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
const updated = {
|
|
2522
|
-
...task,
|
|
2523
|
-
status: to,
|
|
2524
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2525
|
-
...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
|
|
2526
|
-
...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
|
|
2527
|
-
...executions !== void 0 ? { executions } : {},
|
|
2528
|
-
...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
|
|
2529
|
-
};
|
|
2530
|
-
await this.options.store.save(updated);
|
|
2531
|
-
await this.audit(`task.${to === "queued" ? "queued" : to}`, updated.id, {
|
|
2532
|
-
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];
|
|
2533
1829
|
});
|
|
2534
|
-
return updated;
|
|
2535
1830
|
}
|
|
2536
|
-
async
|
|
2537
|
-
await this.
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
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));
|
|
2543
1838
|
}
|
|
2544
1839
|
};
|
|
2545
|
-
|
|
2546
|
-
|
|
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
|
+
);
|
|
2547
1890
|
}
|
|
2548
|
-
function
|
|
2549
|
-
|
|
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
|
+
);
|
|
2550
1914
|
}
|
|
2551
|
-
function
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
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
|
+
);
|
|
2557
1944
|
}
|
|
2558
|
-
function
|
|
2559
|
-
|
|
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
|
+
);
|
|
2560
1975
|
}
|
|
2561
1976
|
|
|
2562
|
-
// src/
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
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."
|
|
1986
|
+
);
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
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
|
+
);
|
|
1995
|
+
}
|
|
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);
|
|
2015
|
+
}
|
|
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.");
|
|
2571
2020
|
}
|
|
2572
|
-
|
|
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}`);
|
|
2030
|
+
}
|
|
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
|
+
);
|
|
2038
|
+
}
|
|
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}`
|
|
2044
|
+
);
|
|
2045
|
+
} else {
|
|
2046
|
+
console.log(`Run \`omninode provider test ${newProvider.name}\` to verify connectivity.`);
|
|
2047
|
+
}
|
|
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
|
+
);
|
|
2058
|
+
}
|
|
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();
|
|
2573
2063
|
try {
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
try {
|
|
2578
|
-
await handle.appendFile(`${JSON.stringify(event)}
|
|
2579
|
-
`, "utf8");
|
|
2580
|
-
await handle.sync();
|
|
2581
|
-
} finally {
|
|
2582
|
-
await handle.close();
|
|
2583
|
-
}
|
|
2064
|
+
const result = await provider.connectAndRegister(registry);
|
|
2065
|
+
printProviderStatus(result.status);
|
|
2066
|
+
console.log(` registered: ${result.registered}`);
|
|
2584
2067
|
} catch (error) {
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
);
|
|
2068
|
+
console.log(` connect failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2069
|
+
process.exitCode = 1;
|
|
2588
2070
|
}
|
|
2071
|
+
return;
|
|
2589
2072
|
}
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
}
|
|
2596
|
-
|
|
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
|
+
}
|
|
2083
|
+
|
|
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
|
|
2104
|
+
);
|
|
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
|
+
);
|
|
2597
2110
|
}
|
|
2598
|
-
const
|
|
2111
|
+
const registry = new ModelRegistry();
|
|
2112
|
+
for (const providerConfig of selected) {
|
|
2599
2113
|
try {
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
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}`);
|
|
2603
2120
|
}
|
|
2604
|
-
}
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
}
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2121
|
+
}
|
|
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;
|
|
2129
|
+
}
|
|
2130
|
+
console.log("");
|
|
2131
|
+
for (const model of models) {
|
|
2132
|
+
console.log(`${model.provider}:${model.id}`);
|
|
2133
|
+
}
|
|
2615
2134
|
});
|
|
2616
2135
|
}
|
|
2617
|
-
function defaultMetadata(name, options = {}) {
|
|
2618
|
-
return {
|
|
2619
|
-
name,
|
|
2620
|
-
capabilities: options.capabilities ?? ["retrieve", "store", "search"],
|
|
2621
|
-
relevanceRanking: options.relevanceRanking ?? false,
|
|
2622
|
-
...options.backend !== void 0 ? { backend: options.backend } : {}
|
|
2623
|
-
};
|
|
2624
|
-
}
|
|
2625
2136
|
|
|
2626
|
-
// src/
|
|
2627
|
-
var
|
|
2628
|
-
|
|
2629
|
-
"
|
|
2630
|
-
"
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
"
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
"
|
|
2645
|
-
|
|
2646
|
-
"
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
"
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
"
|
|
2656
|
-
|
|
2657
|
-
"been",
|
|
2658
|
-
"were",
|
|
2659
|
-
"also",
|
|
2660
|
-
"because",
|
|
2661
|
-
"about",
|
|
2662
|
-
"after",
|
|
2663
|
-
"before"
|
|
2664
|
-
]);
|
|
2665
|
-
function keywords(text) {
|
|
2666
|
-
return new Set(
|
|
2667
|
-
(text ?? "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 4 && !STOPWORDS.has(t))
|
|
2668
|
-
);
|
|
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;
|
|
2147
|
+
}
|
|
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
|
+
);
|
|
2153
|
+
}
|
|
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);
|
|
2669
2168
|
}
|
|
2670
|
-
function
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
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}`);
|
|
2676
2187
|
}
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
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
|
+
);
|
|
2680
2195
|
}
|
|
2681
|
-
|
|
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.`);
|
|
2682
2199
|
}
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
relevanceRanking: true,
|
|
2692
|
-
backend: { kind: "json-file", deterministicScoring: true }
|
|
2693
|
-
});
|
|
2694
|
-
}
|
|
2695
|
-
search(text, options = {}) {
|
|
2696
|
-
return defaultSearch(this, text, options);
|
|
2697
|
-
}
|
|
2698
|
-
/** @deprecated v1 alias of {@link retrieve}. */
|
|
2699
|
-
query(query) {
|
|
2700
|
-
return this.retrieve(query);
|
|
2701
|
-
}
|
|
2702
|
-
/** @deprecated v1 alias of {@link store}. */
|
|
2703
|
-
write(entry2) {
|
|
2704
|
-
return this.store(entry2);
|
|
2705
|
-
}
|
|
2706
|
-
async retrieve(query) {
|
|
2707
|
-
const entries = await this.readAll();
|
|
2708
|
-
const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter((entry2) => query.category ? entry2.category === query.category : true).filter(
|
|
2709
|
-
(entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
|
|
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}.`
|
|
2710
2208
|
);
|
|
2711
|
-
if (!query.text || query.text.trim().length === 0) {
|
|
2712
|
-
return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
|
|
2713
|
-
}
|
|
2714
|
-
const queryKeywords = keywords(query.text);
|
|
2715
|
-
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);
|
|
2716
|
-
}
|
|
2717
|
-
store(entry2) {
|
|
2718
|
-
return this.mutate((entries) => {
|
|
2719
|
-
const index = entries.findIndex((e) => e.key === entry2.key);
|
|
2720
|
-
const stamped = {
|
|
2721
|
-
...entry2,
|
|
2722
|
-
createdAt: entry2.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2723
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2724
|
-
};
|
|
2725
|
-
if (index >= 0) entries[index] = stamped;
|
|
2726
|
-
else entries.push(stamped);
|
|
2727
|
-
return [entries, void 0];
|
|
2728
|
-
});
|
|
2729
2209
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2210
|
+
const agent = createAgent(agentConfig);
|
|
2211
|
+
console.log(
|
|
2212
|
+
`Testing agent "${name}" (${agent.info.integration}, input=${agentConfig.inputMode ?? "stdin"}) \u2014 sending a trivial task.`
|
|
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, " ")}`);
|
|
2732
2223
|
}
|
|
2224
|
+
if (result.error) console.log(` error: ${result.error}`);
|
|
2225
|
+
if (result.status !== "completed") process.exitCode = 1;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
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: []
|
|
2733
2261
|
};
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
var
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
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" });
|
|
2268
|
+
}
|
|
2269
|
+
options;
|
|
2741
2270
|
log;
|
|
2742
|
-
|
|
2743
|
-
this.
|
|
2744
|
-
this.apiKeyEnvVar = config.apiKeyEnvVar;
|
|
2745
|
-
this.env = config.env ?? process.env;
|
|
2746
|
-
this.log = log.child({ component: "remembera" });
|
|
2271
|
+
get store() {
|
|
2272
|
+
return this.options.store;
|
|
2747
2273
|
}
|
|
2748
|
-
|
|
2749
|
-
|
|
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;
|
|
2750
2308
|
}
|
|
2751
|
-
|
|
2752
|
-
const
|
|
2753
|
-
return
|
|
2309
|
+
async queue(id) {
|
|
2310
|
+
const task = await this.mustGet(id);
|
|
2311
|
+
return this.transition(task, "queued");
|
|
2754
2312
|
}
|
|
2755
|
-
/**
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
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 } : {}
|
|
2336
|
+
});
|
|
2337
|
+
if (memoryContext.length > 0) {
|
|
2338
|
+
background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
|
|
2763
2339
|
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2340
|
+
}
|
|
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 }
|
|
2358
|
+
});
|
|
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
|
+
}
|
|
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;
|
|
2407
|
+
}
|
|
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);
|
|
2432
|
+
}
|
|
2433
|
+
return current;
|
|
2780
2434
|
}
|
|
2781
|
-
async
|
|
2782
|
-
const
|
|
2783
|
-
if (status
|
|
2784
|
-
throw new
|
|
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.`);
|
|
2785
2439
|
}
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2440
|
+
if (task.agent) {
|
|
2441
|
+
const agent = this.options.agents.get(task.agent);
|
|
2442
|
+
await agent?.cancel?.(task.id);
|
|
2789
2443
|
}
|
|
2790
|
-
return
|
|
2444
|
+
return this.transition(task, "cancelled");
|
|
2791
2445
|
}
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
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}".`
|
|
2454
|
+
);
|
|
2796
2455
|
}
|
|
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;
|
|
2797
2466
|
}
|
|
2798
|
-
async
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
throw new MemoryError(`Remembera request to ${url} failed: ${message}`, { cause: error });
|
|
2467
|
+
async get(id) {
|
|
2468
|
+
return this.options.store.get(id);
|
|
2469
|
+
}
|
|
2470
|
+
async list(filter) {
|
|
2471
|
+
return this.options.store.list(filter);
|
|
2472
|
+
}
|
|
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.`);
|
|
2809
2477
|
}
|
|
2478
|
+
return task;
|
|
2810
2479
|
}
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
}
|
|
2816
|
-
const entry2 = raw;
|
|
2817
|
-
if (typeof entry2.key !== "string" || typeof entry2.content !== "string") {
|
|
2818
|
-
this.log.warn("Skipping Remembera memory entry without key/content.");
|
|
2819
|
-
return void 0;
|
|
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}".`);
|
|
2820
2484
|
}
|
|
2821
|
-
const
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
...
|
|
2827
|
-
...
|
|
2828
|
-
...
|
|
2829
|
-
...
|
|
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 } } : {}
|
|
2830
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;
|
|
2831
2500
|
}
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
`Memory provider "remembera" needs a base_url in omninode.yaml.`
|
|
2844
|
-
);
|
|
2845
|
-
}
|
|
2846
|
-
return new RememberaMemoryProvider({
|
|
2847
|
-
baseUrl: config.baseUrl,
|
|
2848
|
-
apiKeyEnvVar: config.apiKeyEnvVar
|
|
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 } : {}
|
|
2849
2512
|
});
|
|
2850
2513
|
}
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
);
|
|
2514
|
+
};
|
|
2515
|
+
function generateTaskId() {
|
|
2516
|
+
return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
2855
2517
|
}
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
this.log = log.child({ component: "memory", provider: provider.name });
|
|
2864
|
-
this.required = options.required ?? false;
|
|
2865
|
-
this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
|
|
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";
|
|
2866
2525
|
}
|
|
2867
|
-
|
|
2526
|
+
return "failed";
|
|
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;
|
|
2868
2537
|
log;
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
return this.provider;
|
|
2873
|
-
}
|
|
2874
|
-
get providerName() {
|
|
2875
|
-
return this.provider.name;
|
|
2876
|
-
}
|
|
2877
|
-
get isRequired() {
|
|
2878
|
-
return this.required;
|
|
2538
|
+
constructor(directory = `${process.cwd()}/.omninode`, log = logger) {
|
|
2539
|
+
this.filePath = `${directory}/audit.jsonl`;
|
|
2540
|
+
this.log = log.child({ component: "audit" });
|
|
2879
2541
|
}
|
|
2880
|
-
|
|
2881
|
-
async contextFor(query) {
|
|
2882
|
-
let entries;
|
|
2542
|
+
async record(event) {
|
|
2883
2543
|
try {
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
limit: query.limit ?? DEFAULT_ENTRY_LIMIT
|
|
2888
|
-
});
|
|
2544
|
+
await mkdir2(dirname2(this.filePath), { recursive: true });
|
|
2545
|
+
await appendFile(this.filePath, `${JSON.stringify(event)}
|
|
2546
|
+
`, "utf8");
|
|
2889
2547
|
} catch (error) {
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
return this.format(entries);
|
|
2893
|
-
}
|
|
2894
|
-
/** Prompt-ready context text (v1 convenience wrapper). */
|
|
2895
|
-
async gatherContext(query) {
|
|
2896
|
-
return (await this.contextFor(query)).text;
|
|
2897
|
-
}
|
|
2898
|
-
/** Records a finished task's outcome — task history plus "known problems" (§12). */
|
|
2899
|
-
async recordTaskOutcome(task) {
|
|
2900
|
-
const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
|
|
2901
|
-
try {
|
|
2902
|
-
await this.provider.store({
|
|
2903
|
-
key: `task:${task.id}`,
|
|
2904
|
-
scope: "task",
|
|
2905
|
-
category: "tasks",
|
|
2906
|
-
content: `Objective: ${task.objective}
|
|
2907
|
-
Status: ${task.status}
|
|
2908
|
-
Outcome: ${outcome}`,
|
|
2909
|
-
tags: ["task", ...task.agent ? [task.agent] : [], ...task.role ? [task.role] : []],
|
|
2910
|
-
createdAt: task.createdAt
|
|
2911
|
-
});
|
|
2912
|
-
const significant = (task.result?.reports ?? []).flatMap(
|
|
2913
|
-
(report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
|
|
2548
|
+
this.log.warn(
|
|
2549
|
+
`Audit write failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2914
2550
|
);
|
|
2915
|
-
for (const { report, finding } of significant) {
|
|
2916
|
-
await this.provider.store({
|
|
2917
|
-
key: `finding:${report.id}:${finding.id}`,
|
|
2918
|
-
scope: "project",
|
|
2919
|
-
category: "problems",
|
|
2920
|
-
content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
|
|
2921
|
-
tags: ["known-problem", report.agent]
|
|
2922
|
-
});
|
|
2923
|
-
}
|
|
2924
|
-
} catch (error) {
|
|
2925
|
-
this.handleFailure("store", error, void 0);
|
|
2926
|
-
}
|
|
2927
|
-
}
|
|
2928
|
-
format(entries) {
|
|
2929
|
-
if (entries.length === 0) return { text: "", entries, truncated: false };
|
|
2930
|
-
const byScope = /* @__PURE__ */ new Map();
|
|
2931
|
-
for (const entry2 of entries) {
|
|
2932
|
-
byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
|
|
2933
|
-
}
|
|
2934
|
-
const parts = [];
|
|
2935
|
-
for (const scope of ["project", "task", "role", "knowledge"]) {
|
|
2936
|
-
const scopeEntries = byScope.get(scope) ?? [];
|
|
2937
|
-
if (scopeEntries.length === 0) continue;
|
|
2938
|
-
const lines = scopeEntries.map((entry2) => {
|
|
2939
|
-
const category = entry2.category ? `[${entry2.category}] ` : "";
|
|
2940
|
-
return `- ${category}${entry2.content}`;
|
|
2941
|
-
}).join("\n");
|
|
2942
|
-
parts.push(`### Relevant memory (${scope})
|
|
2943
|
-
${lines}`);
|
|
2944
2551
|
}
|
|
2945
|
-
let text = parts.join("\n\n");
|
|
2946
|
-
const truncated = text.length > this.maxContextChars;
|
|
2947
|
-
if (truncated) text = `${text.slice(0, this.maxContextChars - 1)}\u2026`;
|
|
2948
|
-
return { text, entries, truncated };
|
|
2949
2552
|
}
|
|
2950
|
-
/**
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
`Memory is marked required, but ${operation} failed: ${message}`,
|
|
2958
|
-
{ cause: error }
|
|
2959
|
-
);
|
|
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 [];
|
|
2960
2560
|
}
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
}
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
register(role) {
|
|
2970
|
-
this.roles.set(role.id, role);
|
|
2971
|
-
}
|
|
2972
|
-
registerAll(roles) {
|
|
2973
|
-
for (const role of roles) this.register(role);
|
|
2974
|
-
}
|
|
2975
|
-
get(id) {
|
|
2976
|
-
return this.roles.get(id);
|
|
2977
|
-
}
|
|
2978
|
-
list() {
|
|
2979
|
-
return [...this.roles.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
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);
|
|
2980
2569
|
}
|
|
2981
2570
|
};
|
|
2982
|
-
function buildRoleRegistry(config) {
|
|
2983
|
-
const registry = new RoleRegistry();
|
|
2984
|
-
registry.registerAll(config.project.roles);
|
|
2985
|
-
return registry;
|
|
2986
|
-
}
|
|
2987
2571
|
|
|
2988
2572
|
// src/tasks/engine-factory.ts
|
|
2989
2573
|
function createTaskEngine(config, log = logger, directory) {
|
|
@@ -3075,6 +2659,7 @@ function registerTaskCommands(program) {
|
|
|
3075
2659
|
printOutcome(finished);
|
|
3076
2660
|
if (finished.status !== "completed") process.exitCode = 1;
|
|
3077
2661
|
});
|
|
2662
|
+
registerTaskInspect(task);
|
|
3078
2663
|
}
|
|
3079
2664
|
function printOutcome(task, options = {}) {
|
|
3080
2665
|
console.log(` id: ${task.id}`);
|
|
@@ -4130,6 +3715,7 @@ var PipelineEngine = class {
|
|
|
4130
3715
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4131
3716
|
action: "pipeline.run.started",
|
|
4132
3717
|
id: run2.id,
|
|
3718
|
+
pipelineId: def.id,
|
|
4133
3719
|
detail: { pipeline: def.id, objective }
|
|
4134
3720
|
});
|
|
4135
3721
|
const deps = this.effectiveDependencies(def);
|
|
@@ -4170,7 +3756,9 @@ var PipelineEngine = class {
|
|
|
4170
3756
|
combinedContext: contextParts.join("\n\n"),
|
|
4171
3757
|
reports: [...allReports],
|
|
4172
3758
|
pipelineRunId: run2.id,
|
|
4173
|
-
runId: run2.id
|
|
3759
|
+
runId: run2.id,
|
|
3760
|
+
pipelineDefId: def.id,
|
|
3761
|
+
taskId: run2.id
|
|
4174
3762
|
};
|
|
4175
3763
|
return this.executeStep(step, ctx);
|
|
4176
3764
|
})
|
|
@@ -4247,8 +3835,9 @@ ${contribution}`);
|
|
|
4247
3835
|
this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
|
|
4248
3836
|
await this.options.audit?.record({
|
|
4249
3837
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4250
|
-
action: "pipeline.run.finished",
|
|
3838
|
+
action: cancelled ? "pipeline.run.cancelled" : finalStatus === "failed" ? "pipeline.run.failed" : "pipeline.run.finished",
|
|
4251
3839
|
id: run2.id,
|
|
3840
|
+
pipelineId: def.id,
|
|
4252
3841
|
detail: { pipeline: def.id, status: finalStatus }
|
|
4253
3842
|
});
|
|
4254
3843
|
return finished;
|
|
@@ -4329,6 +3918,7 @@ ${contribution}`);
|
|
|
4329
3918
|
agentNames.map(
|
|
4330
3919
|
(name) => this.options.tasks.create({
|
|
4331
3920
|
objective: ctx.objective,
|
|
3921
|
+
...ctx.pipelineRunId !== void 0 ? { pipelineId: ctx.pipelineRunId } : {},
|
|
4332
3922
|
agent: name,
|
|
4333
3923
|
...ctx.role ? { role: ctx.role.id } : {},
|
|
4334
3924
|
...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
|
|
@@ -4371,6 +3961,7 @@ ${contribution}`);
|
|
|
4371
3961
|
async runSingle(step, ctx, agentName) {
|
|
4372
3962
|
const task = await this.options.tasks.create({
|
|
4373
3963
|
objective: ctx.objective,
|
|
3964
|
+
...ctx.pipelineRunId !== void 0 ? { pipelineId: ctx.pipelineRunId } : {},
|
|
4374
3965
|
agent: agentName,
|
|
4375
3966
|
...ctx.role ? { role: ctx.role.id } : {},
|
|
4376
3967
|
...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
|
|
@@ -4441,7 +4032,13 @@ ${contribution}`);
|
|
|
4441
4032
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4442
4033
|
action: "plan.generated",
|
|
4443
4034
|
id: plan.id,
|
|
4444
|
-
|
|
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
|
+
}
|
|
4445
4042
|
});
|
|
4446
4043
|
return {
|
|
4447
4044
|
status: "completed",
|
|
@@ -4475,7 +4072,7 @@ function formatPlan(plan) {
|
|
|
4475
4072
|
}
|
|
4476
4073
|
return lines.join("\n");
|
|
4477
4074
|
}
|
|
4478
|
-
function createDefaultChatFn(providers) {
|
|
4075
|
+
function createDefaultChatFn(providers, options = {}) {
|
|
4479
4076
|
return async (modelRef, messages) => {
|
|
4480
4077
|
const separator = modelRef.indexOf(":");
|
|
4481
4078
|
if (separator <= 0 || separator === modelRef.length - 1) {
|
|
@@ -4493,33 +4090,15 @@ function createDefaultChatFn(providers) {
|
|
|
4493
4090
|
`Provider "${providerName}" (model "${modelRef}") is not configured.`
|
|
4494
4091
|
);
|
|
4495
4092
|
}
|
|
4496
|
-
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
|
+
});
|
|
4497
4097
|
const response = await provider.chat({ model: modelId, messages });
|
|
4498
4098
|
return response.content;
|
|
4499
4099
|
};
|
|
4500
4100
|
}
|
|
4501
4101
|
|
|
4502
|
-
// src/pipelines/store.ts
|
|
4503
|
-
var FilePipelineRunStore = class extends JsonFileStore {
|
|
4504
|
-
constructor(directory) {
|
|
4505
|
-
super({ ...directory !== void 0 ? { directory } : {}, fileName: "pipelines.json" });
|
|
4506
|
-
}
|
|
4507
|
-
save(run2) {
|
|
4508
|
-
return this.mutate((runs) => {
|
|
4509
|
-
const index = runs.findIndex((r) => r.id === run2.id);
|
|
4510
|
-
if (index >= 0) runs[index] = run2;
|
|
4511
|
-
else runs.push(run2);
|
|
4512
|
-
return [runs, void 0];
|
|
4513
|
-
});
|
|
4514
|
-
}
|
|
4515
|
-
async get(id) {
|
|
4516
|
-
return (await this.readAll()).find((r) => r.id === id);
|
|
4517
|
-
}
|
|
4518
|
-
async list(filter = {}) {
|
|
4519
|
-
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 ?? ""));
|
|
4520
|
-
}
|
|
4521
|
-
};
|
|
4522
|
-
|
|
4523
4102
|
// src/planner/store.ts
|
|
4524
4103
|
var FilePlanStore = class extends JsonFileStore {
|
|
4525
4104
|
constructor(directory) {
|
|
@@ -4564,9 +4143,10 @@ function buildPlanner(plannerConfig, chat, log = logger) {
|
|
|
4564
4143
|
|
|
4565
4144
|
// src/pipelines/engine-factory.ts
|
|
4566
4145
|
function buildPipelineEngine(config, log = logger) {
|
|
4146
|
+
const auditLog = new FileAuditLog();
|
|
4567
4147
|
const agents = buildAgentRegistry(config, log);
|
|
4568
4148
|
const roles = buildRoleRegistry(config);
|
|
4569
|
-
const chat = createDefaultChatFn(config.project.providers);
|
|
4149
|
+
const chat = createDefaultChatFn(config.project.providers, { audit: auditLog });
|
|
4570
4150
|
const tasks = new TaskEngine({
|
|
4571
4151
|
agents,
|
|
4572
4152
|
roles,
|
|
@@ -4694,6 +4274,7 @@ function registerPipelineCommands(program) {
|
|
|
4694
4274
|
console.log(` reports: ${run2.reports.length}`);
|
|
4695
4275
|
}
|
|
4696
4276
|
});
|
|
4277
|
+
registerPipelineInspect(pipeline);
|
|
4697
4278
|
}
|
|
4698
4279
|
async function runPipelineAction(id, objective) {
|
|
4699
4280
|
const config = loadConfig();
|
|
@@ -5093,7 +4674,17 @@ function createProgram() {
|
|
|
5093
4674
|
registerRunAlias(program);
|
|
5094
4675
|
return program;
|
|
5095
4676
|
}
|
|
4677
|
+
function applyLoggingConfig() {
|
|
4678
|
+
try {
|
|
4679
|
+
if (findConfigFile()) {
|
|
4680
|
+
const logging = loadConfig().logging;
|
|
4681
|
+
if (logging) configureLogger(logging);
|
|
4682
|
+
}
|
|
4683
|
+
} catch {
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
5096
4686
|
async function run(argv) {
|
|
4687
|
+
applyLoggingConfig();
|
|
5097
4688
|
const program = createProgram();
|
|
5098
4689
|
try {
|
|
5099
4690
|
await program.parseAsync(argv);
|