@hilbras/omninode 2.0.0-alpha.1 → 2.0.0-alpha.3

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
@@ -1,3 +1,12 @@
1
+ // src/types/task.ts
2
+ var TERMINAL_TASK_STATUSES = [
3
+ "completed",
4
+ "failed",
5
+ "timed_out",
6
+ "cancelled",
7
+ "partially_completed"
8
+ ];
9
+
1
10
  // src/errors/index.ts
2
11
  var OmniNodeError = class extends Error {
3
12
  code;
@@ -664,6 +673,344 @@ var ModelRegistry = class {
664
673
 
665
674
  // src/agents/process/index.ts
666
675
  import { spawn } from "child_process";
676
+
677
+ // src/agent-protocol/version.ts
678
+ var PROTOCOL_V2 = "omninode-agent-protocol/2";
679
+ var SUPPORTED_PROTOCOL_VERSIONS = [
680
+ "omninode-agent-protocol/1",
681
+ PROTOCOL_V2
682
+ ];
683
+ var MAX_MESSAGE_BYTES = 1048576;
684
+ function isSupportedProtocol(protocol) {
685
+ return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
686
+ }
687
+
688
+ // src/agent-protocol/types.ts
689
+ function newMessageId(prefix = "msg") {
690
+ const random = Math.random().toString(36).slice(2, 10);
691
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
692
+ }
693
+ function envelope(type, payload, ids = {}) {
694
+ return {
695
+ protocol: PROTOCOL_V2,
696
+ messageId: newMessageId(),
697
+ ...ids.requestId !== void 0 ? { requestId: ids.requestId } : {},
698
+ ...ids.taskId !== void 0 ? { taskId: ids.taskId } : {},
699
+ ...ids.agentId !== void 0 ? { agentId: ids.agentId } : {},
700
+ ...ids.pipelineId !== void 0 ? { pipelineId: ids.pipelineId } : {},
701
+ ...ids.executionId !== void 0 ? { executionId: ids.executionId } : {},
702
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
703
+ type,
704
+ payload,
705
+ ...ids.metadata !== void 0 ? { metadata: ids.metadata } : {}
706
+ };
707
+ }
708
+
709
+ // src/agent-protocol/codec.ts
710
+ var KNOWN_TYPES = /* @__PURE__ */ new Set([
711
+ // node → agent
712
+ "TASK",
713
+ "HELLO_ACK",
714
+ "CONTEXT",
715
+ "INSTRUCTION",
716
+ "RESPONSE",
717
+ "CANCEL",
718
+ // agent → node
719
+ "HELLO",
720
+ "TASK_REQUEST",
721
+ "TASK_ACCEPTED",
722
+ "TASK_STARTED",
723
+ "STATUS",
724
+ "QUESTION",
725
+ "REPORT",
726
+ "ARTIFACT",
727
+ "COMPLETION",
728
+ "ERROR",
729
+ "CANCELLED"
730
+ ]);
731
+ var PAYLOAD_REQUIRED_TYPES = /* @__PURE__ */ new Set(["TASK", "REPORT", "COMPLETION", "ERROR", "QUESTION", "ARTIFACT", "HELLO"]);
732
+ function validateEnvelope(value, line) {
733
+ const sample = line?.slice(0, 200);
734
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
735
+ return { ok: false, error: { code: "NOT_AN_OBJECT", message: "message must be a JSON object", ...sample ? { sample } : {} } };
736
+ }
737
+ const raw = value;
738
+ const protocol = raw.protocol;
739
+ if (protocol === void 0) {
740
+ const type = raw.type;
741
+ if (typeof type !== "string" || !KNOWN_TYPES.has(type)) {
742
+ return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(type)}`, ...sample ? { sample } : {} } };
743
+ }
744
+ return {
745
+ ok: true,
746
+ legacy: true,
747
+ envelope: {
748
+ protocol: "omninode-agent-protocol/1",
749
+ messageId: typeof raw.messageId === "string" ? raw.messageId : "legacy",
750
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
751
+ type,
752
+ payload: raw.payload
753
+ }
754
+ };
755
+ }
756
+ if (typeof protocol !== "string" || !isSupportedProtocol(protocol)) {
757
+ return {
758
+ ok: false,
759
+ error: { code: "UNSUPPORTED_VERSION", message: `unsupported protocol "${String(protocol)}"`, ...sample ? { sample } : {} }
760
+ };
761
+ }
762
+ if (raw.type === void 0) {
763
+ return { ok: false, error: { code: "MISSING_FIELD", message: "message is missing 'type'", ...sample ? { sample } : {} } };
764
+ }
765
+ if (typeof raw.type !== "string" || !KNOWN_TYPES.has(raw.type)) {
766
+ return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(raw.type)}`, ...sample ? { sample } : {} } };
767
+ }
768
+ if (raw.timestamp !== void 0 && typeof raw.timestamp !== "string") {
769
+ return { ok: false, error: { code: "MISSING_FIELD", message: "'timestamp' must be a string", ...sample ? { sample } : {} } };
770
+ }
771
+ if (PAYLOAD_REQUIRED_TYPES.has(raw.type) && (raw.payload === void 0 || raw.payload === null)) {
772
+ return { ok: false, error: { code: "INVALID_PAYLOAD", message: `${raw.type} requires a payload`, ...sample ? { sample } : {} } };
773
+ }
774
+ return {
775
+ ok: true,
776
+ legacy: protocol !== PROTOCOL_V2,
777
+ envelope: {
778
+ protocol,
779
+ messageId: typeof raw.messageId === "string" ? raw.messageId : "unknown",
780
+ ...typeof raw.requestId === "string" ? { requestId: raw.requestId } : {},
781
+ ...typeof raw.taskId === "string" ? { taskId: raw.taskId } : {},
782
+ ...typeof raw.agentId === "string" ? { agentId: raw.agentId } : {},
783
+ ...typeof raw.pipelineId === "string" ? { pipelineId: raw.pipelineId } : {},
784
+ ...typeof raw.executionId === "string" ? { executionId: raw.executionId } : {},
785
+ timestamp: typeof raw.timestamp === "string" ? raw.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
786
+ type: raw.type,
787
+ payload: raw.payload,
788
+ ...raw.metadata !== null && typeof raw.metadata === "object" ? { metadata: raw.metadata } : {}
789
+ }
790
+ };
791
+ }
792
+ function decodeMessage(line, maxBytes = MAX_MESSAGE_BYTES) {
793
+ const trimmed = line.trim();
794
+ if (trimmed.length === 0) {
795
+ return { ok: false, error: { code: "INVALID_JSON", message: "empty line" } };
796
+ }
797
+ if (trimmed.length > maxBytes) {
798
+ return {
799
+ ok: false,
800
+ error: { code: "OVERSIZED", message: `message of ${trimmed.length} bytes exceeds the ${maxBytes} byte limit`, sample: trimmed.slice(0, 200) }
801
+ };
802
+ }
803
+ let value;
804
+ try {
805
+ value = JSON.parse(trimmed);
806
+ } catch (error) {
807
+ return {
808
+ ok: false,
809
+ error: { code: "INVALID_JSON", message: error instanceof Error ? error.message : "invalid JSON", sample: trimmed.slice(0, 200) }
810
+ };
811
+ }
812
+ return validateEnvelope(value, trimmed);
813
+ }
814
+ function encodeMessage(envelope2, maxBytes = MAX_MESSAGE_BYTES) {
815
+ const line = JSON.stringify(envelope2);
816
+ if (line.length > maxBytes) {
817
+ throw new Error(`outbound protocol message of ${line.length} bytes exceeds the ${maxBytes} byte limit`);
818
+ }
819
+ return line;
820
+ }
821
+ var ProtocolDecoder = class {
822
+ buffer = "";
823
+ maxBytes;
824
+ constructor(maxBytes = MAX_MESSAGE_BYTES) {
825
+ this.maxBytes = maxBytes;
826
+ }
827
+ push(chunk) {
828
+ this.buffer += chunk;
829
+ const results = [];
830
+ let index = this.buffer.indexOf("\n");
831
+ while (index >= 0) {
832
+ const line = this.buffer.slice(0, index);
833
+ this.buffer = this.buffer.slice(index + 1);
834
+ if (line.trim().length > 0) results.push(decodeMessage(line, this.maxBytes));
835
+ index = this.buffer.indexOf("\n");
836
+ }
837
+ if (this.buffer.length > this.maxBytes) {
838
+ results.push({
839
+ ok: false,
840
+ error: { code: "OVERSIZED", message: `line exceeded ${this.maxBytes} bytes without a newline`, sample: this.buffer.slice(0, 200) }
841
+ });
842
+ this.buffer = "";
843
+ }
844
+ return results;
845
+ }
846
+ /** Flush any trailing line when the process exits. */
847
+ flush() {
848
+ if (this.buffer.trim().length === 0) {
849
+ this.buffer = "";
850
+ return [];
851
+ }
852
+ const result = decodeMessage(this.buffer, this.maxBytes);
853
+ this.buffer = "";
854
+ return [result];
855
+ }
856
+ };
857
+
858
+ // src/agent-protocol/handshake.ts
859
+ function parseHello(envelopeValue) {
860
+ if (envelopeValue.type !== "HELLO") {
861
+ return { ok: false, reason: `expected HELLO, received ${envelopeValue.type}` };
862
+ }
863
+ const payload = envelopeValue.payload;
864
+ if (payload === null || typeof payload !== "object") {
865
+ return { ok: false, reason: "HELLO payload must be an object" };
866
+ }
867
+ const p = payload;
868
+ if (typeof p.name !== "string" || p.name.length === 0) {
869
+ return { ok: false, reason: "HELLO payload is missing 'name'" };
870
+ }
871
+ return {
872
+ ok: true,
873
+ descriptor: {
874
+ protocol: envelopeValue.protocol,
875
+ name: p.name,
876
+ ...typeof p.version === "string" ? { version: p.version } : {},
877
+ capabilities: Array.isArray(p.capabilities) ? p.capabilities.filter((c) => typeof c === "string") : [],
878
+ supports: Array.isArray(p.supports) ? p.supports.filter((s) => typeof s === "string") : []
879
+ }
880
+ };
881
+ }
882
+ function buildHello(descriptor) {
883
+ return envelope("HELLO", descriptor, { agentId: descriptor.name });
884
+ }
885
+ function buildHelloAck(descriptor) {
886
+ return envelope("HELLO_ACK", {
887
+ ok: true,
888
+ protocol: PROTOCOL_V2,
889
+ agent: descriptor.name,
890
+ ...descriptor.version !== void 0 ? { agentVersion: descriptor.version } : {},
891
+ supportedProtocols: [PROTOCOL_V2]
892
+ }, { agentId: descriptor.name });
893
+ }
894
+
895
+ // src/agent-protocol/session.ts
896
+ var ProtocolSession = class {
897
+ decoder;
898
+ ids;
899
+ responder;
900
+ violations = [];
901
+ questions = [];
902
+ artifacts = [];
903
+ reports = [];
904
+ descriptor;
905
+ completionSummary;
906
+ errorMessage;
907
+ constructor(ids, options = {}) {
908
+ this.ids = ids;
909
+ this.responder = options.responder;
910
+ this.decoder = new ProtocolDecoder(options.maxBytes);
911
+ }
912
+ /** The v2 messages OmniNode sends to open a task (written to the agent's stdin). */
913
+ taskMessages(objective, role, context, instruction) {
914
+ const ids = this.ids;
915
+ const messages = [envelope("TASK", { taskId: ids.taskId, objective }, ids)];
916
+ if (role !== void 0) messages.push(envelope("CONTEXT", { role }, ids));
917
+ if (context !== void 0 && context.length > 0) messages.push(envelope("CONTEXT", { body: context }, ids));
918
+ if (instruction !== void 0 && instruction.length > 0) messages.push(envelope("INSTRUCTION", { text: instruction }, ids));
919
+ return messages.map((m) => encodeMessage(m));
920
+ }
921
+ helloAck(descriptor) {
922
+ return encodeMessage(buildHelloAck(descriptor));
923
+ }
924
+ /** Consumes decoded stdout, answering questions as they arrive. */
925
+ async handle(decoded) {
926
+ if (!decoded.ok) {
927
+ this.violations.push(decoded.error);
928
+ return;
929
+ }
930
+ const message = decoded.envelope;
931
+ if (message.protocol === PROTOCOL_V2) this.sawV2 = true;
932
+ if (message.type === "HELLO") {
933
+ const hello = parseHello(message);
934
+ if (hello.ok) this.descriptor = hello.descriptor;
935
+ else this.violations.push({ code: "INVALID_PAYLOAD", message: `bad HELLO: ${hello.reason}` });
936
+ return;
937
+ }
938
+ if (message.type === "QUESTION") {
939
+ await this.answerQuestion(message);
940
+ return;
941
+ }
942
+ if (message.type === "REPORT") {
943
+ this.reports.push(message.payload);
944
+ return;
945
+ }
946
+ if (message.type === "ARTIFACT") {
947
+ this.artifacts.push(message.payload);
948
+ return;
949
+ }
950
+ if (message.type === "COMPLETION") {
951
+ const payload = message.payload;
952
+ this.completionSummary = typeof payload?.summary === "string" ? payload.summary : "";
953
+ return;
954
+ }
955
+ if (message.type === "ERROR") {
956
+ const payload = message.payload;
957
+ const detail = typeof payload?.message === "string" ? payload.message : "agent reported an error";
958
+ const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
959
+ this.errorMessage = `Agent error${code}: ${detail}`;
960
+ return;
961
+ }
962
+ if (message.type === "TASK_ACCEPTED" || message.type === "TASK_STARTED" || message.type === "STATUS") {
963
+ return;
964
+ }
965
+ this.violations.push({
966
+ code: "UNKNOWN_TYPE",
967
+ message: `unexpected message type ${message.type} from agent`
968
+ });
969
+ }
970
+ async answerQuestion(message) {
971
+ const payload = message.payload ?? {};
972
+ const questionId = typeof payload.questionId === "string" ? payload.questionId : message.messageId;
973
+ const question = typeof payload.question === "string" ? payload.question : "";
974
+ const expected = typeof payload.expected === "string" ? payload.expected : "text";
975
+ let answer;
976
+ if (this.responder) {
977
+ try {
978
+ answer = await this.responder({ questionId, question, expected });
979
+ } catch (error) {
980
+ this.violations.push({
981
+ code: "INVALID_PAYLOAD",
982
+ message: `question responder failed: ${error instanceof Error ? error.message : String(error)}`
983
+ });
984
+ }
985
+ }
986
+ this.questions.push({ questionId, question, ...answer !== void 0 ? { answer } : {} });
987
+ this.pendingAnswers.push(
988
+ encodeMessage(
989
+ envelope("RESPONSE", { questionId, ...answer !== void 0 ? { body: answer } : {} }, {
990
+ ...this.ids,
991
+ requestId: message.requestId ?? this.ids.requestId
992
+ })
993
+ )
994
+ );
995
+ }
996
+ pendingAnswers = [];
997
+ /** True once any inbound message carried the v2 protocol field. */
998
+ sawV2 = false;
999
+ };
1000
+ async function decodeStream(session, chunk) {
1001
+ const decoded = session.decoder.push(chunk);
1002
+ for (const result of decoded) {
1003
+ await session.handle(result);
1004
+ }
1005
+ return session.pendingAnswers.splice(0);
1006
+ }
1007
+ function requireCompletion(session) {
1008
+ if (session.errorMessage !== void 0) {
1009
+ throw new ProtocolError(session.errorMessage);
1010
+ }
1011
+ }
1012
+
1013
+ // src/agents/process/index.ts
667
1014
  var DEFAULT_TIMEOUT_MS2 = 6e5;
