@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/README.md +2 -2
- package/dist/cli/index.js +534 -111
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +291 -49
- package/dist/index.js +566 -106
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { realpathSync } from "fs";
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/version.ts
|
|
9
|
-
var OMNINODE_VERSION = "2.0.0-alpha.
|
|
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
|
|
1251
|
+
const session = new ProtocolSession({
|
|
1252
|
+
requestId: `req-${input.taskId}`,
|
|
934
1253
|
taskId: input.taskId,
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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
|
-
{
|
|
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({
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
-
|
|
1110
|
-
|
|
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:
|
|
1119
|
-
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
|
-
|
|
1124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
1821
|
+
let background = task.context?.background;
|
|
1470
1822
|
if (this.options.memory) {
|
|
1471
1823
|
const memoryContext = await this.options.memory.gatherContext({
|
|
1472
|
-
objective:
|
|
1473
|
-
...
|
|
1474
|
-
...
|
|
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
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
});
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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(
|
|
1895
|
+
await this.options.memory.recordTaskOutcome(current);
|
|
1508
1896
|
}
|
|
1509
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
2758
|
-
const
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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`,
|