@hilbras/omninode 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -5
- package/dist/cli/index.js +492 -86
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +113 -34
- package/dist/index.js +335 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { realpathSync } from "fs";
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/version.ts
|
|
9
|
-
var OMNINODE_VERSION = "0.
|
|
9
|
+
var OMNINODE_VERSION = "0.4.0";
|
|
10
10
|
|
|
11
11
|
// src/errors/index.ts
|
|
12
12
|
var OmniNodeError = class extends Error {
|
|
@@ -29,6 +29,16 @@ var ProviderError = class extends OmniNodeError {
|
|
|
29
29
|
super(code, message, options);
|
|
30
30
|
}
|
|
31
31
|
};
|
|
32
|
+
var AgentError = class extends OmniNodeError {
|
|
33
|
+
constructor(code, message, options = {}) {
|
|
34
|
+
super(code, message, options);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var ProtocolError = class extends OmniNodeError {
|
|
38
|
+
constructor(message, options = {}) {
|
|
39
|
+
super("PROTOCOL_VIOLATION", message, options);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
32
42
|
function isOmniNodeError(value) {
|
|
33
43
|
return value instanceof OmniNodeError;
|
|
34
44
|
}
|
|
@@ -55,6 +65,8 @@ var agentConfigSchema = z.object({
|
|
|
55
65
|
args: z.array(z.string()).optional(),
|
|
56
66
|
cwd: z.string().optional(),
|
|
57
67
|
env: z.record(z.string()).optional(),
|
|
68
|
+
input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
|
|
69
|
+
timeout_ms: z.number().int().positive().optional(),
|
|
58
70
|
metadata: z.record(z.unknown()).optional()
|
|
59
71
|
}).strict();
|
|
60
72
|
var roleConfigSchema = z.object({
|
|
@@ -166,6 +178,8 @@ function toAgentConfig(raw) {
|
|
|
166
178
|
args: raw.args,
|
|
167
179
|
cwd: raw.cwd,
|
|
168
180
|
env: raw.env,
|
|
181
|
+
inputMode: raw.input_mode,
|
|
182
|
+
timeoutMs: raw.timeout_ms,
|
|
169
183
|
metadata: raw.metadata
|
|
170
184
|
};
|
|
171
185
|
}
|
|
@@ -243,10 +257,6 @@ function registerProjectCommands(program) {
|
|
|
243
257
|
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));
|
|
244
258
|
}
|
|
245
259
|
|
|
246
|
-
// src/cli/commands/providers.ts
|
|
247
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
248
|
-
import { parseDocument } from "yaml";
|
|
249
|
-
|
|
250
260
|
// src/providers/auth.ts
|
|
251
261
|
function resolveApiKey(config, env = process.env) {
|
|
252
262
|
if (!config.apiKeyEnvVar) return void 0;
|
|
@@ -500,6 +510,49 @@ function inferHealth(error) {
|
|
|
500
510
|
return status !== void 0 && status >= 500 ? "degraded" : "unreachable";
|
|
501
511
|
}
|
|
502
512
|
|
|
513
|
+
// src/providers/omnihilbras/index.ts
|
|
514
|
+
var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
|
|
515
|
+
/**
|
|
516
|
+
* Run the full connect flow. Throws on authentication or transport failure
|
|
517
|
+
* (use healthCheck() for a non-throwing probe).
|
|
518
|
+
*/
|
|
519
|
+
async connect(registry) {
|
|
520
|
+
const apiKey = resolveApiKey(this.config);
|
|
521
|
+
const discovered = await this.listModels();
|
|
522
|
+
const seen = /* @__PURE__ */ new Set();
|
|
523
|
+
const models = [];
|
|
524
|
+
const duplicates = [];
|
|
525
|
+
for (const model of discovered) {
|
|
526
|
+
if (seen.has(model.id)) {
|
|
527
|
+
duplicates.push(model.id);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
seen.add(model.id);
|
|
531
|
+
models.push({
|
|
532
|
+
...model,
|
|
533
|
+
provider: this.config.name,
|
|
534
|
+
status: "available",
|
|
535
|
+
metadata: { ...model.metadata, gateway: "omnihilbras" }
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
if (registry) {
|
|
539
|
+
for (const model of models) registry.register(model);
|
|
540
|
+
}
|
|
541
|
+
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)` : "") + ".";
|
|
542
|
+
const status = {
|
|
543
|
+
name: this.config.name,
|
|
544
|
+
type: this.config.type,
|
|
545
|
+
baseUrl: this.config.baseUrl,
|
|
546
|
+
health: "healthy",
|
|
547
|
+
connected: true,
|
|
548
|
+
modelCount: models.length,
|
|
549
|
+
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
550
|
+
message
|
|
551
|
+
};
|
|
552
|
+
return { status, models, duplicates, registered: registry ? models.length : 0 };
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
503
556
|
// src/providers/factory.ts
|
|
504
557
|
var OPENAI_COMPATIBLE_TYPES = [
|
|
505
558
|
"openai-compatible",
|
|
@@ -508,25 +561,95 @@ var OPENAI_COMPATIBLE_TYPES = [
|
|
|
508
561
|
"custom"
|
|
509
562
|
];
|
|
510
563
|
function createProvider(config) {
|
|
564
|
+
if (config.type === "omnihilbras") {
|
|
565
|
+
return new OmniHilbrasProvider(config);
|
|
566
|
+
}
|
|
511
567
|
if (OPENAI_COMPATIBLE_TYPES.includes(config.type)) {
|
|
512
568
|
return new OpenAICompatibleProvider(config);
|
|
513
569
|
}
|
|
514
|
-
if (config.type === "omnihilbras") {
|
|
515
|
-
throw new OmniNodeError(
|
|
516
|
-
"NOT_IMPLEMENTED",
|
|
517
|
-
`Provider type "omnihilbras" is planned for Phase 2 (OmniHilbras Integration). If your OmniHilbras endpoint is OpenAI-compatible, use type "openai-compatible" for now.`
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
570
|
throw new OmniNodeError("NOT_IMPLEMENTED", `Provider type "${config.type}" has no adapter yet.`);
|
|
521
571
|
}
|
|
522
572
|
|
|
573
|
+
// src/registry/index.ts
|
|
574
|
+
function registryKey(provider, id) {
|
|
575
|
+
return `${provider}:${id}`;
|
|
576
|
+
}
|
|
577
|
+
var ModelRegistry = class {
|
|
578
|
+
models = /* @__PURE__ */ new Map();
|
|
579
|
+
register(model) {
|
|
580
|
+
this.models.set(registryKey(model.provider, model.id), model);
|
|
581
|
+
}
|
|
582
|
+
get(provider, id) {
|
|
583
|
+
return this.models.get(registryKey(provider, id));
|
|
584
|
+
}
|
|
585
|
+
list(filter = {}) {
|
|
586
|
+
return [...this.models.values()].filter((model) => filter.provider ? model.provider === filter.provider : true).filter(
|
|
587
|
+
(model) => filter.capability ? model.capabilities?.[filter.capability] === true : true
|
|
588
|
+
).sort((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
|
|
589
|
+
}
|
|
590
|
+
/** Availability validation (§27 Phase 1): present in the registry and not retired. */
|
|
591
|
+
isAvailable(provider, id) {
|
|
592
|
+
const model = this.get(provider, id);
|
|
593
|
+
return model !== void 0 && (model.status ?? "available") === "available";
|
|
594
|
+
}
|
|
595
|
+
/** Discover models from one provider and register them. */
|
|
596
|
+
async discoverFrom(provider) {
|
|
597
|
+
const discovered = await provider.listModels();
|
|
598
|
+
for (const model of discovered) this.register(model);
|
|
599
|
+
return discovered;
|
|
600
|
+
}
|
|
601
|
+
/** Discover from many providers; a failing provider does not abort the rest. */
|
|
602
|
+
async discoverAll(providers) {
|
|
603
|
+
return Promise.all(
|
|
604
|
+
providers.map(async (provider) => {
|
|
605
|
+
try {
|
|
606
|
+
return {
|
|
607
|
+
provider: provider.config.name,
|
|
608
|
+
models: await this.discoverFrom(provider)
|
|
609
|
+
};
|
|
610
|
+
} catch (error) {
|
|
611
|
+
return {
|
|
612
|
+
provider: provider.config.name,
|
|
613
|
+
models: [],
|
|
614
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
})
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
|
|
523
622
|
// src/cli/commands/shared.ts
|
|
623
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
624
|
+
import { parseDocument } from "yaml";
|
|
524
625
|
function notImplemented(command, when) {
|
|
525
626
|
return new OmniNodeError(
|
|
526
627
|
"NOT_IMPLEMENTED",
|
|
527
628
|
`${command} is planned for ${when}. Edit omninode.yaml directly for now.`
|
|
528
629
|
);
|
|
529
630
|
}
|
|
631
|
+
function appendToConfigList(listKey, item) {
|
|
632
|
+
const configPath = findConfigFile();
|
|
633
|
+
if (!configPath) {
|
|
634
|
+
throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
|
|
635
|
+
}
|
|
636
|
+
const doc = parseDocument(readFileSync2(configPath, "utf8"));
|
|
637
|
+
if (doc.getIn(["project", listKey]) === void 0) {
|
|
638
|
+
doc.setIn(["project", listKey], [item]);
|
|
639
|
+
} else {
|
|
640
|
+
doc.addIn(["project", listKey], item);
|
|
641
|
+
}
|
|
642
|
+
const validated = appConfigSchema.safeParse(doc.toJS());
|
|
643
|
+
if (!validated.success) {
|
|
644
|
+
const issues = validated.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
645
|
+
throw new ConfigError(
|
|
646
|
+
"CONFIG_INVALID",
|
|
647
|
+
`Editing ${configPath} would produce an invalid configuration: ${issues}`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
writeFileSync2(configPath, doc.toString(), "utf8");
|
|
651
|
+
return configPath;
|
|
652
|
+
}
|
|
530
653
|
|
|
531
654
|
// src/cli/commands/providers.ts
|
|
532
655
|
function registerProviderCommands(program) {
|
|
@@ -558,7 +681,10 @@ function registerProviderCommands(program) {
|
|
|
558
681
|
"--api-key-env-var <envVar>",
|
|
559
682
|
"Environment variable holding the API key. Keys are never written to the config file."
|
|
560
683
|
).action(async (options) => addProvider(options));
|
|
561
|
-
provider.command("test <name>").description("Test connectivity, authentication and model discovery for a configured provider.").
|
|
684
|
+
provider.command("test <name>").description("Test connectivity, authentication and model discovery for a configured provider.").option(
|
|
685
|
+
"--connect",
|
|
686
|
+
"Run the full provider connect flow (authenticate, fetch, validate, register) where the adapter supports it."
|
|
687
|
+
).action(async (name, options) => testProvider(name, options));
|
|
562
688
|
provider.command("remove <name>").description("Remove a provider (planned for Phase 1 follow-up).").action(() => {
|
|
563
689
|
throw notImplemented("omninode provider remove", "a Phase 1 follow-up");
|
|
564
690
|
});
|
|
@@ -583,25 +709,11 @@ async function addProvider(options) {
|
|
|
583
709
|
if (existing.project.providers.some((p) => p.name === newProvider.name)) {
|
|
584
710
|
throw new OmniNodeError(
|
|
585
711
|
"CLI_USAGE",
|
|
586
|
-
`Provider "${newProvider.name}" already exists in ${
|
|
712
|
+
`Provider "${newProvider.name}" already exists in ${findConfigFile() ?? "omninode.yaml"}.`
|
|
587
713
|
);
|
|
588
714
|
}
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
doc.setIn(["project", "providers"], [newProvider]);
|
|
592
|
-
} else {
|
|
593
|
-
doc.addIn(["project", "providers"], newProvider);
|
|
594
|
-
}
|
|
595
|
-
const validated = appConfigSchema.safeParse(doc.toJS());
|
|
596
|
-
if (!validated.success) {
|
|
597
|
-
const issues = validated.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
598
|
-
throw new ConfigError(
|
|
599
|
-
"CONFIG_INVALID",
|
|
600
|
-
`Editing ${configPath} would produce an invalid configuration: ${issues}`
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
writeFileSync2(configPath, doc.toString(), "utf8");
|
|
604
|
-
console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${configPath}.`);
|
|
715
|
+
const writtenPath = appendToConfigList("providers", newProvider);
|
|
716
|
+
console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${writtenPath}.`);
|
|
605
717
|
if (newProvider.api_key_env_var) {
|
|
606
718
|
console.log(
|
|
607
719
|
`Set $${newProvider.api_key_env_var} in your environment, then run: omninode provider test ${newProvider.name}`
|
|
@@ -610,7 +722,7 @@ async function addProvider(options) {
|
|
|
610
722
|
console.log(`Run \`omninode provider test ${newProvider.name}\` to verify connectivity.`);
|
|
611
723
|
}
|
|
612
724
|
}
|
|
613
|
-
async function testProvider(name) {
|
|
725
|
+
async function testProvider(name, options = {}) {
|
|
614
726
|
const config = loadConfig();
|
|
615
727
|
const providerConfig = config.project.providers.find((p) => p.name === name);
|
|
616
728
|
if (!providerConfig) {
|
|
@@ -622,62 +734,28 @@ async function testProvider(name) {
|
|
|
622
734
|
}
|
|
623
735
|
const provider = createProvider(providerConfig);
|
|
624
736
|
console.log(`Testing provider "${name}" (${providerConfig.type}) at ${provider.config.baseUrl}`);
|
|
737
|
+
if (options.connect && provider instanceof OmniHilbrasProvider) {
|
|
738
|
+
const registry = new ModelRegistry();
|
|
739
|
+
try {
|
|
740
|
+
const result = await provider.connect(registry);
|
|
741
|
+
printProviderStatus(result.status);
|
|
742
|
+
console.log(` registered: ${result.registered}`);
|
|
743
|
+
} catch (error) {
|
|
744
|
+
console.log(` connect failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
745
|
+
process.exitCode = 1;
|
|
746
|
+
}
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
625
749
|
const health = await provider.healthCheck();
|
|
626
|
-
|
|
627
|
-
console.log(` connected: ${health.connected}`);
|
|
628
|
-
console.log(` models: ${health.modelCount}`);
|
|
629
|
-
if (health.message) console.log(` detail: ${health.message}`);
|
|
750
|
+
printProviderStatus(health);
|
|
630
751
|
if (!health.connected) process.exitCode = 1;
|
|
631
752
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
753
|
+
function printProviderStatus(status) {
|
|
754
|
+
console.log(` health: ${status.health}`);
|
|
755
|
+
console.log(` connected: ${status.connected}`);
|
|
756
|
+
console.log(` models: ${status.modelCount}`);
|
|
757
|
+
if (status.message) console.log(` detail: ${status.message}`);
|
|
636
758
|
}
|
|
637
|
-
var ModelRegistry = class {
|
|
638
|
-
models = /* @__PURE__ */ new Map();
|
|
639
|
-
register(model) {
|
|
640
|
-
this.models.set(registryKey(model.provider, model.id), model);
|
|
641
|
-
}
|
|
642
|
-
get(provider, id) {
|
|
643
|
-
return this.models.get(registryKey(provider, id));
|
|
644
|
-
}
|
|
645
|
-
list(filter = {}) {
|
|
646
|
-
return [...this.models.values()].filter((model) => filter.provider ? model.provider === filter.provider : true).filter(
|
|
647
|
-
(model) => filter.capability ? model.capabilities?.[filter.capability] === true : true
|
|
648
|
-
).sort((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
|
|
649
|
-
}
|
|
650
|
-
/** Availability validation (§27 Phase 1): present in the registry and not retired. */
|
|
651
|
-
isAvailable(provider, id) {
|
|
652
|
-
const model = this.get(provider, id);
|
|
653
|
-
return model !== void 0 && (model.status ?? "available") === "available";
|
|
654
|
-
}
|
|
655
|
-
/** Discover models from one provider and register them. */
|
|
656
|
-
async discoverFrom(provider) {
|
|
657
|
-
const discovered = await provider.listModels();
|
|
658
|
-
for (const model of discovered) this.register(model);
|
|
659
|
-
return discovered;
|
|
660
|
-
}
|
|
661
|
-
/** Discover from many providers; a failing provider does not abort the rest. */
|
|
662
|
-
async discoverAll(providers) {
|
|
663
|
-
return Promise.all(
|
|
664
|
-
providers.map(async (provider) => {
|
|
665
|
-
try {
|
|
666
|
-
return {
|
|
667
|
-
provider: provider.config.name,
|
|
668
|
-
models: await this.discoverFrom(provider)
|
|
669
|
-
};
|
|
670
|
-
} catch (error) {
|
|
671
|
-
return {
|
|
672
|
-
provider: provider.config.name,
|
|
673
|
-
models: [],
|
|
674
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
|
-
})
|
|
678
|
-
);
|
|
679
|
-
}
|
|
680
|
-
};
|
|
681
759
|
|
|
682
760
|
// src/cli/commands/models.ts
|
|
683
761
|
var CAPABILITIES = [
|
|
@@ -732,26 +810,354 @@ function registerModelsCommand(program) {
|
|
|
732
810
|
});
|
|
733
811
|
}
|
|
734
812
|
|
|
813
|
+
// src/agents/process/index.ts
|
|
814
|
+
import { spawn } from "child_process";
|
|
815
|
+
var DEFAULT_TIMEOUT_MS2 = 6e5;
|
|
816
|
+
var KILL_GRACE_MS = 5e3;
|
|
817
|
+
function composeTaskText(input) {
|
|
818
|
+
const sections = [`# Objective
|
|
819
|
+
${input.objective}`];
|
|
820
|
+
if (input.role) {
|
|
821
|
+
const lines = ["# Role", `${input.role.name} (${input.role.id})`];
|
|
822
|
+
if (input.role.responsibilities.length > 0) {
|
|
823
|
+
lines.push("", "Responsibilities:", ...input.role.responsibilities.map((r) => `- ${r}`));
|
|
824
|
+
}
|
|
825
|
+
if (input.role.expectedOutputs.length > 0) {
|
|
826
|
+
lines.push("", "Expected outputs:", ...input.role.expectedOutputs.map((o) => `- ${o}`));
|
|
827
|
+
}
|
|
828
|
+
sections.push(lines.join("\n"));
|
|
829
|
+
}
|
|
830
|
+
if (input.context) sections.push(`# Context
|
|
831
|
+
${input.context}`);
|
|
832
|
+
if (input.instruction) sections.push(`# Instruction
|
|
833
|
+
${input.instruction}`);
|
|
834
|
+
return sections.join("\n\n");
|
|
835
|
+
}
|
|
836
|
+
function buildProtocolMessages(input, agentName) {
|
|
837
|
+
const meta = {
|
|
838
|
+
taskId: input.taskId,
|
|
839
|
+
agent: agentName,
|
|
840
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
841
|
+
};
|
|
842
|
+
const messages = [
|
|
843
|
+
{ type: "TASK", payload: { taskId: input.taskId, objective: input.objective } }
|
|
844
|
+
];
|
|
845
|
+
if (input.role) messages.push({ type: "ROLE", payload: input.role });
|
|
846
|
+
if (input.context) messages.push({ type: "CONTEXT", payload: { body: input.context } });
|
|
847
|
+
if (input.instruction) messages.push({ type: "INSTRUCTION", payload: { text: input.instruction } });
|
|
848
|
+
return messages.map((message) => JSON.stringify({ meta, message }));
|
|
849
|
+
}
|
|
850
|
+
var ProcessAgent = class {
|
|
851
|
+
config;
|
|
852
|
+
info;
|
|
853
|
+
log;
|
|
854
|
+
running = /* @__PURE__ */ new Map();
|
|
855
|
+
constructor(config, log = logger) {
|
|
856
|
+
if (!config.command) {
|
|
857
|
+
throw new AgentError("AGENT_NOT_FOUND", `Agent "${config.name}" has no command configured.`);
|
|
858
|
+
}
|
|
859
|
+
this.config = config;
|
|
860
|
+
this.log = log.child({ agent: config.name });
|
|
861
|
+
this.info = {
|
|
862
|
+
name: config.name,
|
|
863
|
+
integration: "process",
|
|
864
|
+
command: config.command,
|
|
865
|
+
cwd: config.cwd,
|
|
866
|
+
status: "ready",
|
|
867
|
+
capabilities: [config.inputMode ?? "stdin"]
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
async run(input) {
|
|
871
|
+
const command = this.config.command;
|
|
872
|
+
if (!command) {
|
|
873
|
+
throw new AgentError("AGENT_NOT_FOUND", `Agent "${this.config.name}" has no command configured.`);
|
|
874
|
+
}
|
|
875
|
+
const mode = this.config.inputMode ?? "stdin";
|
|
876
|
+
const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
877
|
+
return new Promise((resolve, reject) => {
|
|
878
|
+
const child = spawn(
|
|
879
|
+
command,
|
|
880
|
+
[...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
|
|
881
|
+
{
|
|
882
|
+
cwd: this.config.cwd,
|
|
883
|
+
env: { ...process.env, ...this.config.env },
|
|
884
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
885
|
+
}
|
|
886
|
+
);
|
|
887
|
+
this.running.set(input.taskId, child);
|
|
888
|
+
let stdout = "";
|
|
889
|
+
let stderr = "";
|
|
890
|
+
let timedOut = false;
|
|
891
|
+
let settled = false;
|
|
892
|
+
const timer = setTimeout(() => {
|
|
893
|
+
timedOut = true;
|
|
894
|
+
this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
|
|
895
|
+
child.kill("SIGTERM");
|
|
896
|
+
setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
|
|
897
|
+
}, timeoutMs);
|
|
898
|
+
const settle = (finish) => {
|
|
899
|
+
if (settled) return;
|
|
900
|
+
settled = true;
|
|
901
|
+
clearTimeout(timer);
|
|
902
|
+
this.running.delete(input.taskId);
|
|
903
|
+
finish();
|
|
904
|
+
};
|
|
905
|
+
child.stdout?.on("data", (chunk) => {
|
|
906
|
+
stdout += chunk.toString();
|
|
907
|
+
});
|
|
908
|
+
child.stderr?.on("data", (chunk) => {
|
|
909
|
+
stderr += chunk.toString();
|
|
910
|
+
});
|
|
911
|
+
child.stdin?.on("error", () => {
|
|
912
|
+
});
|
|
913
|
+
if (mode !== "arg") {
|
|
914
|
+
const payload = mode === "protocol" ? buildProtocolMessages(input, this.config.name).join("\n") + "\n" : composeTaskText(input) + "\n";
|
|
915
|
+
child.stdin?.write(payload);
|
|
916
|
+
child.stdin?.end();
|
|
917
|
+
}
|
|
918
|
+
child.on("error", (error) => {
|
|
919
|
+
settle(() => {
|
|
920
|
+
if (error.code === "ENOENT") {
|
|
921
|
+
reject(
|
|
922
|
+
new AgentError(
|
|
923
|
+
"AGENT_NOT_FOUND",
|
|
924
|
+
`Agent "${this.config.name}": command "${command}" was not found. Is it installed and on PATH?`,
|
|
925
|
+
{ cause: error }
|
|
926
|
+
)
|
|
927
|
+
);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
reject(
|
|
931
|
+
new AgentError("AGENT_FAILED", `Agent "${this.config.name}" failed to start: ${error.message}.`, {
|
|
932
|
+
cause: error
|
|
933
|
+
})
|
|
934
|
+
);
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
child.on("close", (code, signal) => {
|
|
938
|
+
settle(() => {
|
|
939
|
+
if (timedOut) {
|
|
940
|
+
reject(
|
|
941
|
+
new AgentError(
|
|
942
|
+
"AGENT_TIMEOUT",
|
|
943
|
+
`Agent "${this.config.name}" timed out after ${timeoutMs}ms and was killed.`,
|
|
944
|
+
{ details: { taskId: input.taskId, stdout: stdout.slice(0, 4e3), stderr: stderr.slice(0, 2e3) } }
|
|
945
|
+
)
|
|
946
|
+
);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
|
|
950
|
+
if (mode === "protocol") {
|
|
951
|
+
try {
|
|
952
|
+
resolve(
|
|
953
|
+
interpretProtocolRun({
|
|
954
|
+
input,
|
|
955
|
+
agentName: this.config.name,
|
|
956
|
+
stdout,
|
|
957
|
+
exitCode: code,
|
|
958
|
+
logs
|
|
959
|
+
})
|
|
960
|
+
);
|
|
961
|
+
} catch (error) {
|
|
962
|
+
reject(error);
|
|
963
|
+
}
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
const trimmed = stdout.trim();
|
|
967
|
+
resolve({
|
|
968
|
+
taskId: input.taskId,
|
|
969
|
+
status: code === 0 ? "completed" : "failed",
|
|
970
|
+
summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
|
|
971
|
+
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
972
|
+
exitCode: code ?? void 0,
|
|
973
|
+
logs,
|
|
974
|
+
error: code === 0 ? void 0 : `Process exited with code ${code ?? "unknown"}${signal ? ` (signal: ${signal})` : ""}`
|
|
975
|
+
});
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
/** Ask the agent to stop current work (SIGTERM). */
|
|
981
|
+
async cancel(taskId) {
|
|
982
|
+
const child = this.running.get(taskId);
|
|
983
|
+
if (child) child.kill("SIGTERM");
|
|
984
|
+
}
|
|
985
|
+
runningTaskIds() {
|
|
986
|
+
return [...this.running.keys()];
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
function interpretProtocolRun({ input, agentName, stdout, exitCode, logs }) {
|
|
990
|
+
const reports = [];
|
|
991
|
+
let summary;
|
|
992
|
+
let error;
|
|
993
|
+
let sawProtocolMessage = false;
|
|
994
|
+
for (const line of stdout.split("\n")) {
|
|
995
|
+
const trimmed2 = line.trim();
|
|
996
|
+
if (trimmed2.length === 0) continue;
|
|
997
|
+
const message = parseAgentMessage(trimmed2);
|
|
998
|
+
if (!message) continue;
|
|
999
|
+
sawProtocolMessage = true;
|
|
1000
|
+
if (message.type === "REPORT") {
|
|
1001
|
+
const report = normalizeReport(message.payload, input.taskId, agentName, reports.length);
|
|
1002
|
+
if (report) reports.push(report);
|
|
1003
|
+
} else if (message.type === "COMPLETION") {
|
|
1004
|
+
const payload = message.payload;
|
|
1005
|
+
if (typeof payload?.summary === "string") summary = payload.summary;
|
|
1006
|
+
} else if (message.type === "ERROR") {
|
|
1007
|
+
const payload = message.payload;
|
|
1008
|
+
const detail = typeof payload?.message === "string" ? payload.message : "unknown error";
|
|
1009
|
+
const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
|
|
1010
|
+
error = `Agent error${code}: ${detail}`;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (!sawProtocolMessage && exitCode === 0) {
|
|
1014
|
+
throw new ProtocolError(
|
|
1015
|
+
`Agent "${agentName}" produced no protocol messages on stdout (input mode "protocol").`,
|
|
1016
|
+
{ details: { taskId: input.taskId, stdout: stdout.slice(0, 2e3) } }
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
const trimmed = stdout.trim();
|
|
1020
|
+
return {
|
|
1021
|
+
taskId: input.taskId,
|
|
1022
|
+
status: error !== void 0 || exitCode !== 0 ? "failed" : "completed",
|
|
1023
|
+
summary: summary ?? reports[0]?.summary,
|
|
1024
|
+
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
1025
|
+
exitCode: exitCode ?? void 0,
|
|
1026
|
+
logs,
|
|
1027
|
+
reports: reports.length > 0 ? reports : void 0,
|
|
1028
|
+
error: error ?? (exitCode !== 0 ? `Process exited with code ${exitCode ?? "unknown"}` : void 0)
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
function parseAgentMessage(line) {
|
|
1032
|
+
try {
|
|
1033
|
+
const parsed = JSON.parse(line);
|
|
1034
|
+
if (typeof parsed.type === "string") {
|
|
1035
|
+
return { type: parsed.type, payload: parsed.payload };
|
|
1036
|
+
}
|
|
1037
|
+
const inner = parsed.message;
|
|
1038
|
+
if (inner && typeof inner.type === "string") {
|
|
1039
|
+
return { type: inner.type, payload: inner.payload };
|
|
1040
|
+
}
|
|
1041
|
+
} catch {
|
|
1042
|
+
}
|
|
1043
|
+
return void 0;
|
|
1044
|
+
}
|
|
1045
|
+
function normalizeReport(payload, taskId, agentName, index) {
|
|
1046
|
+
if (payload === null || typeof payload !== "object") return void 0;
|
|
1047
|
+
const raw = payload;
|
|
1048
|
+
if (typeof raw.summary !== "string") return void 0;
|
|
1049
|
+
return {
|
|
1050
|
+
id: typeof raw.id === "string" ? raw.id : `${taskId}-report-${index}`,
|
|
1051
|
+
taskId: typeof raw.taskId === "string" ? raw.taskId : taskId,
|
|
1052
|
+
agent: typeof raw.agent === "string" ? raw.agent : agentName,
|
|
1053
|
+
summary: raw.summary,
|
|
1054
|
+
findings: Array.isArray(raw.findings) ? raw.findings : [],
|
|
1055
|
+
recommendations: Array.isArray(raw.recommendations) ? raw.recommendations : [],
|
|
1056
|
+
createdAt: typeof raw.createdAt === "string" ? raw.createdAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
function firstLine(text, maxLength = 200) {
|
|
1060
|
+
const line = text.split("\n", 1)[0] ?? text;
|
|
1061
|
+
return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// src/agents/factory.ts
|
|
1065
|
+
function createAgent(config) {
|
|
1066
|
+
if (config.integration === "process") {
|
|
1067
|
+
return new ProcessAgent(config);
|
|
1068
|
+
}
|
|
1069
|
+
if (config.integration === "native") {
|
|
1070
|
+
throw new OmniNodeError(
|
|
1071
|
+
"NOT_IMPLEMENTED",
|
|
1072
|
+
`Native agent adapters are planned for a later phase. Use input over stdin/stdout (type "cli") for "${config.name}" for now.`
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
735
1078
|
// src/cli/commands/agents.ts
|
|
1079
|
+
var collectArgs = (value, previous) => [...previous, value];
|
|
736
1080
|
function registerAgentCommands(program) {
|
|
737
1081
|
const agent = program.command("agent").description("Manage CLI agents registered with OmniNode.");
|
|
738
1082
|
agent.command("list").description("List configured agents.").action(async () => {
|
|
739
1083
|
const config = loadConfig();
|
|
740
1084
|
const agents = config.project.agents;
|
|
741
1085
|
if (agents.length === 0) {
|
|
742
|
-
console.log("No agents configured. Add one
|
|
1086
|
+
console.log("No agents configured. Add one with `omninode agent add` or edit omninode.yaml.");
|
|
743
1087
|
return;
|
|
744
1088
|
}
|
|
745
1089
|
for (const agent2 of agents) {
|
|
746
1090
|
const command = agent2.command ?? "(no command)";
|
|
747
1091
|
console.log(
|
|
748
|
-
`${agent2.name} integration=${agent2.integration} command=${command}`
|
|
1092
|
+
`${agent2.name} integration=${agent2.integration} command=${command} input=${agent2.inputMode ?? "stdin"}`
|
|
749
1093
|
);
|
|
750
1094
|
}
|
|
751
1095
|
});
|
|
752
|
-
agent.command("add").description(
|
|
753
|
-
|
|
1096
|
+
agent.command("add").description(
|
|
1097
|
+
"Register a CLI agent in omninode.yaml. Agents run as child processes (stdin/stdout); native adapters come in a later phase."
|
|
1098
|
+
).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(
|
|
1099
|
+
"--input-mode <mode>",
|
|
1100
|
+
"How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
|
|
1101
|
+
"stdin"
|
|
1102
|
+
).option(
|
|
1103
|
+
"--timeout-ms <ms>",
|
|
1104
|
+
"Per-task timeout in milliseconds.",
|
|
1105
|
+
(value) => Number(value)
|
|
1106
|
+
).action(async (options) => addAgent(options));
|
|
1107
|
+
agent.command("test <name>").description("Verify the OmniNode \u2194 agent connection by sending a trivial task.").action(async (name) => testAgent(name));
|
|
1108
|
+
}
|
|
1109
|
+
async function addAgent(options) {
|
|
1110
|
+
const candidate = agentConfigSchema.safeParse({
|
|
1111
|
+
name: options.name,
|
|
1112
|
+
type: "cli",
|
|
1113
|
+
command: options.command,
|
|
1114
|
+
...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
|
|
1115
|
+
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
1116
|
+
input_mode: options.inputMode,
|
|
1117
|
+
...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {}
|
|
754
1118
|
});
|
|
1119
|
+
if (!candidate.success) {
|
|
1120
|
+
const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
1121
|
+
throw new OmniNodeError("CLI_USAGE", `Invalid agent options: ${issues}`);
|
|
1122
|
+
}
|
|
1123
|
+
const newAgent = candidate.data;
|
|
1124
|
+
const existing = loadConfig();
|
|
1125
|
+
if (existing.project.agents.some((a) => a.name === newAgent.name)) {
|
|
1126
|
+
throw new OmniNodeError(
|
|
1127
|
+
"CLI_USAGE",
|
|
1128
|
+
`Agent "${newAgent.name}" already exists in omninode.yaml. Remove it first or pick another name.`
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
const configPath = appendToConfigList("agents", newAgent);
|
|
1132
|
+
console.log(`Added agent "${newAgent.name}" (${newAgent.type}, input=${newAgent.input_mode}) to ${configPath}.`);
|
|
1133
|
+
console.log(`Run \`omninode agent test ${newAgent.name}\` to verify the connection.`);
|
|
1134
|
+
}
|
|
1135
|
+
async function testAgent(name) {
|
|
1136
|
+
const config = loadConfig();
|
|
1137
|
+
const agentConfig = config.project.agents.find((a) => a.name === name);
|
|
1138
|
+
if (!agentConfig) {
|
|
1139
|
+
const available = config.project.agents.map((a) => a.name).join(", ") || "(none)";
|
|
1140
|
+
throw new AgentError(
|
|
1141
|
+
"AGENT_NOT_FOUND",
|
|
1142
|
+
`Agent "${name}" is not configured. Configured agents: ${available}.`
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
const agent = createAgent(agentConfig);
|
|
1146
|
+
console.log(
|
|
1147
|
+
`Testing agent "${name}" (${agent.info.integration}, input=${agentConfig.inputMode ?? "stdin"}) \u2014 sending a trivial task.`
|
|
1148
|
+
);
|
|
1149
|
+
const result = await agent.run({
|
|
1150
|
+
taskId: `test-${Date.now()}`,
|
|
1151
|
+
objective: "Reply with the single word: OK"
|
|
1152
|
+
});
|
|
1153
|
+
console.log(` status: ${result.status}`);
|
|
1154
|
+
if (result.exitCode !== void 0) console.log(` exit: ${result.exitCode}`);
|
|
1155
|
+
if (result.summary) console.log(` output: ${result.summary}`);
|
|
1156
|
+
if (result.rawOutput && result.rawOutput !== result.summary) {
|
|
1157
|
+
console.log(` raw: ${result.rawOutput.slice(0, 200).replace(/\n/g, " ")}`);
|
|
1158
|
+
}
|
|
1159
|
+
if (result.error) console.log(` error: ${result.error}`);
|
|
1160
|
+
if (result.status !== "completed") process.exitCode = 1;
|
|
755
1161
|
}
|
|
756
1162
|
|
|
757
1163
|
// src/cli/commands/roles.ts
|