668
1015
  var KILL_GRACE_MS = 5e3;
669
1016
  function buildChildEnv(config) {
@@ -697,18 +1044,19 @@ ${input.instruction}`);
697
1044
  return sections.join("\n\n");
698
1045
  }
699
1046
  function buildProtocolMessages(input, agentName) {
700
- const meta = {
1047
+ const session = new ProtocolSession({
1048
+ requestId: `req-${input.taskId}`,
701
1049
  taskId: input.taskId,
702
- agent: agentName,
703
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
704
- };
705
- const messages = [
706
- { type: "TASK", payload: { taskId: input.taskId, objective: input.objective } }
707
- ];
708
- if (input.role) messages.push({ type: "ROLE", payload: input.role });
709
- if (input.context) messages.push({ type: "CONTEXT", payload: { body: input.context } });
710
- if (input.instruction) messages.push({ type: "INSTRUCTION", payload: { text: input.instruction } });
711
- return messages.map((message) => JSON.stringify({ meta, message }));
1050
+ agentId: agentName,
1051
+ ...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
1052
+ ...input.executionId !== void 0 ? { executionId: input.executionId } : {}
1053
+ });
1054
+ return session.taskMessages(
1055
+ input.objective,
1056
+ input.role,
1057
+ input.context,
1058
+ input.instruction
1059
+ );
712
1060
  }
713
1061
  var ProcessAgent = class {
714
1062
  config;
@@ -804,7 +1152,19 @@ var ProcessAgent = class {
804
1152
  new AgentError(
805
1153
  "AGENT_TIMEOUT",
806
1154
  `Agent "${this.config.name}" timed out after ${timeoutMs}ms and was killed.`,
807
- { details: { taskId: input.taskId, stdout: stdout.slice(0, 4e3), stderr: stderr.slice(0, 2e3) } }
1155
+ {
1156
+ details: {
1157
+ operation: "agent.run",
1158
+ agent: this.config.name,
1159
+ durationMs: timeoutMs,
1160
+ // The process was started and may have performed side
1161
+ // effects before being killed — the outcome is unknown,
1162
+ // not failed (roadmap §6.3).
1163
+ dispatched: true,
1164
+ stdout: stdout.slice(0, 4e3),
1165
+ stderr: stderr.slice(0, 2e3)
1166
+ }
1167
+ }
808
1168
  )
809
1169
  );
810
1170
  return;
@@ -849,62 +1209,75 @@ var ProcessAgent = class {
849
1209
  return [...this.running.keys()];
850
1210
  }
851
1211
  };
852
- function interpretProtocolRun({ input, agentName, stdout, exitCode, logs }) {
853
- const reports = [];
854
- let summary;
855
- let error;
856
- let sawProtocolMessage = false;
857
- for (const line of stdout.split("\n")) {
858
- const trimmed2 = line.trim();
859
- if (trimmed2.length === 0) continue;
860
- const message = parseAgentMessage(trimmed2);
861
- if (!message) continue;
862
- sawProtocolMessage = true;
863
- if (message.type === "REPORT") {
864
- const report = normalizeReport(message.payload, input.taskId, agentName, reports.length);
865
- if (report) reports.push(report);
866
- } else if (message.type === "COMPLETION") {
867
- const payload = message.payload;
868
- if (typeof payload?.summary === "string") summary = payload.summary;
869
- } else if (message.type === "ERROR") {
870
- const payload = message.payload;
871
- const detail = typeof payload?.message === "string" ? payload.message : "unknown error";
872
- const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
873
- error = `Agent error${code}: ${detail}`;
874
- }
1212
+ function interpretProtocolRun({
1213
+ input,
1214
+ agentName,
1215
+ stdout,
1216
+ exitCode,
1217
+ logs,
1218
+ responder
1219
+ }) {
1220
+ const session = new ProtocolSession(
1221
+ {
1222
+ requestId: `req-${input.taskId}`,
1223
+ taskId: input.taskId,
1224
+ agentId: agentName,
1225
+ ...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
1226
+ ...input.executionId !== void 0 ? { executionId: input.executionId } : {}
1227
+ },
1228
+ { ...responder !== void 0 ? { responder } : {} }
1229
+ );
1230
+ for (const result of session.decoder.push(stdout)) {
1231
+ session.handle(result);
875
1232
  }
876
- if (!sawProtocolMessage && exitCode === 0) {
877
- throw new ProtocolError(
878
- `Agent "${agentName}" produced no protocol messages on stdout (input mode "protocol").`,
879
- { details: { taskId: input.taskId, stdout: stdout.slice(0, 2e3) } }
880
- );
1233
+ for (const result of session.decoder.flush()) {
1234
+ session.handle(result);
881
1235
  }
1236
+ const reports = session.reports.flatMap((payload) => Array.isArray(payload) ? payload : [payload]).map((payload, index) => normalizeReport(payload, input.taskId, agentName, index)).filter((report) => report !== void 0);
882
1237
  const trimmed = stdout.trim();
1238
+ const protocolBlock = {
1239
+ protocol: session.descriptor?.protocol ?? PROTOCOL_V2,
1240
+ legacy: !session.sawV2 && session.descriptor === void 0,
1241
+ ...session.descriptor !== void 0 ? { descriptor: session.descriptor } : {},
1242
+ questions: session.questions,
1243
+ violations: session.violations.map((v) => `${v.code}: ${v.message}`)
1244
+ };
1245
+ if (session.errorMessage !== void 0) {
1246
+ return {
1247
+ taskId: input.taskId,
1248
+ status: "failed",
1249
+ error: session.errorMessage,
1250
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
1251
+ exitCode: exitCode ?? void 0,
1252
+ logs,
1253
+ protocol: protocolBlock,
1254
+ ...reports.length > 0 ? { reports } : {}
1255
+ };
1256
+ }
1257
+ if (session.descriptor === void 0 && session.violations.length > 0 && session.reports.length === 0 && session.completionSummary === void 0) {
1258
+ return {
1259
+ taskId: input.taskId,
1260
+ status: "failed",
1261
+ error: `agent produced no valid protocol messages: ${protocolBlock.violations.join("; ")}`,
1262
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
1263
+ exitCode: exitCode ?? void 0,
1264
+ logs,
1265
+ protocol: protocolBlock
1266
+ };
1267
+ }
1268
+ const completed = exitCode === 0 && session.completionSummary !== void 0;
883
1269
  return {
884
1270
  taskId: input.taskId,
885
- status: error !== void 0 || exitCode !== 0 ? "failed" : "completed",
886
- summary: summary ?? reports[0]?.summary,
1271
+ status: completed || exitCode === 0 ? completed ? "completed" : "completed" : "failed",
1272
+ summary: session.completionSummary ?? reports[0]?.summary,
887
1273
  rawOutput: trimmed.length > 0 ? trimmed : void 0,
888
1274
  exitCode: exitCode ?? void 0,
889
1275
  logs,
890
- reports: reports.length > 0 ? reports : void 0,
891
- error: error ?? (exitCode !== 0 ? `Process exited with code ${exitCode ?? "unknown"}` : void 0)
1276
+ protocol: protocolBlock,
1277
+ ...reports.length > 0 ? { reports } : {},
1278
+ ...exitCode !== 0 ? { error: `Process exited with code ${exitCode ?? "unknown"}` } : {}
892
1279
  };
893
1280
  }
894
- function parseAgentMessage(line) {
895
- try {
896
- const parsed = JSON.parse(line);
897
- if (typeof parsed.type === "string") {
898
- return { type: parsed.type, payload: parsed.payload };
899
- }
900
- const inner = parsed.message;
901
- if (inner && typeof inner.type === "string") {
902
- return { type: inner.type, payload: inner.payload };
903
- }
904
- } catch {
905
- }
906
- return void 0;
907
- }
908
1281
  function normalizeReport(payload, taskId, agentName, index) {
909
1282
  if (payload === null || typeof payload !== "object") return void 0;
910
1283
  const raw = payload;
@@ -1082,12 +1455,18 @@ import { randomBytes } from "crypto";
1082
1455
  var ALLOWED_TRANSITIONS = {
1083
1456
  created: ["queued", "running", "cancelled"],
1084
1457
  queued: ["running", "cancelled"],
1085
- running: ["completed", "failed", "cancelled"],
1458
+ running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
1086
1459
  waiting: ["running", "cancelled"],
1087
1460
  completed: [],
1088
- failed: [],
1089
- cancelled: []
1461
+ // failed → running is the only re-entry: bounded automatic retry (§6.4).
1462
+ failed: ["running"],
1463
+ timed_out: [],
1464
+ cancelled: [],
1465
+ unknown: [],
1466
+ partially_completed: []
1090
1467
  };
1468
+ var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
1469
+ var EXECUTION_HISTORY_LIMIT = 10;
1091
1470
  var TaskEngine = class {
1092
1471
  constructor(options) {
1093
1472
  this.options = options;
@@ -1119,6 +1498,7 @@ var TaskEngine = class {
1119
1498
  id: generateTaskId(),
1120
1499
  objective: input.objective.trim(),
1121
1500
  status: "created",
1501
+ attempt: 0,
1122
1502
  createdAt: now,
1123
1503
  updatedAt: now,
1124
1504
  ...input.project !== void 0 ? { project: input.project } : {},
@@ -1135,8 +1515,10 @@ var TaskEngine = class {
1135
1515
  const task = await this.mustGet(id);
1136
1516
  return this.transition(task, "queued");
1137
1517
  }
1138
- /** Runs the task through its assigned agent and records the result. */
1139
- async run(id) {
1518
+ /** Runs the task through its assigned agent and records the result.
1519
+ * `maxAttempts` enables bounded automatic retries of *retryable* errors
1520
+ * with linear backoff; unknown/timed_out outcomes are never auto-retried. */
1521
+ async run(id, options = {}) {
1140
1522
  const task = await this.mustGet(id);
1141
1523
  if (task.status !== "created" && task.status !== "queued") {
1142
1524
  throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
@@ -1149,49 +1531,84 @@ var TaskEngine = class {
1149
1531
  throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
1150
1532
  }
1151
1533
  const role = task.role ? this.options.roles.get(task.role) : void 0;
1152
- const running = await this.transition(task, "running");
1153
1534
  this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
1154
- let background = running.context?.background;
1535
+ let background = task.context?.background;
1155
1536
  if (this.options.memory) {
1156
1537
  const memoryContext = await this.options.memory.gatherContext({
1157
- objective: running.objective,
1158
- ...running.role ? { role: running.role } : {},
1159
- ...running.project ? { project: running.project } : {}
1538
+ objective: task.objective,
1539
+ ...task.role ? { role: task.role } : {},
1540
+ ...task.project ? { project: task.project } : {}
1160
1541
  });
1161
1542
  if (memoryContext.length > 0) {
1162
1543
  background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
1163
1544
  }
1164
1545
  }
1165
- let output;
1166
- try {
1167
- output = await agent.run({
1168
- taskId: running.id,
1169
- objective: running.objective,
1170
- ...role ? { role } : {},
1171
- ...background !== void 0 ? { context: background } : {}
1172
- });
1173
- } catch (error) {
1174
- const message = error instanceof Error ? error.message : String(error);
1175
- this.log.error(`Task ${task.id} failed: ${message}`);
1176
- return this.transition(running, "failed", {
1177
- result: { error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }
1546
+ const maxAttempts = Math.max(1, options.maxAttempts ?? 1);
1547
+ const backoffMs = options.retryBackoffMs ?? 0;
1548
+ let current = task;
1549
+ for (let i = 0; i < maxAttempts; i += 1) {
1550
+ const attempt = current.attempt + 1;
1551
+ const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1552
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1553
+ current = await this.transition(current, "running", { attempt });
1554
+ let output;
1555
+ let thrown;
1556
+ try {
1557
+ output = await agent.run({
1558
+ taskId: current.id,
1559
+ objective: current.objective,
1560
+ ...role ? { role } : {},
1561
+ ...background !== void 0 ? { context: background } : {}
1562
+ });
1563
+ } catch (error) {
1564
+ thrown = error;
1565
+ }
1566
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
1567
+ if (thrown !== void 0 || output === void 0) {
1568
+ const outcome = classifyThrown(thrown);
1569
+ const message = thrown instanceof Error ? thrown.message : String(thrown);
1570
+ const retryable = outcome === "failed" && RETRYABLE_ERROR_CODES.includes(errorCode(thrown)) && i < maxAttempts - 1;
1571
+ current = await this.transition(current, outcome, {
1572
+ attempt,
1573
+ lastError: message,
1574
+ execution: {
1575
+ executionId,
1576
+ attempt,
1577
+ startedAt,
1578
+ finishedAt,
1579
+ outcome,
1580
+ error: message
1581
+ }
1582
+ });
1583
+ this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
1584
+ if (retryable) {
1585
+ if (backoffMs > 0) await sleep(backoffMs * attempt);
1586
+ continue;
1587
+ }
1588
+ if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
1589
+ return current;
1590
+ }
1591
+ const result = {
1592
+ ...output.summary !== void 0 ? { summary: output.summary } : {},
1593
+ ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
1594
+ // Keep the verbatim output (bounded) for report extraction and audit.
1595
+ ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
1596
+ ...output.error !== void 0 ? { error: output.error } : {},
1597
+ finishedAt
1598
+ };
1599
+ const finalStatus = output.status === "completed" ? "completed" : "failed";
1600
+ current = await this.transition(current, finalStatus, {
1601
+ attempt,
1602
+ result,
1603
+ execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
1178
1604
  });
1605
+ this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
1606
+ break;
1179
1607
  }
1180
- const result = {
1181
- ...output.summary !== void 0 ? { summary: output.summary } : {},
1182
- ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
1183
- // Keep the verbatim output (bounded) for report extraction and audit.
1184
- ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
1185
- ...output.error !== void 0 ? { error: output.error } : {},
1186
- finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1187
- };
1188
- const finalStatus = output.status === "completed" ? "completed" : "failed";
1189
- const finished = await this.transition(running, finalStatus, { result });
1190
- this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
1191
1608
  if (this.options.memory) {
1192
- await this.options.memory.recordTaskOutcome(finished);
1609
+ await this.options.memory.recordTaskOutcome(current);
1193
1610
  }
1194
- return finished;
1611
+ return current;
1195
1612
  }
1196
1613
  async cancel(id) {
1197
1614
  const task = await this.mustGet(id);
@@ -1204,13 +1621,14 @@ var TaskEngine = class {
1204
1621
  }
1205
1622
  return this.transition(task, "cancelled");
1206
1623
  }
1207
- /** Error recovery (§27 Phase 9): reset a failed/cancelled task so it can run again. */
1624
+ /** Error recovery: reset a failed/cancelled/unknown/timed_out task so it can run again. */
1208
1625
  async retry(id) {
1209
1626
  const task = await this.mustGet(id);
1210
- if (task.status !== "failed" && task.status !== "cancelled") {
1627
+ const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
1628
+ if (!retryableStates.includes(task.status)) {
1211
1629
  throw new TaskError(
1212
1630
  "TASK_INVALID",
1213
- `Only failed or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
1631
+ `Only failed, timed out, unknown or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
1214
1632
  );
1215
1633
  }
1216
1634
  const { result: _discarded, ...rest } = task;
@@ -1242,10 +1660,14 @@ var TaskEngine = class {
1242
1660
  if (!allowed.includes(to)) {
1243
1661
  throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
1244
1662
  }
1663
+ const executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
1245
1664
  const updated = {
1246
1665
  ...task,
1247
1666
  status: to,
1248
1667
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1668
+ ...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
1669
+ ...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
1670
+ ...executions !== void 0 ? { executions } : {},
1249
1671
  ...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
1250
1672
  };
1251
1673
  await this.options.store.save(updated);
@@ -1266,6 +1688,19 @@ var TaskEngine = class {
1266
1688
  function generateTaskId() {
1267
1689
  return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1268
1690
  }
1691
+ function errorCode(error) {
1692
+ return error?.code ?? "";
1693
+ }
1694
+ function classifyThrown(error) {
1695
+ if (error instanceof AgentError && error.code === "AGENT_TIMEOUT") {
1696
+ const dispatched = error.details?.dispatched;
1697
+ return dispatched === false ? "timed_out" : "unknown";
1698
+ }
1699
+ return "failed";
1700
+ }
1701
+ function sleep(ms) {
1702
+ return new Promise((resolve) => setTimeout(resolve, ms));
1703
+ }
1269
1704
 
1270
1705
  // src/audit/index.ts
1271
1706
  import { appendFile, mkdir as mkdir2 } from "fs/promises";
@@ -1974,7 +2409,7 @@ ${contribution}`);
1974
2409
  }
