@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/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 = "2.0.0-alpha.1";
9
+ var OMNINODE_VERSION = "2.0.0-alpha.3";
10
10
 
11
11
  // src/errors/index.ts
12
12
  var OmniNodeError = class extends Error {
@@ -44,11 +44,6 @@ var PipelineError = class extends OmniNodeError {
44
44
  super(code, message, options);
45
45
  }
46
46
  };
47
- var ProtocolError = class extends OmniNodeError {
48
- constructor(message, options = {}) {
49
- super("PROTOCOL_VIOLATION", message, options);
50
- }
51
- };
52
47
  var PlannerError = class extends OmniNodeError {
53
48
  constructor(code, message, options = {}) {
54
49
  super(code, message, options);
@@ -897,6 +892,329 @@ function registerModelsCommand(program) {
897
892
 
898
893
  // src/agents/process/index.ts
899
894
  import { spawn } from "child_process";
895
+
896
+ // src/agent-protocol/version.ts
897
+ var PROTOCOL_V2 = "omninode-agent-protocol/2";
898
+ var SUPPORTED_PROTOCOL_VERSIONS = [
899
+ "omninode-agent-protocol/1",
900
+ PROTOCOL_V2
901
+ ];
902
+ var MAX_MESSAGE_BYTES = 1048576;
903
+ function isSupportedProtocol(protocol) {
904
+ return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
905
+ }
906
+
907
+ // src/agent-protocol/types.ts
908
+ function newMessageId(prefix = "msg") {
909
+ const random = Math.random().toString(36).slice(2, 10);
910
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
911
+ }
912
+ function envelope(type, payload, ids = {}) {
913
+ return {
914
+ protocol: PROTOCOL_V2,
915
+ messageId: newMessageId(),
916
+ ...ids.requestId !== void 0 ? { requestId: ids.requestId } : {},
917
+ ...ids.taskId !== void 0 ? { taskId: ids.taskId } : {},
918
+ ...ids.agentId !== void 0 ? { agentId: ids.agentId } : {},
919
+ ...ids.pipelineId !== void 0 ? { pipelineId: ids.pipelineId } : {},
920
+ ...ids.executionId !== void 0 ? { executionId: ids.executionId } : {},
921
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
922
+ type,
923
+ payload,
924
+ ...ids.metadata !== void 0 ? { metadata: ids.metadata } : {}
925
+ };
926
+ }
927
+
928
+ // src/agent-protocol/codec.ts
929
+ var KNOWN_TYPES = /* @__PURE__ */ new Set([
930
+ // node → agent
931
+ "TASK",
932
+ "HELLO_ACK",
933
+ "CONTEXT",
934
+ "INSTRUCTION",
935
+ "RESPONSE",
936
+ "CANCEL",
937
+ // agent → node
938
+ "HELLO",
939
+ "TASK_REQUEST",
940
+ "TASK_ACCEPTED",
941
+ "TASK_STARTED",
942
+ "STATUS",
943
+ "QUESTION",
944
+ "REPORT",
945
+ "ARTIFACT",
946
+ "COMPLETION",
947
+ "ERROR",
948
+ "CANCELLED"
949
+ ]);
950
+ var PAYLOAD_REQUIRED_TYPES = /* @__PURE__ */ new Set(["TASK", "REPORT", "COMPLETION", "ERROR", "QUESTION", "ARTIFACT", "HELLO"]);
951
+ function validateEnvelope(value, line) {
952
+ const sample = line?.slice(0, 200);
953
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
954
+ return { ok: false, error: { code: "NOT_AN_OBJECT", message: "message must be a JSON object", ...sample ? { sample } : {} } };
955
+ }
956
+ const raw = value;
957
+ const protocol = raw.protocol;
958
+ if (protocol === void 0) {
959
+ const type = raw.type;
960
+ if (typeof type !== "string" || !KNOWN_TYPES.has(type)) {
961
+ return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(type)}`, ...sample ? { sample } : {} } };
962
+ }
963
+ return {
964
+ ok: true,
965
+ legacy: true,
966
+ envelope: {
967
+ protocol: "omninode-agent-protocol/1",
968
+ messageId: typeof raw.messageId === "string" ? raw.messageId : "legacy",
969
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
970
+ type,
971
+ payload: raw.payload
972
+ }
973
+ };
974
+ }
975
+ if (typeof protocol !== "string" || !isSupportedProtocol(protocol)) {
976
+ return {
977
+ ok: false,
978
+ error: { code: "UNSUPPORTED_VERSION", message: `unsupported protocol "${String(protocol)}"`, ...sample ? { sample } : {} }
979
+ };
980
+ }
981
+ if (raw.type === void 0) {
982
+ return { ok: false, error: { code: "MISSING_FIELD", message: "message is missing 'type'", ...sample ? { sample } : {} } };
983
+ }
984
+ if (typeof raw.type !== "string" || !KNOWN_TYPES.has(raw.type)) {
985
+ return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(raw.type)}`, ...sample ? { sample } : {} } };
986
+ }
987
+ if (raw.timestamp !== void 0 && typeof raw.timestamp !== "string") {
988
+ return { ok: false, error: { code: "MISSING_FIELD", message: "'timestamp' must be a string", ...sample ? { sample } : {} } };
989
+ }
990
+ if (PAYLOAD_REQUIRED_TYPES.has(raw.type) && (raw.payload === void 0 || raw.payload === null)) {
991
+ return { ok: false, error: { code: "INVALID_PAYLOAD", message: `${raw.type} requires a payload`, ...sample ? { sample } : {} } };
992
+ }
993
+ return {
994
+ ok: true,
995
+ legacy: protocol !== PROTOCOL_V2,
996
+ envelope: {
997
+ protocol,
998
+ messageId: typeof raw.messageId === "string" ? raw.messageId : "unknown",
999
+ ...typeof raw.requestId === "string" ? { requestId: raw.requestId } : {},
1000
+ ...typeof raw.taskId === "string" ? { taskId: raw.taskId } : {},
1001
+ ...typeof raw.agentId === "string" ? { agentId: raw.agentId } : {},
1002
+ ...typeof raw.pipelineId === "string" ? { pipelineId: raw.pipelineId } : {},
1003
+ ...typeof raw.executionId === "string" ? { executionId: raw.executionId } : {},
1004
+ timestamp: typeof raw.timestamp === "string" ? raw.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
1005
+ type: raw.type,
1006
+ payload: raw.payload,
1007
+ ...raw.metadata !== null && typeof raw.metadata === "object" ? { metadata: raw.metadata } : {}
1008
+ }
1009
+ };
1010
+ }
1011
+ function decodeMessage(line, maxBytes = MAX_MESSAGE_BYTES) {
1012
+ const trimmed = line.trim();
1013
+ if (trimmed.length === 0) {
1014
+ return { ok: false, error: { code: "INVALID_JSON", message: "empty line" } };
1015
+ }
1016
+ if (trimmed.length > maxBytes) {
1017
+ return {
1018
+ ok: false,
1019
+ error: { code: "OVERSIZED", message: `message of ${trimmed.length} bytes exceeds the ${maxBytes} byte limit`, sample: trimmed.slice(0, 200) }
1020
+ };
1021
+ }
1022
+ let value;
1023
+ try {
1024
+ value = JSON.parse(trimmed);
1025
+ } catch (error) {
1026
+ return {
1027
+ ok: false,
1028
+ error: { code: "INVALID_JSON", message: error instanceof Error ? error.message : "invalid JSON", sample: trimmed.slice(0, 200) }
1029
+ };
1030
+ }
1031
+ return validateEnvelope(value, trimmed);
1032
+ }
1033
+ function encodeMessage(envelope2, maxBytes = MAX_MESSAGE_BYTES) {
1034
+ const line = JSON.stringify(envelope2);
1035
+ if (line.length > maxBytes) {
1036
+ throw new Error(`outbound protocol message of ${line.length} bytes exceeds the ${maxBytes} byte limit`);
1037
+ }
1038
+ return line;
1039
+ }
1040
+ var ProtocolDecoder = class {
1041
+ buffer = "";
1042
+ maxBytes;
1043
+ constructor(maxBytes = MAX_MESSAGE_BYTES) {
1044
+ this.maxBytes = maxBytes;
1045
+ }
1046
+ push(chunk) {
1047
+ this.buffer += chunk;
1048
+ const results = [];
1049
+ let index = this.buffer.indexOf("\n");
1050
+ while (index >= 0) {
1051
+ const line = this.buffer.slice(0, index);
1052
+ this.buffer = this.buffer.slice(index + 1);
1053
+ if (line.trim().length > 0) results.push(decodeMessage(line, this.maxBytes));
1054
+ index = this.buffer.indexOf("\n");
1055
+ }
1056
+ if (this.buffer.length > this.maxBytes) {
1057
+ results.push({
1058
+ ok: false,
1059
+ error: { code: "OVERSIZED", message: `line exceeded ${this.maxBytes} bytes without a newline`, sample: this.buffer.slice(0, 200) }
1060
+ });
1061
+ this.buffer = "";
1062
+ }
1063
+ return results;
1064
+ }
1065
+ /** Flush any trailing line when the process exits. */
1066
+ flush() {
1067
+ if (this.buffer.trim().length === 0) {
1068
+ this.buffer = "";
1069
+ return [];
1070
+ }
1071
+ const result = decodeMessage(this.buffer, this.maxBytes);
1072
+ this.buffer = "";
1073
+ return [result];
1074
+ }
1075
+ };
1076
+
1077
+ // src/agent-protocol/handshake.ts
1078
+ function parseHello(envelopeValue) {
1079
+ if (envelopeValue.type !== "HELLO") {
1080
+ return { ok: false, reason: `expected HELLO, received ${envelopeValue.type}` };
1081
+ }
1082
+ const payload = envelopeValue.payload;
1083
+ if (payload === null || typeof payload !== "object") {
1084
+ return { ok: false, reason: "HELLO payload must be an object" };
1085
+ }
1086
+ const p = payload;
1087
+ if (typeof p.name !== "string" || p.name.length === 0) {
1088
+ return { ok: false, reason: "HELLO payload is missing 'name'" };
1089
+ }
1090
+ return {
1091
+ ok: true,
1092
+ descriptor: {
1093
+ protocol: envelopeValue.protocol,
1094
+ name: p.name,
1095
+ ...typeof p.version === "string" ? { version: p.version } : {},
1096
+ capabilities: Array.isArray(p.capabilities) ? p.capabilities.filter((c) => typeof c === "string") : [],
1097
+ supports: Array.isArray(p.supports) ? p.supports.filter((s) => typeof s === "string") : []
1098
+ }
1099
+ };
1100
+ }
1101
+ function buildHelloAck(descriptor) {
1102
+ return envelope("HELLO_ACK", {
1103
+ ok: true,
1104
+ protocol: PROTOCOL_V2,
1105
+ agent: descriptor.name,
1106
+ ...descriptor.version !== void 0 ? { agentVersion: descriptor.version } : {},
1107
+ supportedProtocols: [PROTOCOL_V2]
1108
+ }, { agentId: descriptor.name });
1109
+ }
1110
+
1111
+ // src/agent-protocol/session.ts
1112
+ var ProtocolSession = class {
1113
+ decoder;
1114
+ ids;
1115
+ responder;
1116
+ violations = [];
1117
+ questions = [];
1118
+ artifacts = [];
1119
+ reports = [];
1120
+ descriptor;
1121
+ completionSummary;
1122
+ errorMessage;
1123
+ constructor(ids, options = {}) {
1124
+ this.ids = ids;
1125
+ this.responder = options.responder;
1126
+ this.decoder = new ProtocolDecoder(options.maxBytes);
1127
+ }
1128
+ /** The v2 messages OmniNode sends to open a task (written to the agent's stdin). */
1129
+ taskMessages(objective, role, context, instruction) {
1130
+ const ids = this.ids;
1131
+ const messages = [envelope("TASK", { taskId: ids.taskId, objective }, ids)];
1132
+ if (role !== void 0) messages.push(envelope("CONTEXT", { role }, ids));
1133
+ if (context !== void 0 && context.length > 0) messages.push(envelope("CONTEXT", { body: context }, ids));
1134
+ if (instruction !== void 0 && instruction.length > 0) messages.push(envelope("INSTRUCTION", { text: instruction }, ids));
1135
+ return messages.map((m) => encodeMessage(m));
1136
+ }
1137
+ helloAck(descriptor) {
1138
+ return encodeMessage(buildHelloAck(descriptor));
1139
+ }
1140
+ /** Consumes decoded stdout, answering questions as they arrive. */
1141
+ async handle(decoded) {
1142
+ if (!decoded.ok) {
1143
+ this.violations.push(decoded.error);
1144
+ return;
1145
+ }
1146
+ const message = decoded.envelope;
1147
+ if (message.protocol === PROTOCOL_V2) this.sawV2 = true;
1148
+ if (message.type === "HELLO") {
1149
+ const hello = parseHello(message);
1150
+ if (hello.ok) this.descriptor = hello.descriptor;
1151
+ else this.violations.push({ code: "INVALID_PAYLOAD", message: `bad HELLO: ${hello.reason}` });
1152
+ return;
1153
+ }
1154
+ if (message.type === "QUESTION") {
1155
+ await this.answerQuestion(message);
1156
+ return;
1157
+ }
1158
+ if (message.type === "REPORT") {
1159
+ this.reports.push(message.payload);
1160
+ return;
1161
+ }
1162
+ if (message.type === "ARTIFACT") {
1163
+ this.artifacts.push(message.payload);
1164
+ return;
1165
+ }
1166
+ if (message.type === "COMPLETION") {
1167
+ const payload = message.payload;
1168
+ this.completionSummary = typeof payload?.summary === "string" ? payload.summary : "";
1169
+ return;
1170
+ }
1171
+ if (message.type === "ERROR") {
1172
+ const payload = message.payload;
1173
+ const detail = typeof payload?.message === "string" ? payload.message : "agent reported an error";
1174
+ const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
1175
+ this.errorMessage = `Agent error${code}: ${detail}`;
1176
+ return;
1177
+ }
1178
+ if (message.type === "TASK_ACCEPTED" || message.type === "TASK_STARTED" || message.type === "STATUS") {
1179
+ return;
1180
+ }
1181
+ this.violations.push({
1182
+ code: "UNKNOWN_TYPE",
1183
+ message: `unexpected message type ${message.type} from agent`
1184
+ });
1185
+ }
1186
+ async answerQuestion(message) {
1187
+ const payload = message.payload ?? {};
1188
+ const questionId = typeof payload.questionId === "string" ? payload.questionId : message.messageId;
1189
+ const question = typeof payload.question === "string" ? payload.question : "";
1190
+ const expected = typeof payload.expected === "string" ? payload.expected : "text";
1191
+ let answer;
1192
+ if (this.responder) {
1193
+ try {
1194
+ answer = await this.responder({ questionId, question, expected });
1195
+ } catch (error) {
1196
+ this.violations.push({
1197
+ code: "INVALID_PAYLOAD",
1198
+ message: `question responder failed: ${error instanceof Error ? error.message : String(error)}`
1199
+ });
1200
+ }
1201
+ }
1202
+ this.questions.push({ questionId, question, ...answer !== void 0 ? { answer } : {} });
1203
+ this.pendingAnswers.push(
1204
+ encodeMessage(
1205
+ envelope("RESPONSE", { questionId, ...answer !== void 0 ? { body: answer } : {} }, {
1206
+ ...this.ids,
1207
+ requestId: message.requestId ?? this.ids.requestId
1208
+ })
1209
+ )
1210
+ );
1211
+ }
1212
+ pendingAnswers = [];
1213
+ /** True once any inbound message carried the v2 protocol field. */
1214
+ sawV2 = false;
1215
+ };
1216
+
1217
+ // src/agents/process/index.ts
900
1218
  var DEFAULT_TIMEOUT_MS2 = 6e5;
