@hilbras/omninode 0.3.0 → 0.5.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/dist/index.js CHANGED
@@ -123,6 +123,8 @@ var agentConfigSchema = z.object({
123
123
  args: z.array(z.string()).optional(),
124
124
  cwd: z.string().optional(),
125
125
  env: z.record(z.string()).optional(),
126
+ input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
127
+ timeout_ms: z.number().int().positive().optional(),
126
128
  metadata: z.record(z.unknown()).optional()
127
129
  }).strict();
128
130
  var roleConfigSchema = z.object({
@@ -234,6 +236,8 @@ function toAgentConfig(raw) {
234
236
  args: raw.args,
235
237
  cwd: raw.cwd,
236
238
  env: raw.env,
239
+ inputMode: raw.input_mode,
240
+ timeoutMs: raw.timeout_ms,
237
241
  metadata: raw.metadata
238
242
  };
239
243
  }
@@ -591,12 +595,520 @@ var ModelRegistry = class {
591
595
  }
592
596
  };
593
597
 
598
+ // src/agents/process/index.ts
599
+ import { spawn } from "child_process";
600
+ var DEFAULT_TIMEOUT_MS2 = 6e5;
601
+ var KILL_GRACE_MS = 5e3;
602
+ function composeTaskText(input) {
603
+ const sections = [`# Objective
604
+ ${input.objective}`];
605
+ if (input.role) {
606
+ const lines = ["# Role", `${input.role.name} (${input.role.id})`];
607
+ if (input.role.responsibilities.length > 0) {
608
+ lines.push("", "Responsibilities:", ...input.role.responsibilities.map((r) => `- ${r}`));
609
+ }
610
+ if (input.role.expectedOutputs.length > 0) {
611
+ lines.push("", "Expected outputs:", ...input.role.expectedOutputs.map((o) => `- ${o}`));
612
+ }
613
+ sections.push(lines.join("\n"));
614
+ }
615
+ if (input.context) sections.push(`# Context
616
+ ${input.context}`);
617
+ if (input.instruction) sections.push(`# Instruction
618
+ ${input.instruction}`);
619
+ return sections.join("\n\n");
620
+ }
621
+ function buildProtocolMessages(input, agentName) {
622
+ const meta = {
623
+ taskId: input.taskId,
624
+ agent: agentName,
625
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
626
+ };
627
+ const messages = [
628
+ { type: "TASK", payload: { taskId: input.taskId, objective: input.objective } }
629
+ ];
630
+ if (input.role) messages.push({ type: "ROLE", payload: input.role });
631
+ if (input.context) messages.push({ type: "CONTEXT", payload: { body: input.context } });
632
+ if (input.instruction) messages.push({ type: "INSTRUCTION", payload: { text: input.instruction } });
633
+ return messages.map((message) => JSON.stringify({ meta, message }));
634
+ }
635
+ var ProcessAgent = class {
636
+ config;
637
+ info;
638
+ log;
639
+ running = /* @__PURE__ */ new Map();
640
+ constructor(config, log = logger) {
641
+ if (!config.command) {
642
+ throw new AgentError("AGENT_NOT_FOUND", `Agent "${config.name}" has no command configured.`);
643
+ }
644
+ this.config = config;
645
+ this.log = log.child({ agent: config.name });
646
+ this.info = {
647
+ name: config.name,
648
+ integration: "process",
649
+ command: config.command,
650
+ cwd: config.cwd,
651
+ status: "ready",
652
+ capabilities: [config.inputMode ?? "stdin"]
653
+ };
654
+ }
655
+ async run(input) {
656
+ const command = this.config.command;
657
+ if (!command) {
658
+ throw new AgentError("AGENT_NOT_FOUND", `Agent "${this.config.name}" has no command configured.`);
659
+ }
660
+ const mode = this.config.inputMode ?? "stdin";
661
+ const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
662
+ return new Promise((resolve, reject) => {
663
+ const child = spawn(
664
+ command,
665
+ [...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
666
+ {
667
+ cwd: this.config.cwd,
668
+ env: { ...process.env, ...this.config.env },
669
+ stdio: ["pipe", "pipe", "pipe"]
670
+ }
671
+ );
672
+ this.running.set(input.taskId, child);
673
+ let stdout = "";
674
+ let stderr = "";
675
+ let timedOut = false;
676
+ let settled = false;
677
+ const timer = setTimeout(() => {
678
+ timedOut = true;
679
+ this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
680
+ child.kill("SIGTERM");
681
+ setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
682
+ }, timeoutMs);
683
+ const settle = (finish) => {
684
+ if (settled) return;
685
+ settled = true;
686
+ clearTimeout(timer);
687
+ this.running.delete(input.taskId);
688
+ finish();
689
+ };
690
+ child.stdout?.on("data", (chunk) => {
691
+ stdout += chunk.toString();
692
+ });
693
+ child.stderr?.on("data", (chunk) => {
694
+ stderr += chunk.toString();
695
+ });
696
+ child.stdin?.on("error", () => {
697
+ });
698
+ if (mode !== "arg") {
699
+ const payload = mode === "protocol" ? buildProtocolMessages(input, this.config.name).join("\n") + "\n" : composeTaskText(input) + "\n";
700
+ child.stdin?.write(payload);
701
+ child.stdin?.end();
702
+ }
703
+ child.on("error", (error) => {
704
+ settle(() => {
705
+ if (error.code === "ENOENT") {
706
+ reject(
707
+ new AgentError(
708
+ "AGENT_NOT_FOUND",
709
+ `Agent "${this.config.name}": command "${command}" was not found. Is it installed and on PATH?`,
710
+ { cause: error }
711
+ )
712
+ );
713
+ return;
714
+ }
715
+ reject(
716
+ new AgentError("AGENT_FAILED", `Agent "${this.config.name}" failed to start: ${error.message}.`, {
717
+ cause: error
718
+ })
719
+ );
720
+ });
721
+ });
722
+ child.on("close", (code, signal) => {
723
+ settle(() => {
724
+ if (timedOut) {
725
+ reject(
726
+ new AgentError(
727
+ "AGENT_TIMEOUT",
728
+ `Agent "${this.config.name}" timed out after ${timeoutMs}ms and was killed.`,
729
+ { details: { taskId: input.taskId, stdout: stdout.slice(0, 4e3), stderr: stderr.slice(0, 2e3) } }
730
+ )
731
+ );
732
+ return;
733
+ }
734
+ const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
735
+ if (mode === "protocol") {
736
+ try {
737
+ resolve(
738
+ interpretProtocolRun({
739
+ input,
740
+ agentName: this.config.name,
741
+ stdout,
742
+ exitCode: code,
743
+ logs
744
+ })
745
+ );
746
+ } catch (error) {
747
+ reject(error);
748
+ }
749
+ return;
750
+ }
751
+ const trimmed = stdout.trim();
752
+ resolve({
753
+ taskId: input.taskId,
754
+ status: code === 0 ? "completed" : "failed",
755
+ summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
756
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
757
+ exitCode: code ?? void 0,
758
+ logs,
759
+ error: code === 0 ? void 0 : `Process exited with code ${code ?? "unknown"}${signal ? ` (signal: ${signal})` : ""}`
760
+ });
761
+ });
762
+ });
763
+ });
764
+ }
765
+ /** Ask the agent to stop current work (SIGTERM). */
766
+ async cancel(taskId) {
767
+ const child = this.running.get(taskId);
768
+ if (child) child.kill("SIGTERM");
769
+ }
770
+ runningTaskIds() {
771
+ return [...this.running.keys()];
772
+ }
773
+ };
774
+ function interpretProtocolRun({ input, agentName, stdout, exitCode, logs }) {
775
+ const reports = [];
776
+ let summary;
777
+ let error;
778
+ let sawProtocolMessage = false;
779
+ for (const line of stdout.split("\n")) {
780
+ const trimmed2 = line.trim();
781
+ if (trimmed2.length === 0) continue;
782
+ const message = parseAgentMessage(trimmed2);
783
+ if (!message) continue;
784
+ sawProtocolMessage = true;
785
+ if (message.type === "REPORT") {
786
+ const report = normalizeReport(message.payload, input.taskId, agentName, reports.length);
787
+ if (report) reports.push(report);
788
+ } else if (message.type === "COMPLETION") {
789
+ const payload = message.payload;
790
+ if (typeof payload?.summary === "string") summary = payload.summary;
791
+ } else if (message.type === "ERROR") {
792
+ const payload = message.payload;
793
+ const detail = typeof payload?.message === "string" ? payload.message : "unknown error";
794
+ const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
795
+ error = `Agent error${code}: ${detail}`;
796
+ }
797
+ }
798
+ if (!sawProtocolMessage && exitCode === 0) {
799
+ throw new ProtocolError(
800
+ `Agent "${agentName}" produced no protocol messages on stdout (input mode "protocol").`,
801
+ { details: { taskId: input.taskId, stdout: stdout.slice(0, 2e3) } }
802
+ );
803
+ }
804
+ const trimmed = stdout.trim();
805
+ return {
806
+ taskId: input.taskId,
807
+ status: error !== void 0 || exitCode !== 0 ? "failed" : "completed",
808
+ summary: summary ?? reports[0]?.summary,
809
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
810
+ exitCode: exitCode ?? void 0,
811
+ logs,
812
+ reports: reports.length > 0 ? reports : void 0,
813
+ error: error ?? (exitCode !== 0 ? `Process exited with code ${exitCode ?? "unknown"}` : void 0)
814
+ };
815
+ }
816
+ function parseAgentMessage(line) {
817
+ try {
818
+ const parsed = JSON.parse(line);
819
+ if (typeof parsed.type === "string") {
820
+ return { type: parsed.type, payload: parsed.payload };
821
+ }
822
+ const inner = parsed.message;
823
+ if (inner && typeof inner.type === "string") {
824
+ return { type: inner.type, payload: inner.payload };
825
+ }
826
+ } catch {
827
+ }
828
+ return void 0;
829
+ }
830
+ function normalizeReport(payload, taskId, agentName, index) {
831
+ if (payload === null || typeof payload !== "object") return void 0;
832
+ const raw = payload;
833
+ if (typeof raw.summary !== "string") return void 0;
834
+ return {
835
+ id: typeof raw.id === "string" ? raw.id : `${taskId}-report-${index}`,
836
+ taskId: typeof raw.taskId === "string" ? raw.taskId : taskId,
837
+ agent: typeof raw.agent === "string" ? raw.agent : agentName,
838
+ summary: raw.summary,
839
+ findings: Array.isArray(raw.findings) ? raw.findings : [],
840
+ recommendations: Array.isArray(raw.recommendations) ? raw.recommendations : [],
841
+ createdAt: typeof raw.createdAt === "string" ? raw.createdAt : (/* @__PURE__ */ new Date()).toISOString()
842
+ };
843
+ }
844
+ function firstLine(text, maxLength = 200) {
845
+ const line = text.split("\n", 1)[0] ?? text;
846
+ return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
847
+ }
848
+
849
+ // src/agents/registry.ts
850
+ var AgentRegistry = class {
851
+ agents = /* @__PURE__ */ new Map();
852
+ register(agent) {
853
+ this.agents.set(agent.info.name, agent);
854
+ }
855
+ get(name) {
856
+ return this.agents.get(name);
857
+ }
858
+ list() {
859
+ return [...this.agents.values()].sort((a, b) => a.info.name.localeCompare(b.info.name));
860
+ }
861
+ };
862
+
863
+ // src/agents/factory.ts
864
+ function createAgent(config) {
865
+ if (config.integration === "process") {
866
+ return new ProcessAgent(config);
867
+ }
868
+ if (config.integration === "native") {
869
+ throw new OmniNodeError(
870
+ "NOT_IMPLEMENTED",
871
+ `Native agent adapters are planned for a later phase. Use input over stdin/stdout (type "cli") for "${config.name}" for now.`
872
+ );
873
+ }
874
+ throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
875
+ }
876
+ function buildAgentRegistry(config, log = logger) {
877
+ const registry = new AgentRegistry();
878
+ for (const agentConfig of config.project.agents) {
879
+ try {
880
+ registry.register(createAgent(agentConfig));
881
+ } catch (error) {
882
+ log.warn(
883
+ `Skipping agent "${agentConfig.name}": ${error instanceof Error ? error.message : String(error)}`
884
+ );
885
+ }
886
+ }
887
+ return registry;
888
+ }
889
+
890
+ // src/roles/index.ts
891
+ var RoleRegistry = class {
892
+ roles = /* @__PURE__ */ new Map();
893
+ register(role) {
894
+ this.roles.set(role.id, role);
895
+ }
896
+ registerAll(roles) {
897
+ for (const role of roles) this.register(role);
898
+ }
899
+ get(id) {
900
+ return this.roles.get(id);
901
+ }
902
+ list() {
903
+ return [...this.roles.values()].sort((a, b) => a.id.localeCompare(b.id));
904
+ }
905
+ };
906
+ function buildRoleRegistry(config) {
907
+ const registry = new RoleRegistry();
908
+ registry.registerAll(config.project.roles);
909
+ return registry;
910
+ }
911
+
912
+ // src/tasks/store.ts
913
+ import { mkdir, readFile, rename, writeFile } from "fs/promises";
914
+ import { dirname } from "path";
915
+ var FileTaskStore = class {
916
+ filePath;
917
+ constructor(directory = `${process.cwd()}/.omninode`) {
918
+ this.filePath = `${directory}/tasks.json`;
919
+ }
920
+ async save(task) {
921
+ const tasks = await this.load();
922
+ const index = tasks.findIndex((t) => t.id === task.id);
923
+ if (index >= 0) tasks[index] = task;
924
+ else tasks.push(task);
925
+ await this.write(tasks);
926
+ }
927
+ async get(id) {
928
+ const tasks = await this.load();
929
+ return tasks.find((t) => t.id === id);
930
+ }
931
+ async list(filter = {}) {
932
+ const tasks = await this.load();
933
+ 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));
934
+ }
935
+ async load() {
936
+ try {
937
+ const raw = await readFile(this.filePath, "utf8");
938
+ const parsed = JSON.parse(raw);
939
+ return Array.isArray(parsed) ? parsed : [];
940
+ } catch {
941
+ return [];
942
+ }
943
+ }
944
+ async write(tasks) {
945
+ const tmp = `${this.filePath}.tmp`;
946
+ await mkdir(dirname(this.filePath), { recursive: true });
947
+ await writeFile(tmp, `${JSON.stringify(tasks, null, 2)}
948
+ `, "utf8");
949
+ await rename(tmp, this.filePath);
950
+ }
951
+ };
952
+
953
+ // src/tasks/engine.ts
954
+ import { randomBytes } from "crypto";
955
+ var ALLOWED_TRANSITIONS = {
956
+ created: ["queued", "running", "cancelled"],
957
+ queued: ["running", "cancelled"],
958
+ running: ["completed", "failed", "cancelled"],
959
+ waiting: ["running", "cancelled"],
960
+ completed: [],
961
+ failed: [],
962
+ cancelled: []
963
+ };
964
+ var TaskEngine = class {
965
+ constructor(options) {
966
+ this.options = options;
967
+ this.log = (options.log ?? logger).child({ component: "tasks" });
968
+ }
969
+ options;
970
+ log;
971
+ get store() {
972
+ return this.options.store;
973
+ }
974
+ async create(input) {
975
+ if (!input.objective || input.objective.trim().length === 0) {
976
+ throw new TaskError("TASK_INVALID", "Task objective must not be empty.");
977
+ }
978
+ if (input.role && !this.options.roles.get(input.role)) {
979
+ throw new TaskError(
980
+ "TASK_INVALID",
981
+ `Role "${input.role}" is not defined. Add it under project.roles in omninode.yaml.`
982
+ );
983
+ }
984
+ if (input.agent && !this.options.agents.get(input.agent)) {
985
+ throw new TaskError(
986
+ "TASK_INVALID",
987
+ `Agent "${input.agent}" is not registered. Add it with \`omninode agent add\`.`
988
+ );
989
+ }
990
+ const now = (/* @__PURE__ */ new Date()).toISOString();
991
+ const task = {
992
+ id: generateTaskId(),
993
+ objective: input.objective.trim(),
994
+ status: "created",
995
+ createdAt: now,
996
+ updatedAt: now,
997
+ ...input.project !== void 0 ? { project: input.project } : {},
998
+ ...input.role !== void 0 ? { role: input.role } : {},
999
+ ...input.agent !== void 0 ? { agent: input.agent } : {},
1000
+ ...input.context !== void 0 ? { context: input.context } : {}
1001
+ };
1002
+ await this.options.store.save(task);
1003
+ this.log.info(`Created task ${task.id}.`);
1004
+ return task;
1005
+ }
1006
+ async queue(id) {
1007
+ const task = await this.mustGet(id);
1008
+ return this.transition(task, "queued");
1009
+ }
1010
+ /** Runs the task through its assigned agent and records the result. */
1011
+ async run(id) {
1012
+ const task = await this.mustGet(id);
1013
+ if (task.status !== "created" && task.status !== "queued") {
1014
+ throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
1015
+ }
1016
+ if (!task.agent) {
1017
+ throw new TaskError("TASK_INVALID", `Task ${id} has no agent assigned. Re-create it with --agent.`);
1018
+ }
1019
+ const agent = this.options.agents.get(task.agent);
1020
+ if (!agent) {
1021
+ throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
1022
+ }
1023
+ const role = task.role ? this.options.roles.get(task.role) : void 0;
1024
+ const running = await this.transition(task, "running");
1025
+ this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
1026
+ let output;
1027
+ try {
1028
+ output = await agent.run({
1029
+ taskId: running.id,
1030
+ objective: running.objective,
1031
+ ...role ? { role } : {},
1032
+ ...running.context?.background ? { context: running.context.background } : {}
1033
+ });
1034
+ } catch (error) {
1035
+ const message = error instanceof Error ? error.message : String(error);
1036
+ this.log.error(`Task ${task.id} failed: ${message}`);
1037
+ return this.transition(running, "failed", {
1038
+ result: { error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }
1039
+ });
1040
+ }
1041
+ const result = {
1042
+ ...output.summary !== void 0 ? { summary: output.summary } : {},
1043
+ ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
1044
+ ...output.error !== void 0 ? { error: output.error } : {},
1045
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1046
+ };
1047
+ const finalStatus = output.status === "completed" ? "completed" : "failed";
1048
+ this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
1049
+ return this.transition(running, finalStatus, { result });
1050
+ }
1051
+ async cancel(id) {
1052
+ const task = await this.mustGet(id);
1053
+ if (!ALLOWED_TRANSITIONS[task.status].includes("cancelled")) {
1054
+ throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be cancelled.`);
1055
+ }
1056
+ if (task.agent) {
1057
+ const agent = this.options.agents.get(task.agent);
1058
+ await agent?.cancel?.(task.id);
1059
+ }
1060
+ return this.transition(task, "cancelled");
1061
+ }
1062
+ async get(id) {
1063
+ return this.options.store.get(id);
1064
+ }
1065
+ async list(filter) {
1066
+ return this.options.store.list(filter);
1067
+ }
1068
+ async mustGet(id) {
1069
+ const task = await this.options.store.get(id);
1070
+ if (!task) {
1071
+ throw new TaskError("TASK_NOT_FOUND", `Task "${id}" does not exist in this project's task store.`);
1072
+ }
1073
+ return task;
1074
+ }
1075
+ async transition(task, to, extra = {}) {
1076
+ const allowed = ALLOWED_TRANSITIONS[task.status] ?? [];
1077
+ if (!allowed.includes(to)) {
1078
+ throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
1079
+ }
1080
+ const updated = {
1081
+ ...task,
1082
+ status: to,
1083
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1084
+ ...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
1085
+ };
1086
+ await this.options.store.save(updated);
1087
+ return updated;
1088
+ }
1089
+ };
1090
+ function generateTaskId() {
1091
+ return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1092
+ }
1093
+
1094
+ // src/tasks/engine-factory.ts
1095
+ function createTaskEngine(config, log = logger, directory) {
1096
+ return new TaskEngine({
1097
+ agents: buildAgentRegistry(config, log),
1098
+ roles: buildRoleRegistry(config),
1099
+ store: new FileTaskStore(directory),
1100
+ log
1101
+ });
1102
+ }
1103
+
594
1104
  // src/version.ts
595
- var OMNINODE_VERSION = "0.3.0";
1105
+ var OMNINODE_VERSION = "0.4.0";
596
1106
  export {
597
1107
  AgentError,
1108
+ AgentRegistry,
598
1109
  ConfigError,
599
1110
  ConsoleLogSink,
1111
+ FileTaskStore,
600
1112
  Logger,
601
1113
  MemoryError,
602
1114
  ModelRegistry,
@@ -606,13 +1118,21 @@ export {
606
1118
  OmniNodeError,
607
1119
  OpenAICompatibleProvider,
608
1120
  PipelineError,
1121
+ ProcessAgent,
609
1122
  ProtocolError,
610
1123
  ProviderError,
1124
+ RoleRegistry,
1125
+ TaskEngine,
611
1126
  TaskError,
612
1127
  agentConfigSchema,
613
1128
  appConfigSchema,
614
1129
  authHeaders,
1130
+ buildAgentRegistry,
1131
+ buildRoleRegistry,
1132
+ composeTaskText,
1133
+ createAgent,
615
1134
  createProvider,
1135
+ createTaskEngine,
616
1136
  defaultProjectConfigYaml,
617
1137
  expandEnvRefs,
618
1138
  findConfigFile,