1975
2410
  const stepRun = {
1976
2411
  stepId: step.id,
1977
- status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
2412
+ status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
1978
2413
  ...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
1979
2414
  ...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
1980
2415
  ...outcome.error !== void 0 ? { error: outcome.error } : {}
@@ -1985,8 +2420,10 @@ ${contribution}`);
1985
2420
  run.reports = [...allReports];
1986
2421
  await this.options.store.save(run);
1987
2422
  }
1988
- const failed = [...outcomes.values()].some((o) => o.status === "failed");
1989
- const finalStatus = failed ? "failed" : "completed";
2423
+ const allOutcomes = [...outcomes.values()];
2424
+ const failed = allOutcomes.some((o) => o.status === "failed");
2425
+ const partial = !failed && allOutcomes.some((o) => o.status === "partial");
2426
+ const finalStatus = failed ? "failed" : partial ? "partial" : "completed";
1990
2427
  const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
1991
2428
  let resultSummary;
1992
2429
  for (let i = def.steps.length - 1; i >= 0; i -= 1) {
@@ -2092,6 +2529,7 @@ ${contribution}`);
2092
2529
  )
2093
2530
  );
2094
2531
  const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
2532
+ const completedTasks = results.filter((r) => r.status === "completed");
2095
2533
  const failedTasks = results.filter((r) => r.status !== "completed");