901
1219
  var KILL_GRACE_MS = 5e3;
902
1220
  function buildChildEnv(config) {
@@ -930,18 +1248,19 @@ ${input.instruction}`);
930
1248
  return sections.join("\n\n");
931
1249
  }
932
1250
  function buildProtocolMessages(input, agentName) {
933
- const meta = {
1251
+ const session = new ProtocolSession({
1252
+ requestId: `req-${input.taskId}`,
934
1253
  taskId: input.taskId,
935
- agent: agentName,
936
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
937
- };
938
- const messages = [
939
- { type: "TASK", payload: { taskId: input.taskId, objective: input.objective } }
940
- ];
941
- if (input.role) messages.push({ type: "ROLE", payload: input.role });
942
- if (input.context) messages.push({ type: "CONTEXT", payload: { body: input.context } });
943
- if (input.instruction) messages.push({ type: "INSTRUCTION", payload: { text: input.instruction } });
944
- return messages.map((message) => JSON.stringify({ meta, message }));
1254
+ agentId: agentName,
1255
+ ...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
1256
+ ...input.executionId !== void 0 ? { executionId: input.executionId } : {}
1257
+ });
1258
+ return session.taskMessages(
1259
+ input.objective,
1260
+ input.role,
1261
+ input.context,
1262
+ input.instruction
1263
+ );
945
1264
  }
946
1265
  var ProcessAgent = class {
947
1266
  config;
@@ -1037,7 +1356,19 @@ var ProcessAgent = class {
1037
1356
  new AgentError(
1038
1357
  "AGENT_TIMEOUT",
1039
1358
  `Agent "${this.config.name}" timed out after ${timeoutMs}ms and was killed.`,
1040
- { details: { taskId: input.taskId, stdout: stdout.slice(0, 4e3), stderr: stderr.slice(0, 2e3) } }
1359
+ {
1360
+ details: {
1361
+ operation: "agent.run",
1362
+ agent: this.config.name,
1363
+ durationMs: timeoutMs,
1364
+ // The process was started and may have performed side
1365
+ // effects before being killed — the outcome is unknown,
1366
+ // not failed (roadmap §6.3).
1367
+ dispatched: true,
1368
+ stdout: stdout.slice(0, 4e3),
1369
+ stderr: stderr.slice(0, 2e3)
1370
+ }
1371
+ }
1041
1372
  )
1042
1373
  );
1043
1374
  return;
@@ -1082,62 +1413,75 @@ var ProcessAgent = class {
1082
1413
  return [...this.running.keys()];
1083
1414
  }
1084
1415
  };
1085
- function interpretProtocolRun({ input, agentName, stdout, exitCode, logs }) {
1086
- const reports = [];
1087
- let summary;
1088
- let error;
1089
- let sawProtocolMessage = false;
1090
- for (const line of stdout.split("\n")) {
1091
- const trimmed2 = line.trim();
1092
- if (trimmed2.length === 0) continue;
1093
- const message = parseAgentMessage(trimmed2);
1094
- if (!message) continue;
1095
- sawProtocolMessage = true;
1096
- if (message.type === "REPORT") {
1097
- const report = normalizeReport(message.payload, input.taskId, agentName, reports.length);
1098
- if (report) reports.push(report);
1099
- } else if (message.type === "COMPLETION") {
1100
- const payload = message.payload;
1101
- if (typeof payload?.summary === "string") summary = payload.summary;
1102
- } else if (message.type === "ERROR") {
1103
- const payload = message.payload;
1104
- const detail = typeof payload?.message === "string" ? payload.message : "unknown error";
1105
- const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
1106
- error = `Agent error${code}: ${detail}`;
1107
- }
1416
+ function interpretProtocolRun({
1417
+ input,
1418
+ agentName,
1419
+ stdout,
1420
+ exitCode,
1421
+ logs,
1422
+ responder
1423
+ }) {
1424
+ const session = new ProtocolSession(
1425
+ {
1426
+ requestId: `req-${input.taskId}`,
1427
+ taskId: input.taskId,
1428
+ agentId: agentName,
1429
+ ...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
1430
+ ...input.executionId !== void 0 ? { executionId: input.executionId } : {}
1431
+ },
1432
+ { ...responder !== void 0 ? { responder } : {} }
1433
+ );
1434
+ for (const result of session.decoder.push(stdout)) {
1435
+ session.handle(result);
1108
1436
  }
1109
- if (!sawProtocolMessage && exitCode === 0) {
1110
- throw new ProtocolError(
1111
- `Agent "${agentName}" produced no protocol messages on stdout (input mode "protocol").`,
1112
- { details: { taskId: input.taskId, stdout: stdout.slice(0, 2e3) } }
1113
- );
1437
+ for (const result of session.decoder.flush()) {
1438
+ session.handle(result);
1114
1439
  }
1440
+ 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);
1115
1441
  const trimmed = stdout.trim();
1442
+ const protocolBlock = {
1443
+ protocol: session.descriptor?.protocol ?? PROTOCOL_V2,
1444
+ legacy: !session.sawV2 && session.descriptor === void 0,
1445
+ ...session.descriptor !== void 0 ? { descriptor: session.descriptor } : {},
1446
+ questions: session.questions,
1447
+ violations: session.violations.map((v) => `${v.code}: ${v.message}`)
1448
+ };
1449
+ if (session.errorMessage !== void 0) {
1450
+ return {
1451
+ taskId: input.taskId,
1452
+ status: "failed",
1453
+ error: session.errorMessage,
1454
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
1455
+ exitCode: exitCode ?? void 0,
1456
+ logs,
1457
+ protocol: protocolBlock,
1458
+ ...reports.length > 0 ? { reports } : {}
1459
+ };
1460
+ }
1461
+ if (session.descriptor === void 0 && session.violations.length > 0 && session.reports.length === 0 && session.completionSummary === void 0) {
1462
+ return {
1463
+ taskId: input.taskId,
1464
+ status: "failed",
1465
+ error: `agent produced no valid protocol messages: ${protocolBlock.violations.join("; ")}`,
1466
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
1467
+ exitCode: exitCode ?? void 0,
1468
+ logs,
1469
+ protocol: protocolBlock
1470
+ };
1471
+ }
1472
+ const completed = exitCode === 0 && session.completionSummary !== void 0;
1116
1473
  return {
1117
1474
  taskId: input.taskId,
1118
- status: error !== void 0 || exitCode !== 0 ? "failed" : "completed",
1119
- summary: summary ?? reports[0]?.summary,
1475
+ status: completed || exitCode === 0 ? completed ? "completed" : "completed" : "failed",
1476
+ summary: session.completionSummary ?? reports[0]?.summary,
1120
1477
  rawOutput: trimmed.length > 0 ? trimmed : void 0,
1121
1478
  exitCode: exitCode ?? void 0,
1122
1479
  logs,
1123
- reports: reports.length > 0 ? reports : void 0,
1124
- error: error ?? (exitCode !== 0 ? `Process exited with code ${exitCode ?? "unknown"}` : void 0)
1480
+ protocol: protocolBlock,
1481
+ ...reports.length > 0 ? { reports } : {},
1482
+ ...exitCode !== 0 ? { error: `Process exited with code ${exitCode ?? "unknown"}` } : {}
1125
1483
  };
1126
1484
  }
1127
- function parseAgentMessage(line) {
1128
- try {
1129
- const parsed = JSON.parse(line);
1130
- if (typeof parsed.type === "string") {
1131
- return { type: parsed.type, payload: parsed.payload };
1132
- }
1133
- const inner = parsed.message;
1134
- if (inner && typeof inner.type === "string") {
1135
- return { type: inner.type, payload: inner.payload };
1136
- }
1137
- } catch {
1138
- }
1139
- return void 0;
1140
- }
1141
1485
  function normalizeReport(payload, taskId, agentName, index) {
1142
1486
  if (payload === null || typeof payload !== "object") return void 0;
1143
1487
  const raw = payload;
@@ -1397,12 +1741,18 @@ import { randomBytes } from "crypto";
1397
1741
  var ALLOWED_TRANSITIONS = {
1398
1742
  created: ["queued", "running", "cancelled"],
1399
1743
  queued: ["running", "cancelled"],
1400
- running: ["completed", "failed", "cancelled"],
1744
+ running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
1401
1745
  waiting: ["running", "cancelled"],
1402
1746
  completed: [],
1403
- failed: [],
1404
- cancelled: []
1747
+ // failed → running is the only re-entry: bounded automatic retry (§6.4).
1748
+ failed: ["running"],
1749
+ timed_out: [],
1750
+ cancelled: [],
1751
+ unknown: [],
1752
+ partially_completed: []
1405
1753
  };
1754
+ var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
1755
+ var EXECUTION_HISTORY_LIMIT = 10;
1406
1756
  var TaskEngine = class {
1407
1757
  constructor(options) {
1408
1758
  this.options = options;
@@ -1434,6 +1784,7 @@ var TaskEngine = class {
1434
1784
  id: generateTaskId(),
1435
1785
  objective: input.objective.trim(),
1436
1786
  status: "created",
1787
+ attempt: 0,
1437
1788
  createdAt: now,
1438
1789
  updatedAt: now,
1439
1790
  ...input.project !== void 0 ? { project: input.project } : {},
@@ -1450,8 +1801,10 @@ var TaskEngine = class {
1450
1801
  const task = await this.mustGet(id);
1451
1802
  return this.transition(task, "queued");
1452
1803
  }
1453
- /** Runs the task through its assigned agent and records the result. */
1454
- async run(id) {
1804
+ /** Runs the task through its assigned agent and records the result.
1805
+ * `maxAttempts` enables bounded automatic retries of *retryable* errors
1806
+ * with linear backoff; unknown/timed_out outcomes are never auto-retried. */
1807
+ async run(id, options = {}) {
1455
1808
  const task = await this.mustGet(id);
1456
1809
  if (task.status !== "created" && task.status !== "queued") {
1457
1810
  throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
@@ -1464,49 +1817,84 @@ var TaskEngine = class {
1464
1817
  throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
1465
1818
  }
1466
1819
  const role = task.role ? this.options.roles.get(task.role) : void 0;
1467
- const running = await this.transition(task, "running");
1468
1820
  this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
1469
- let background = running.context?.background;
1821
+ let background = task.context?.background;
1470
1822
  if (this.options.memory) {
1471
1823
  const memoryContext = await this.options.memory.gatherContext({
1472
- objective: running.objective,
1473
- ...running.role ? { role: running.role } : {},
1474
- ...running.project ? { project: running.project } : {}
1824
+ objective: task.objective,
1825
+ ...task.role ? { role: task.role } : {},
1826
+ ...task.project ? { project: task.project } : {}
1475
1827
  });
1476
1828
  if (memoryContext.length > 0) {
1477
1829
  background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
1478
1830
  }
1479
1831
  }
1480
- let output;
1481
- try {
1482
- output = await agent.run({
1483
- taskId: running.id,
1484
- objective: running.objective,
1485
- ...role ? { role } : {},
1486
- ...background !== void 0 ? { context: background } : {}
1487
- });
1488
- } catch (error) {
1489
- const message = error instanceof Error ? error.message : String(error);
1490
- this.log.error(`Task ${task.id} failed: ${message}`);
1491
- return this.transition(running, "failed", {
1492
- result: { error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }
1832
+ const maxAttempts = Math.max(1, options.maxAttempts ?? 1);
1833
+ const backoffMs = options.retryBackoffMs ?? 0;
1834
+ let current = task;
1835
+ for (let i = 0; i < maxAttempts; i += 1) {
1836
+ const attempt = current.attempt + 1;
1837
+ const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1838
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1839
+ current = await this.transition(current, "running", { attempt });
1840
+ let output;
1841
+ let thrown;
1842
+ try {
1843
+ output = await agent.run({
1844
+ taskId: current.id,
1845
+ objective: current.objective,
1846
+ ...role ? { role } : {},
1847
+ ...background !== void 0 ? { context: background } : {}
1848
+ });
1849
+ } catch (error) {
1850
+ thrown = error;
1851
+ }
1852
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
1853
+ if (thrown !== void 0 || output === void 0) {
1854
+ const outcome = classifyThrown(thrown);
1855
+ const message = thrown instanceof Error ? thrown.message : String(thrown);
1856
+ const retryable = outcome === "failed" && RETRYABLE_ERROR_CODES.includes(errorCode(thrown)) && i < maxAttempts - 1;
1857
+ current = await this.transition(current, outcome, {
1858
+ attempt,
1859
+ lastError: message,
1860
+ execution: {
1861
+ executionId,
1862
+ attempt,
1863
+ startedAt,
1864
+ finishedAt,
1865
+ outcome,
1866
+ error: message
1867
+ }
1868
+ });
1869
+ this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
1870
+ if (retryable) {
1871
+ if (backoffMs > 0) await sleep(backoffMs * attempt);
1872
+ continue;
1873
+ }
1874
+ if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
1875
+ return current;
1876
+ }
1877
+ const result = {
1878
+ ...output.summary !== void 0 ? { summary: output.summary } : {},
1879
+ ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
1880
+ // Keep the verbatim output (bounded) for report extraction and audit.
1881
+ ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
1882
+ ...output.error !== void 0 ? { error: output.error } : {},
1883
+ finishedAt
1884
+ };
1885
+ const finalStatus = output.status === "completed" ? "completed" : "failed";
1886
+ current = await this.transition(current, finalStatus, {
1887
+ attempt,
1888
+ result,
1889
+ execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
1493
1890
  });
1891
+ this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
1892
+ break;
1494
1893
  }
1495
- const result = {
1496
- ...output.summary !== void 0 ? { summary: output.summary } : {},
1497
- ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
1498
- // Keep the verbatim output (bounded) for report extraction and audit.
1499
- ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
1500
- ...output.error !== void 0 ? { error: output.error } : {},
1501
- finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1502
- };
1503
- const finalStatus = output.status === "completed" ? "completed" : "failed";
1504
- const finished = await this.transition(running, finalStatus, { result });
1505
- this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
1506
1894
  if (this.options.memory) {
1507
- await this.options.memory.recordTaskOutcome(finished);
1895
+ await this.options.memory.recordTaskOutcome(current);
1508
1896
  }
1509
- return finished;
1897
+ return current;
1510
1898
  }
1511
1899
  async cancel(id) {
1512
1900
  const task = await this.mustGet(id);
@@ -1519,13 +1907,14 @@ var TaskEngine = class {
1519
1907
  }
1520
1908
  return this.transition(task, "cancelled");
1521
1909
  }
1522
- /** Error recovery (§27 Phase 9): reset a failed/cancelled task so it can run again. */
1910
+ /** Error recovery: reset a failed/cancelled/unknown/timed_out task so it can run again. */
1523
1911
  async retry(id) {
1524
1912
  const task = await this.mustGet(id);
1525
- if (task.status !== "failed" && task.status !== "cancelled") {
1913
+ const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
1914
+ if (!retryableStates.includes(task.status)) {
1526
1915
  throw new TaskError(
1527
1916
  "TASK_INVALID",
1528
- `Only failed or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
1917
+ `Only failed, timed out, unknown or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
1529
1918
  );
1530
1919
  }
1531
1920
  const { result: _discarded, ...rest } = task;
@@ -1557,10 +1946,14 @@ var TaskEngine = class {
1557
1946
  if (!allowed.includes(to)) {
1558
1947
  throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
1559
1948
  }
1949
+ const executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
1560
1950
  const updated = {
1561
1951
  ...task,
1562
1952
  status: to,
1563
1953
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1954
+ ...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
1955
+ ...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
1956
+ ...executions !== void 0 ? { executions } : {},
1564
1957
  ...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
1565
1958
  };
1566
1959
  await this.options.store.save(updated);
@@ -1581,6 +1974,19 @@ var TaskEngine = class {
1581
1974
  function generateTaskId() {
1582
1975
  return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1583
1976
  }
1977
+ function errorCode(error) {
1978
+ return error?.code ?? "";
1979
+ }
1980
+ function classifyThrown(error) {
1981
+ if (error instanceof AgentError && error.code === "AGENT_TIMEOUT") {
1982
+ const dispatched = error.details?.dispatched;
1983
+ return dispatched === false ? "timed_out" : "unknown";
1984
+ }
1985
+ return "failed";
1986
+ }
1987
+ function sleep(ms) {
1988
+ return new Promise((resolve) => setTimeout(resolve, ms));
1989
+ }
1584
1990
 
1585
1991
  // src/audit/index.ts
1586
1992
  import { appendFile, mkdir as mkdir2 } from "fs/promises";
@@ -1980,7 +2386,7 @@ function registerTaskCommands(program) {
1980
2386
  const cancelled = await engine.cancel(taskId);
1981
2387
  console.log(`Task ${cancelled.id} is now "${cancelled.status}".`);
1982
2388
  });
1983
- task.command("retry <task-id>").description("Reset a failed/cancelled task to created; combine with --run to execute it again.").option("--run", "Run the task again immediately after resetting it.").action(async (taskId, options) => {
2389
+ task.command("retry <task-id>").description("Reset a failed, timed out, unknown or cancelled task to created; combine with --run to execute it again.").option("--run", "Run the task again immediately after resetting it.").action(async (taskId, options) => {
1984
2390
  const config = loadConfig();
1985
2391
  const engine = createTaskEngine(config);
1986
2392
  const reset = await engine.retry(taskId);
@@ -2003,6 +2409,15 @@ function printOutcome(task, options = {}) {
2003
2409
  if (task.context?.files) console.log(` files: ${task.context.files.join(", ")}`);
2004
2410
  console.log(` objective: ${task.objective}`);
2005
2411
  }
2412
+ if (options.verbose && task.attempt > 0) {
2413
+ console.log(` attempt: ${task.attempt}`);
2414
+ }
2415
+ if (task.lastError) console.log(` error: ${task.lastError}`);
2416
+ if (options.verbose && task.executions && task.executions.length > 0) {
2417
+ console.log(
2418
+ ` executions: ${task.executions.map((e) => `#${e.attempt}:${e.outcome}`).join(", ")}`
2419
+ );
2420
+ }
2006
2421
  const result = task.result;
2007
2422
  if (!result) return;
2008
2423
  if (result.summary) console.log(` summary: ${result.summary}`);
@@ -2743,7 +3158,7 @@ ${contribution}`);
2743
3158
  }
2744
3159
  const stepRun = {
2745
3160
  stepId: step.id,
2746
- status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
3161
+ status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
2747
3162
  ...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
2748
3163
  ...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
2749
3164
  ...outcome.error !== void 0 ? { error: outcome.error } : {}
@@ -2754,8 +3169,10 @@ ${contribution}`);
2754
3169
  run2.reports = [...allReports];
2755
3170
  await this.options.store.save(run2);
2756
3171
  }
2757
- const failed = [...outcomes.values()].some((o) => o.status === "failed");
2758
- const finalStatus = failed ? "failed" : "completed";
3172
+ const allOutcomes = [...outcomes.values()];
3173
+ const failed = allOutcomes.some((o) => o.status === "failed");
3174
+ const partial = !failed && allOutcomes.some((o) => o.status === "partial");
3175
+ const finalStatus = failed ? "failed" : partial ? "partial" : "completed";
2759
3176
  const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
2760
3177
  let resultSummary;
2761
3178
  for (let i = def.steps.length - 1; i >= 0; i -= 1) {
@@ -2861,6 +3278,7 @@ ${contribution}`);
2861
3278
  )
2862
3279
  );
2863
3280
  const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
3281
+ const completedTasks = results.filter((r) => r.status === "completed");
2864
3282
  const failedTasks = results.filter((r) => r.status !== "completed");