2096
2534
  const lines = results.map((result, index) => {
2097
2535
  const agentName = agentNames[index] ?? "unknown";
@@ -2101,12 +2539,15 @@ ${contribution}`);
2101
2539
  const reports = results.flatMap((r) => r.result?.reports ?? []);
2102
2540
  const summary = lines.join("\n");
2103
2541
  if (failedTasks.length > 0) {
2104
- const firstError = failedTasks[0]?.result?.error ?? "unknown error";
2542
+ const firstError = failedTasks[0]?.result?.error ?? failedTasks[0]?.lastError ?? "unknown outcome";
2543
+ const states = failedTasks.map((r) => r.status).join(", ");
2105
2544
  return {
2106
- status: "failed",
2545
+ // Some agents succeeded → the step produced usable but incomplete
2546
+ // intelligence: partial, not failed (roadmap §9.3).
2547
+ status: completedTasks.length > 0 ? "partial" : "failed",
2107
2548
  summary,
2108
2549
  taskIds: created.map((t) => t.id),
2109
- error: `${failedTasks.length}/${results.length} task(s) failed \u2014 ${firstError}`,
2550
+ error: `${failedTasks.length}/${results.length} task(s) incomplete (${states}) \u2014 ${firstError}`,
2110
2551
  reports
2111
2552
  };
2112
2553
  }
@@ -2122,7 +2563,9 @@ ${contribution}`);
2122
2563
  const result = await this.options.tasks.run(task.id);
2123
2564
  if (result.status !== "completed") {
2124
2565
  return {
2125
- status: "failed",
2566
+ // Unknown/timed_out outcomes are not failures — the work may have
2567
+ // happened remotely; the step is incomplete, not broken.
2568
+ status: result.status === "unknown" || result.status === "timed_out" ? "partial" : "failed",
2126
2569
  summary: result.result?.summary ?? "",
2127
2570
  taskIds: [task.id],
2128
2571
  error: result.result?.error ?? `task ${task.id} did not complete`,
@@ -2594,7 +3037,7 @@ var ProjectStores = class {
2594
3037
  };
2595
3038
 
2596
3039
  // src/version.ts
2597
- var OMNINODE_VERSION = "2.0.0-alpha.1";
3040
+ var OMNINODE_VERSION = "2.0.0-alpha.3";
2598
3041
  export {
2599
3042
  AgentError,
2600
3043
  AgentRegistry,
@@ -2609,6 +3052,7 @@ export {
2609
3052
  JsonFileStore,
2610
3053
  LocalMemoryProvider,
2611
3054
  Logger,
3055
+ MAX_MESSAGE_BYTES,
2612
3056
  MemoryError,
2613
3057
  MemoryService,
2614
3058
  ModelPlanner,
@@ -2618,16 +3062,21 @@ export {
2618
3062
  OmniHilbrasProvider,
2619
3063
  OmniNodeError,
2620
3064
  OpenAICompatibleProvider,
3065
+ PROTOCOL_V2,
2621
3066
  PipelineEngine,
2622
3067
  PipelineError,
2623
3068
  PlannerError,
2624
3069
  ProcessAgent,
2625
3070
  ProjectStores,
3071
+ ProtocolDecoder,
2626
3072
  ProtocolError,
3073
+ ProtocolSession,
2627
3074
  ProviderError,
2628
3075
  RememberaMemoryProvider,
2629
3076
  ReportService,
2630
3077
  RoleRegistry,
3078
+ SUPPORTED_PROTOCOL_VERSIONS,
3079
+ TERMINAL_TASK_STATUSES,
2631
3080
  TaskEngine,
2632
3081
  TaskError,
2633
3082
  agentConfigSchema,
@@ -2636,6 +3085,8 @@ export {
2636
3085
  authHeaders,
2637
3086
  buildAgentRegistry,
2638
3087
  buildChildEnv,
3088
+ buildHello,
3089
+ buildHelloAck,
2639
3090
  buildPipelineEngine,
2640
3091
  buildPlanner,
2641
3092
  buildPlannerContext,
@@ -2646,14 +3097,21 @@ export {
2646
3097
  createMemoryProvider,
2647
3098
  createProvider,
2648
3099
  createTaskEngine,
3100
+ decodeMessage,
3101
+ decodeStream,
2649
3102
  defaultProjectConfigYaml,
3103
+ encodeMessage,
3104
+ envelope,
2650
3105
  expandEnvRefs,
2651
3106
  extractReportFromText,
2652
3107
  findConfigFile,
2653
3108
  isOmniNodeError,
3109
+ isSupportedProtocol,
2654
3110
  keywords,
2655
3111
  loadConfig,
2656
3112
  logger,
3113
+ newMessageId,
3114
+ parseHello,
2657
3115
  parsePlanJson,
2658
3116
  pipelineConfigSchema,
2659
3117
  pipelineStepConfigSchema,
@@ -2662,8 +3120,10 @@ export {
2662
3120
  providerConfigSchema,
2663
3121
  registryKey,
2664
3122
  requestJson,
3123
+ requireCompletion,
2665
3124
  resolveApiKey,
2666
3125
  roleConfigSchema,
2667
- similar
3126
+ similar,
3127
+ validateEnvelope
2668
3128
  };
2669
3129
  //# sourceMappingURL=index.js.map