2865
3283
  const lines = results.map((result, index) => {
2866
3284
  const agentName = agentNames[index] ?? "unknown";
@@ -2870,12 +3288,15 @@ ${contribution}`);
2870
3288
  const reports = results.flatMap((r) => r.result?.reports ?? []);
2871
3289
  const summary = lines.join("\n");
2872
3290
  if (failedTasks.length > 0) {
2873
- const firstError = failedTasks[0]?.result?.error ?? "unknown error";
3291
+ const firstError = failedTasks[0]?.result?.error ?? failedTasks[0]?.lastError ?? "unknown outcome";
3292
+ const states = failedTasks.map((r) => r.status).join(", ");
2874
3293
  return {
2875
- status: "failed",
3294
+ // Some agents succeeded → the step produced usable but incomplete
3295
+ // intelligence: partial, not failed (roadmap §9.3).
3296
+ status: completedTasks.length > 0 ? "partial" : "failed",
2876
3297
  summary,
2877
3298
  taskIds: created.map((t) => t.id),
2878
- error: `${failedTasks.length}/${results.length} task(s) failed \u2014 ${firstError}`,
3299
+ error: `${failedTasks.length}/${results.length} task(s) incomplete (${states}) \u2014 ${firstError}`,
2879
3300
  reports
2880
3301
  };
2881
3302
  }
@@ -2891,7 +3312,9 @@ ${contribution}`);
2891
3312
  const result = await this.options.tasks.run(task.id);
2892
3313
  if (result.status !== "completed") {
2893
3314
  return {
2894
- status: "failed",
3315
+ // Unknown/timed_out outcomes are not failures — the work may have
3316
+ // happened remotely; the step is incomplete, not broken.
3317
+ status: result.status === "unknown" || result.status === "timed_out" ? "partial" : "failed",
2895
3318
  summary: result.result?.summary ?? "",
2896
3319
  taskIds: [task.id],
2897
3320
  error: result.result?.error ?? `task ${task.id} did not complete`,