@multiclawcli/core 0.5.1 → 0.6.1
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.d.ts +2 -0
- package/dist/index.js +1192 -279
- package/dist/orchestrator/AgentRunner.d.ts +4 -2
- package/dist/orchestrator/AgentSessionManager.d.ts +40 -0
- package/dist/orchestrator/Orchestrator.d.ts +5 -1
- package/dist/providers/detect.d.ts +13 -0
- package/dist/runtime/AcpRuntime.d.ts +4 -10
- package/dist/types/config.d.ts +35 -0
- package/dist/types/leader.d.ts +2 -0
- package/dist/types/runtime.d.ts +112 -1
- package/dist/utils/timeout.d.ts +3 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -191,6 +191,7 @@ Responsibilities:
|
|
|
191
191
|
|
|
192
192
|
Deliverables (save to the working directory):
|
|
193
193
|
- code-review.md \u2014 score (1\u201310), blocking issues, advisory issues, conclusion (Approved / Changes Required)
|
|
194
|
+
- When the task includes a Delivery acceptance review contract, the final response must be only its JSON verdict, not the prose report. Approve only with no unresolved blocking issues; include file locations and fix directions in changes_required issues. The verdict is a review judgment, not proof that unexecuted tests passed.
|
|
194
195
|
|
|
195
196
|
Principles: Judge the code, not the developer. A blocking issue must include the file path, the specific problem, and a fix direction. Advisory items should be actionable, not vague. If the code is solid, say so.`;
|
|
196
197
|
var TASK4 = `## Project
|
|
@@ -358,12 +359,12 @@ There are three outcomes, and the first one matters most:
|
|
|
358
359
|
|
|
359
360
|
Do not apply fixed rules \u2014 use your judgment based on the actual requirement and the code. A minor bug fix may need only a developer and code-reviewer; a complex new feature may need the full pipeline. Read the code to understand the impact before deciding.
|
|
360
361
|
|
|
361
|
-
The console hands you the turns that came before this one. A requirement is often only interpretable next to them: "Postgres" is an answer, not a task, and the turn list is what tells you which question it answers. Read it as your own conversation \u2014 "you asked" is you. If you asked and got an answer, decide now; do not ask the same question again.
|
|
362
|
+
The console hands you the turns that came before this one. A requirement is often only interpretable next to them: "Postgres" is an answer, not a task, and the turn list is what tells you which question it answers. Read it as your own conversation \u2014 "you asked" is you. If you asked and got an answer, decide now; do not ask the same question again. For every "run" decision, include a complete, standalone "requirement" that combines the original task goal with all clarification answers and constraints. Never pass only the latest short answer to the team.
|
|
362
363
|
|
|
363
364
|
Output ONLY a single JSON object on its own line \u2014 no prose, no markdown fences:
|
|
364
365
|
{"mode": "reply", "message": "your answer"}
|
|
365
366
|
{"mode": "ask", "message": "your question"}
|
|
366
|
-
{"mode": "run", "reason": "one sentence on why this set", "run": ["agent-id-1", "agent-id-2"]}`;
|
|
367
|
+
{"mode": "run", "reason": "one sentence on why this set", "requirement": "the complete task and constraints", "run": ["agent-id-1", "agent-id-2"]}`;
|
|
367
368
|
var TASK7 = `## Project
|
|
368
369
|
{{projectName}}
|
|
369
370
|
|
|
@@ -378,7 +379,7 @@ Read the codebase to understand the current state, then decide how to handle the
|
|
|
378
379
|
Output a single JSON object \u2014 exactly one of:
|
|
379
380
|
{"mode": "reply", "message": "your answer"}
|
|
380
381
|
{"mode": "ask", "message": "your question"}
|
|
381
|
-
{"mode": "run", "reason": "one sentence on why this set", "run": ["agent-id", ...]}
|
|
382
|
+
{"mode": "run", "reason": "one sentence on why this set", "requirement": "the complete task and constraints", "run": ["agent-id", ...]}
|
|
382
383
|
Agent IDs must come from the Available Agents list above.`;
|
|
383
384
|
function leader(overrides) {
|
|
384
385
|
return {
|
|
@@ -692,6 +693,215 @@ var EventBus = class {
|
|
|
692
693
|
}
|
|
693
694
|
};
|
|
694
695
|
|
|
696
|
+
// src/utils/abort.ts
|
|
697
|
+
var ABORT_ERROR_NAME = "AbortError";
|
|
698
|
+
function abortError(message = "Operation aborted") {
|
|
699
|
+
const err = new Error(message);
|
|
700
|
+
err.name = ABORT_ERROR_NAME;
|
|
701
|
+
return err;
|
|
702
|
+
}
|
|
703
|
+
function isAbortError(err) {
|
|
704
|
+
return err instanceof Error && err.name === ABORT_ERROR_NAME;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/orchestrator/AgentSessionManager.ts
|
|
708
|
+
var AgentSessionManager = class {
|
|
709
|
+
constructor() {
|
|
710
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
711
|
+
this.closed = false;
|
|
712
|
+
}
|
|
713
|
+
async execute(runtime, task) {
|
|
714
|
+
if (this.closed) throw new Error("AgentSessionManager is closed");
|
|
715
|
+
if (this.resetting) throw new Error("AgentSessionManager is resetting");
|
|
716
|
+
if (task.signal?.aborted) throw abortError();
|
|
717
|
+
let entry3 = this.entries.get(task.agentId);
|
|
718
|
+
if (entry3?.busy || entry3?.session && entry3.session.state !== "idle" && entry3.session.state !== "closed") {
|
|
719
|
+
throw new Error(`Agent session ${task.agentId} is busy`);
|
|
720
|
+
}
|
|
721
|
+
if (entry3?.closing) await entry3.closing;
|
|
722
|
+
const fingerprint = JSON.stringify([
|
|
723
|
+
task.command,
|
|
724
|
+
task.args,
|
|
725
|
+
task.model,
|
|
726
|
+
task.workDir,
|
|
727
|
+
task.systemPrompt,
|
|
728
|
+
task.tools,
|
|
729
|
+
task.sessionId,
|
|
730
|
+
task.resume,
|
|
731
|
+
Boolean(task.onQuestion)
|
|
732
|
+
]);
|
|
733
|
+
if (entry3 && (entry3.runtime !== runtime || entry3.fingerprint !== fingerprint || entry3.session?.state === "closed" || !runtime.openSession)) {
|
|
734
|
+
entry3.busy = true;
|
|
735
|
+
try {
|
|
736
|
+
await this.closeEntry(entry3);
|
|
737
|
+
this.entries.delete(task.agentId);
|
|
738
|
+
} finally {
|
|
739
|
+
entry3.busy = false;
|
|
740
|
+
}
|
|
741
|
+
entry3 = void 0;
|
|
742
|
+
if (this.closed || this.resetting) throw abortError();
|
|
743
|
+
}
|
|
744
|
+
if (task.signal?.aborted) throw abortError();
|
|
745
|
+
const openSession = runtime.openSession;
|
|
746
|
+
if (!openSession) return runtime.execute(task);
|
|
747
|
+
let detach;
|
|
748
|
+
if (!entry3) {
|
|
749
|
+
const startup = new AbortController();
|
|
750
|
+
const forwardAbort = () => startup.abort(task.signal?.reason);
|
|
751
|
+
task.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
752
|
+
detach = () => task.signal?.removeEventListener("abort", forwardAbort);
|
|
753
|
+
const created = {
|
|
754
|
+
runtime,
|
|
755
|
+
fingerprint,
|
|
756
|
+
startup,
|
|
757
|
+
busy: true,
|
|
758
|
+
onWarning: task.onWarning,
|
|
759
|
+
// Defer opening until the cache entry is installed, including synchronous failures.
|
|
760
|
+
opening: Promise.resolve().then(
|
|
761
|
+
() => openSession.call(runtime, {
|
|
762
|
+
agentId: task.agentId,
|
|
763
|
+
systemPrompt: task.systemPrompt,
|
|
764
|
+
tools: task.tools,
|
|
765
|
+
workDir: task.workDir,
|
|
766
|
+
command: task.command,
|
|
767
|
+
args: task.args,
|
|
768
|
+
model: task.model,
|
|
769
|
+
sessionId: task.sessionId,
|
|
770
|
+
resume: task.resume,
|
|
771
|
+
timeout: task.timeout,
|
|
772
|
+
questionForms: Boolean(task.onQuestion),
|
|
773
|
+
signal: startup.signal,
|
|
774
|
+
onWarning: (message) => created.onWarning?.(message)
|
|
775
|
+
})
|
|
776
|
+
).then((session) => {
|
|
777
|
+
created.session = session;
|
|
778
|
+
return session;
|
|
779
|
+
})
|
|
780
|
+
};
|
|
781
|
+
entry3 = created;
|
|
782
|
+
this.entries.set(task.agentId, entry3);
|
|
783
|
+
} else {
|
|
784
|
+
entry3.busy = true;
|
|
785
|
+
entry3.onWarning = task.onWarning;
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
const session = await entry3.opening;
|
|
789
|
+
detach?.();
|
|
790
|
+
detach = void 0;
|
|
791
|
+
if (entry3.startup.signal.aborted || this.closed || this.resetting) {
|
|
792
|
+
await this.closeEntry(entry3);
|
|
793
|
+
throw abortError();
|
|
794
|
+
}
|
|
795
|
+
if (session.state !== "idle") throw new Error(`Agent session ${task.agentId} is busy`);
|
|
796
|
+
return await session.prompt(task.prompt, {
|
|
797
|
+
signal: task.signal,
|
|
798
|
+
timeout: task.timeout,
|
|
799
|
+
onEvent: task.onEvent,
|
|
800
|
+
permissionMode: task.permissionMode,
|
|
801
|
+
onPermission: task.onPermission,
|
|
802
|
+
onQuestion: task.onQuestion
|
|
803
|
+
});
|
|
804
|
+
} finally {
|
|
805
|
+
detach?.();
|
|
806
|
+
entry3.onWarning = void 0;
|
|
807
|
+
try {
|
|
808
|
+
if (entry3.session?.state === "closed") {
|
|
809
|
+
await this.closeEntry(entry3);
|
|
810
|
+
if (this.entries.get(task.agentId) === entry3) this.entries.delete(task.agentId);
|
|
811
|
+
} else if (!entry3.session) {
|
|
812
|
+
if (this.entries.get(task.agentId) === entry3) this.entries.delete(task.agentId);
|
|
813
|
+
}
|
|
814
|
+
} finally {
|
|
815
|
+
entry3.busy = false;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Deliver a message into every session that is running a turn right now.
|
|
821
|
+
*
|
|
822
|
+
* Only `running` sessions are candidates, and that is the whole safety
|
|
823
|
+
* argument: a session between turns has nothing to inject into, and asking a
|
|
824
|
+
* provider to steer an idle session makes it *start* a turn that this manager
|
|
825
|
+
* never scheduled — one whose output belongs to no turn and which nothing
|
|
826
|
+
* would ever settle. Sessions that are idle are therefore not reported as
|
|
827
|
+
* failures but simply absent, and the caller sees an empty list.
|
|
828
|
+
*
|
|
829
|
+
* One agent failing never stops the others: each report stands alone, because
|
|
830
|
+
* the caller has to be able to say "architect took it, qa did not" rather than
|
|
831
|
+
* "steering failed".
|
|
832
|
+
*/
|
|
833
|
+
async supplement(text) {
|
|
834
|
+
if (this.closed) return [];
|
|
835
|
+
const targets = [...this.entries.entries()].filter(
|
|
836
|
+
([, entry3]) => entry3.session?.state === "running"
|
|
837
|
+
);
|
|
838
|
+
return await Promise.all(
|
|
839
|
+
targets.map(async ([agentId, entry3]) => {
|
|
840
|
+
const session = entry3.session;
|
|
841
|
+
if (!session) return { agentId, status: "idle" };
|
|
842
|
+
if (!session.capabilities.supplement || !session.supplement) {
|
|
843
|
+
return { agentId, status: "unsupported" };
|
|
844
|
+
}
|
|
845
|
+
try {
|
|
846
|
+
return { agentId, status: (await session.supplement(text)).status };
|
|
847
|
+
} catch (error) {
|
|
848
|
+
return {
|
|
849
|
+
agentId,
|
|
850
|
+
status: "error",
|
|
851
|
+
error: error instanceof Error ? error.message : String(error)
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
})
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
/** Closes all sessions and permits a fresh conversation after cleanup completes. */
|
|
858
|
+
reset() {
|
|
859
|
+
if (this.closing) return this.closing;
|
|
860
|
+
if (this.resetting) return this.resetting;
|
|
861
|
+
this.resetting = this.drain().finally(() => {
|
|
862
|
+
this.resetting = void 0;
|
|
863
|
+
});
|
|
864
|
+
return this.resetting;
|
|
865
|
+
}
|
|
866
|
+
/** Terminal, idempotent cleanup barrier. No new executions may start. */
|
|
867
|
+
close() {
|
|
868
|
+
if (this.closing) return this.closing;
|
|
869
|
+
this.closed = true;
|
|
870
|
+
this.closing = this.resetting ?? this.drain();
|
|
871
|
+
return this.closing;
|
|
872
|
+
}
|
|
873
|
+
async drain() {
|
|
874
|
+
const entries = [...this.entries.entries()];
|
|
875
|
+
for (const [, entry3] of entries) entry3.startup.abort();
|
|
876
|
+
const results = await Promise.allSettled(
|
|
877
|
+
entries.map(async ([id, entry3]) => {
|
|
878
|
+
await this.closeEntry(entry3);
|
|
879
|
+
if (this.entries.get(id) === entry3) this.entries.delete(id);
|
|
880
|
+
})
|
|
881
|
+
);
|
|
882
|
+
const errors = results.flatMap(
|
|
883
|
+
(result) => result.status === "rejected" ? [result.reason] : []
|
|
884
|
+
);
|
|
885
|
+
if (errors.length === 1) throw errors[0];
|
|
886
|
+
if (errors.length > 1) throw new AggregateError(errors, "Agent session cleanup failed");
|
|
887
|
+
}
|
|
888
|
+
closeEntry(entry3) {
|
|
889
|
+
if (!entry3.closing) {
|
|
890
|
+
entry3.closing = (async () => {
|
|
891
|
+
let session;
|
|
892
|
+
try {
|
|
893
|
+
session = await entry3.opening;
|
|
894
|
+
} catch (error) {
|
|
895
|
+
if (isAbortError(error)) return;
|
|
896
|
+
throw error;
|
|
897
|
+
}
|
|
898
|
+
await session.close();
|
|
899
|
+
})();
|
|
900
|
+
}
|
|
901
|
+
return entry3.closing;
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
|
|
695
905
|
// src/orchestrator/Orchestrator.ts
|
|
696
906
|
import * as fs4 from "fs";
|
|
697
907
|
import * as path5 from "path";
|
|
@@ -967,7 +1177,14 @@ function asDecision(value) {
|
|
|
967
1177
|
const run = record.run;
|
|
968
1178
|
if (!Array.isArray(run) || !run.every((id) => typeof id === "string")) return void 0;
|
|
969
1179
|
const reason = typeof record.reason === "string" ? record.reason.trim() : "";
|
|
970
|
-
|
|
1180
|
+
const requirement = typeof record.requirement === "string" ? record.requirement.trim() : "";
|
|
1181
|
+
if ("requirement" in record && !requirement) return void 0;
|
|
1182
|
+
return {
|
|
1183
|
+
mode: "run",
|
|
1184
|
+
run,
|
|
1185
|
+
...reason ? { reason } : {},
|
|
1186
|
+
...requirement ? { requirement } : {}
|
|
1187
|
+
};
|
|
971
1188
|
}
|
|
972
1189
|
return void 0;
|
|
973
1190
|
}
|
|
@@ -1064,17 +1281,6 @@ function resolveExecutable(command) {
|
|
|
1064
1281
|
return resolveBridgeBinary(command);
|
|
1065
1282
|
}
|
|
1066
1283
|
|
|
1067
|
-
// src/utils/abort.ts
|
|
1068
|
-
var ABORT_ERROR_NAME = "AbortError";
|
|
1069
|
-
function abortError(message = "Operation aborted") {
|
|
1070
|
-
const err = new Error(message);
|
|
1071
|
-
err.name = ABORT_ERROR_NAME;
|
|
1072
|
-
return err;
|
|
1073
|
-
}
|
|
1074
|
-
function isAbortError(err) {
|
|
1075
|
-
return err instanceof Error && err.name === ABORT_ERROR_NAME;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
1284
|
// src/utils/errors.ts
|
|
1079
1285
|
var PermanentError = class extends Error {
|
|
1080
1286
|
constructor(message) {
|
|
@@ -1094,8 +1300,16 @@ var RuntimeBase = class {
|
|
|
1094
1300
|
var discoveredModels = /* @__PURE__ */ new Map();
|
|
1095
1301
|
var KILL_ESCALATION_MS = 3e3;
|
|
1096
1302
|
var CANCEL_GRACE_MS = 2e3;
|
|
1303
|
+
var CLEANUP_ACK_MS = 3e3;
|
|
1304
|
+
var STEER_ACK_MS = 15e3;
|
|
1305
|
+
var DEFAULT_TIMEOUT_MS = 6e5;
|
|
1097
1306
|
var DEFAULT_COMMAND2 = "opencode";
|
|
1098
1307
|
var DEFAULT_ARGS = ["acp"];
|
|
1308
|
+
var STEER_METHOD = "_session/steering";
|
|
1309
|
+
function advertisesSteering(init) {
|
|
1310
|
+
const steering = init._meta?.steering;
|
|
1311
|
+
return isRecord(steering) && steering.supported === true;
|
|
1312
|
+
}
|
|
1099
1313
|
var BASE_CAPABILITIES = {
|
|
1100
1314
|
streaming: true,
|
|
1101
1315
|
sessions: true,
|
|
@@ -1142,15 +1356,23 @@ var KIND_TO_TOOLS = {
|
|
|
1142
1356
|
search: ["Grep", "Glob"],
|
|
1143
1357
|
fetch: ["WebFetch"]
|
|
1144
1358
|
};
|
|
1359
|
+
function toolPermitted(params, tools) {
|
|
1360
|
+
const allowed = new Set(tools.map((tool) => tool.toLowerCase()));
|
|
1361
|
+
const name = params.toolCall.name ?? void 0;
|
|
1362
|
+
return name !== void 0 ? allowed.has(name.toLowerCase()) : (KIND_TO_TOOLS[params.toolCall.kind ?? ""] ?? []).some(
|
|
1363
|
+
(tool) => allowed.has(tool.toLowerCase())
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1145
1366
|
function decidePermission(params, tools, sessionId) {
|
|
1146
1367
|
if (sessionId !== void 0 && params.sessionId !== sessionId) {
|
|
1147
1368
|
return { response: { outcome: { outcome: "cancelled" } } };
|
|
1148
1369
|
}
|
|
1149
|
-
const allowed = new Set(tools.map((tool) => tool.toLowerCase()));
|
|
1150
1370
|
const name = params.toolCall.name ?? void 0;
|
|
1151
1371
|
const kind = params.toolCall.kind ?? void 0;
|
|
1152
|
-
const approved =
|
|
1153
|
-
const option =
|
|
1372
|
+
const approved = toolPermitted(params, tools);
|
|
1373
|
+
const option = params.options.find(
|
|
1374
|
+
(o) => approved ? o.kind === "allow_once" : o.kind === "reject_once"
|
|
1375
|
+
);
|
|
1154
1376
|
const response = option ? { outcome: { outcome: "selected", optionId: option.optionId } } : { outcome: { outcome: "cancelled" } };
|
|
1155
1377
|
if (approved) return { response };
|
|
1156
1378
|
return {
|
|
@@ -1163,6 +1385,68 @@ function decidePermission(params, tools, sessionId) {
|
|
|
1163
1385
|
}
|
|
1164
1386
|
};
|
|
1165
1387
|
}
|
|
1388
|
+
var FORM_ANNOTATIONS = ["title", "description", "default", "_meta"];
|
|
1389
|
+
function knownFormKeys(value, keys) {
|
|
1390
|
+
return Object.keys(value).every((key) => keys.includes(key) || FORM_ANNOTATIONS.includes(key));
|
|
1391
|
+
}
|
|
1392
|
+
function validFormBound(value, integer = false) {
|
|
1393
|
+
return value == null || typeof value === "number" && Number.isFinite(value) && (!integer || Number.isInteger(value) && value >= 0);
|
|
1394
|
+
}
|
|
1395
|
+
function titledChoices(value) {
|
|
1396
|
+
return Array.isArray(value) && value.length > 0 && value.every(
|
|
1397
|
+
(option) => isRecord(option) && knownFormKeys(option, ["const"]) && typeof option.const === "string"
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
function supportedQuestionSchema(schema) {
|
|
1401
|
+
if (!isRecord(schema) || schema.type !== void 0 && schema.type !== "object" || !knownFormKeys(schema, ["type", "properties", "required"]))
|
|
1402
|
+
return false;
|
|
1403
|
+
const properties = schema.properties ?? {};
|
|
1404
|
+
if (!isRecord(properties)) return false;
|
|
1405
|
+
if (schema.required != null && (!Array.isArray(schema.required) || !schema.required.every(
|
|
1406
|
+
(key) => typeof key === "string" && Object.getOwnPropertyDescriptor(properties, key) !== void 0
|
|
1407
|
+
)))
|
|
1408
|
+
return false;
|
|
1409
|
+
return Object.values(properties).every((field) => {
|
|
1410
|
+
if (!isRecord(field)) return false;
|
|
1411
|
+
if (field.type === "string") {
|
|
1412
|
+
return knownFormKeys(field, ["type", "enum", "oneOf", "minLength", "maxLength"]) && validFormBound(field.minLength, true) && validFormBound(field.maxLength, true) && (field.enum == null || Array.isArray(field.enum) && field.enum.length > 0 && field.enum.every((value) => typeof value === "string")) && (field.oneOf == null || titledChoices(field.oneOf));
|
|
1413
|
+
}
|
|
1414
|
+
if (field.type === "boolean") return knownFormKeys(field, ["type"]);
|
|
1415
|
+
if (field.type === "number" || field.type === "integer") {
|
|
1416
|
+
return knownFormKeys(field, ["type", "minimum", "maximum"]) && validFormBound(field.minimum) && validFormBound(field.maximum);
|
|
1417
|
+
}
|
|
1418
|
+
if (field.type !== "array" || !isRecord(field.items) || !knownFormKeys(field, ["type", "items", "minItems", "maxItems"]) || !validFormBound(field.minItems, true) || !validFormBound(field.maxItems, true))
|
|
1419
|
+
return false;
|
|
1420
|
+
return knownFormKeys(field.items, ["type", "enum"]) && field.items.type === "string" && Array.isArray(field.items.enum) && field.items.enum.length > 0 && field.items.enum.every((value) => typeof value === "string") || knownFormKeys(field.items, ["anyOf"]) && field.items.type === void 0 && titledChoices(field.items.anyOf);
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
function validateRuntimeQuestionContent(schema, content) {
|
|
1424
|
+
if (!supportedQuestionSchema(schema) || !isRecord(content)) return false;
|
|
1425
|
+
if (Object.getOwnPropertySymbols(content).length > 0 || Object.getOwnPropertyNames(content).some((key) => {
|
|
1426
|
+
const descriptor = Object.getOwnPropertyDescriptor(content, key);
|
|
1427
|
+
return !descriptor?.enumerable || !("value" in descriptor);
|
|
1428
|
+
}))
|
|
1429
|
+
return false;
|
|
1430
|
+
const properties = schema.properties ?? {};
|
|
1431
|
+
if ((schema.required ?? []).some(
|
|
1432
|
+
(key) => Object.getOwnPropertyDescriptor(content, key) === void 0
|
|
1433
|
+
))
|
|
1434
|
+
return false;
|
|
1435
|
+
return Object.entries(content).every(([key, value]) => {
|
|
1436
|
+
if (Object.getOwnPropertyDescriptor(properties, key) === void 0) return false;
|
|
1437
|
+
const field = properties[key];
|
|
1438
|
+
if (field.type === "boolean") return typeof value === "boolean";
|
|
1439
|
+
if (field.type === "number" || field.type === "integer") {
|
|
1440
|
+
return typeof value === "number" && Number.isFinite(value) && (field.type !== "integer" || Number.isInteger(value)) && (typeof field.minimum !== "number" || value >= field.minimum) && (typeof field.maximum !== "number" || value <= field.maximum);
|
|
1441
|
+
}
|
|
1442
|
+
if (field.type === "string") {
|
|
1443
|
+
return typeof value === "string" && (typeof field.minLength !== "number" || [...value].length >= field.minLength) && (typeof field.maxLength !== "number" || [...value].length <= field.maxLength) && (!Array.isArray(field.enum) || field.enum.includes(value)) && (!Array.isArray(field.oneOf) || field.oneOf.some((option) => isRecord(option) && option.const === value));
|
|
1444
|
+
}
|
|
1445
|
+
const items = field.items;
|
|
1446
|
+
const choices = Array.isArray(items.enum) ? items.enum : items.anyOf.map((option) => option.const);
|
|
1447
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string" && choices.includes(item)) && new Set(value).size === value.length && (typeof field.minItems !== "number" || value.length >= field.minItems) && (typeof field.maxItems !== "number" || value.length <= field.maxItems);
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1166
1450
|
function withAuthHint(acp, err) {
|
|
1167
1451
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
1168
1452
|
if (error instanceof acp.RequestError && error.code === acp.RequestError.authRequired().code) {
|
|
@@ -1173,6 +1457,13 @@ function withAuthHint(acp, err) {
|
|
|
1173
1457
|
function errorMessage(err) {
|
|
1174
1458
|
return err instanceof Error ? err.message : String(err);
|
|
1175
1459
|
}
|
|
1460
|
+
function timeoutMs(value) {
|
|
1461
|
+
const timeout = value ?? DEFAULT_TIMEOUT_MS;
|
|
1462
|
+
if (!Number.isFinite(timeout) || timeout < 0 || timeout > 2147483647) {
|
|
1463
|
+
throw new PermanentError("ACP timeout must be a finite non-negative timer duration");
|
|
1464
|
+
}
|
|
1465
|
+
return timeout;
|
|
1466
|
+
}
|
|
1176
1467
|
var AcpRuntime = class extends RuntimeBase {
|
|
1177
1468
|
constructor() {
|
|
1178
1469
|
super(...arguments);
|
|
@@ -1183,10 +1474,6 @@ var AcpRuntime = class extends RuntimeBase {
|
|
|
1183
1474
|
return {
|
|
1184
1475
|
name: this.name,
|
|
1185
1476
|
available: resolveExecutable(command) !== null,
|
|
1186
|
-
// `tools` is the one capability that depends on the bridge: only some of
|
|
1187
|
-
// them read an allowlist out of `_meta`, and none negotiates it. Claiming
|
|
1188
|
-
// it for a bridge that ignores `task.tools` would let a caller believe a
|
|
1189
|
-
// restriction landed when the agent is running unrestricted.
|
|
1190
1477
|
capabilities: {
|
|
1191
1478
|
...BASE_CAPABILITIES,
|
|
1192
1479
|
tools: specForCommand(command)?.tools ?? false,
|
|
@@ -1195,126 +1482,216 @@ var AcpRuntime = class extends RuntimeBase {
|
|
|
1195
1482
|
};
|
|
1196
1483
|
}
|
|
1197
1484
|
async execute(task) {
|
|
1198
|
-
const
|
|
1199
|
-
const
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1485
|
+
const { prompt, onEvent, signal, permissionMode, onPermission, onQuestion, ...options } = task;
|
|
1486
|
+
const startup = new AbortController();
|
|
1487
|
+
const forward = () => startup.abort();
|
|
1488
|
+
if (signal?.aborted) startup.abort();
|
|
1489
|
+
signal?.addEventListener("abort", forward, { once: true });
|
|
1490
|
+
let session;
|
|
1491
|
+
try {
|
|
1492
|
+
session = await this.openSession({
|
|
1493
|
+
...options,
|
|
1494
|
+
questionForms: Boolean(onQuestion),
|
|
1495
|
+
signal: startup.signal
|
|
1496
|
+
});
|
|
1497
|
+
} finally {
|
|
1498
|
+
signal?.removeEventListener("abort", forward);
|
|
1499
|
+
}
|
|
1500
|
+
try {
|
|
1501
|
+
return await session.prompt(prompt, {
|
|
1502
|
+
signal,
|
|
1503
|
+
timeout: task.timeout,
|
|
1504
|
+
onEvent,
|
|
1505
|
+
permissionMode,
|
|
1506
|
+
onPermission,
|
|
1507
|
+
onQuestion
|
|
1508
|
+
});
|
|
1509
|
+
} finally {
|
|
1510
|
+
await session.close();
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
async openSession(options) {
|
|
1514
|
+
if (options.signal?.aborted) throw abortError();
|
|
1515
|
+
if (options.resume === "fork") {
|
|
1516
|
+
throw new PermanentError("ACP does not support forked sessions");
|
|
1517
|
+
}
|
|
1518
|
+
const defaultTimeout = timeoutMs(options.timeout);
|
|
1519
|
+
const questionForms = options.questionForms === true;
|
|
1520
|
+
let startupReject;
|
|
1521
|
+
const startupFailure = new Promise((_, reject) => {
|
|
1522
|
+
startupReject = reject;
|
|
1523
|
+
});
|
|
1524
|
+
let failSession;
|
|
1525
|
+
const failStartup = (error) => {
|
|
1526
|
+
startupReject(error);
|
|
1527
|
+
failSession?.(error);
|
|
1528
|
+
};
|
|
1529
|
+
const onSessionAbort = () => failStartup(abortError());
|
|
1530
|
+
options.signal?.addEventListener("abort", onSessionAbort, { once: true });
|
|
1531
|
+
const startupTimer = setTimeout(
|
|
1532
|
+
() => failStartup(new Error(`acp startup timed out (${defaultTimeout}ms)`)),
|
|
1533
|
+
defaultTimeout
|
|
1534
|
+
);
|
|
1535
|
+
let cleanup;
|
|
1536
|
+
try {
|
|
1537
|
+
const acp = await Promise.race([import("@agentclientprotocol/sdk"), startupFailure]);
|
|
1538
|
+
if (options.signal?.aborted) throw abortError();
|
|
1539
|
+
const command = options.command ?? DEFAULT_COMMAND2;
|
|
1540
|
+
const spec = specForCommand(command);
|
|
1541
|
+
const meta = spec?.meta?.({ ...options, prompt: "", timeout: defaultTimeout });
|
|
1542
|
+
const child = spawn(resolveExecutable(command) ?? command, options.args ?? DEFAULT_ARGS, {
|
|
1543
|
+
cwd: options.workDir,
|
|
1212
1544
|
env: process.env,
|
|
1213
|
-
// Own process group, so a kill reaches whatever the agent spawned too.
|
|
1214
1545
|
detached: true
|
|
1215
1546
|
});
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1547
|
+
let state = "idle";
|
|
1548
|
+
let sessionId = "";
|
|
1549
|
+
let steeringSupported = false;
|
|
1550
|
+
let active;
|
|
1551
|
+
let childClosed = false;
|
|
1552
|
+
let terminalError;
|
|
1553
|
+
let errorOutput = "";
|
|
1554
|
+
let escalation;
|
|
1555
|
+
let acknowledgement;
|
|
1556
|
+
let closePromise;
|
|
1557
|
+
let closeResolve;
|
|
1558
|
+
let closeReject;
|
|
1559
|
+
let connection;
|
|
1222
1560
|
const killTree = (signal) => {
|
|
1223
|
-
if (child.pid
|
|
1224
|
-
|
|
1225
|
-
|
|
1561
|
+
if (child.pid !== void 0) {
|
|
1562
|
+
try {
|
|
1563
|
+
process.kill(-child.pid, signal);
|
|
1564
|
+
return;
|
|
1565
|
+
} catch {
|
|
1566
|
+
}
|
|
1226
1567
|
}
|
|
1227
1568
|
try {
|
|
1228
|
-
|
|
1569
|
+
child.kill(signal);
|
|
1229
1570
|
} catch {
|
|
1230
|
-
signalChild(signal);
|
|
1231
1571
|
}
|
|
1232
1572
|
};
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
escalation.unref();
|
|
1241
|
-
};
|
|
1242
|
-
let timedOut = false;
|
|
1243
|
-
const timer = task.timeout === void 0 ? void 0 : setTimeout(() => {
|
|
1244
|
-
timedOut = true;
|
|
1245
|
-
stop("SIGTERM");
|
|
1246
|
-
}, task.timeout);
|
|
1247
|
-
let cancelGrace;
|
|
1248
|
-
const clearTimers = () => {
|
|
1249
|
-
if (timer) clearTimeout(timer);
|
|
1250
|
-
if (escalation) clearTimeout(escalation);
|
|
1251
|
-
if (cancelGrace) clearTimeout(cancelGrace);
|
|
1573
|
+
const emitForTurn = (turn, event) => {
|
|
1574
|
+
turn.options.onEvent?.({
|
|
1575
|
+
...event,
|
|
1576
|
+
runId: turn.runId,
|
|
1577
|
+
agentId: options.agentId,
|
|
1578
|
+
seq: turn.seq++
|
|
1579
|
+
});
|
|
1252
1580
|
};
|
|
1253
|
-
const runId = randomUUID();
|
|
1254
|
-
let seq = 0;
|
|
1255
1581
|
const emit = (event) => {
|
|
1256
|
-
|
|
1582
|
+
if (active) emitForTurn(active, event);
|
|
1257
1583
|
};
|
|
1258
|
-
const
|
|
1259
|
-
if (
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1584
|
+
const settleTurn = (turn, output, error) => {
|
|
1585
|
+
if (active !== turn) return;
|
|
1586
|
+
turn.decisions.abort();
|
|
1587
|
+
if (turn.timer) clearTimeout(turn.timer);
|
|
1588
|
+
if (turn.cancelTimer) clearTimeout(turn.cancelTimer);
|
|
1589
|
+
if (turn.onAbort) turn.options.signal?.removeEventListener("abort", turn.onAbort);
|
|
1590
|
+
active = void 0;
|
|
1591
|
+
if (state === "running" || state === "cancelling") state = "idle";
|
|
1592
|
+
if (error) {
|
|
1593
|
+
if (closePromise) {
|
|
1594
|
+
void closePromise.then(
|
|
1595
|
+
() => turn.reject(error),
|
|
1596
|
+
(cleanupError) => turn.reject(cleanupError)
|
|
1597
|
+
);
|
|
1598
|
+
} else turn.reject(error);
|
|
1599
|
+
} else if (output) turn.resolve(output);
|
|
1265
1600
|
};
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1601
|
+
const clearCleanupTimers = () => {
|
|
1602
|
+
if (escalation) clearTimeout(escalation);
|
|
1603
|
+
if (acknowledgement) clearTimeout(acknowledgement);
|
|
1604
|
+
};
|
|
1605
|
+
const close = () => {
|
|
1606
|
+
if (closePromise) return closePromise;
|
|
1607
|
+
closePromise = new Promise((resolve4, reject) => {
|
|
1608
|
+
closeResolve = resolve4;
|
|
1609
|
+
closeReject = reject;
|
|
1610
|
+
});
|
|
1611
|
+
options.signal?.removeEventListener("abort", onSessionAbort);
|
|
1612
|
+
clearTimeout(startupTimer);
|
|
1613
|
+
state = "closing";
|
|
1614
|
+
if (active) settleTurn(active, void 0, active.error ?? terminalError ?? abortError());
|
|
1615
|
+
if (childClosed) {
|
|
1616
|
+
state = "closed";
|
|
1617
|
+
closeResolve?.();
|
|
1618
|
+
return closePromise;
|
|
1619
|
+
}
|
|
1620
|
+
escalation = setTimeout(() => {
|
|
1621
|
+
killTree("SIGKILL");
|
|
1622
|
+
if (childClosed) return;
|
|
1623
|
+
acknowledgement = setTimeout(() => {
|
|
1624
|
+
state = "closed";
|
|
1625
|
+
clearCleanupTimers();
|
|
1626
|
+
closeReject?.(new Error(`${command} cleanup failed: no child close after SIGKILL`));
|
|
1627
|
+
}, CLEANUP_ACK_MS);
|
|
1628
|
+
}, KILL_ESCALATION_MS);
|
|
1629
|
+
try {
|
|
1630
|
+
connection?.close();
|
|
1631
|
+
} catch {
|
|
1632
|
+
}
|
|
1633
|
+
killTree("SIGTERM");
|
|
1634
|
+
return closePromise;
|
|
1278
1635
|
};
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1636
|
+
const fail = (error) => {
|
|
1637
|
+
terminalError ??= error;
|
|
1638
|
+
startupReject(terminalError);
|
|
1639
|
+
void close().catch(() => {
|
|
1640
|
+
});
|
|
1284
1641
|
};
|
|
1642
|
+
failSession = fail;
|
|
1643
|
+
cleanup = close;
|
|
1644
|
+
const stderrDecoder = new StringDecoder("utf8");
|
|
1645
|
+
child.stderr.on("data", (data) => {
|
|
1646
|
+
errorOutput += stderrDecoder.write(data);
|
|
1647
|
+
});
|
|
1648
|
+
child.on("close", (code) => {
|
|
1649
|
+
childClosed = true;
|
|
1650
|
+
errorOutput += stderrDecoder.end();
|
|
1651
|
+
clearCleanupTimers();
|
|
1652
|
+
clearTimeout(startupTimer);
|
|
1653
|
+
options.signal?.removeEventListener("abort", onSessionAbort);
|
|
1654
|
+
if (!closePromise) {
|
|
1655
|
+
fail(new Error(`${command} exited with code ${code}: ${errorOutput}`));
|
|
1656
|
+
}
|
|
1657
|
+
state = "closed";
|
|
1658
|
+
try {
|
|
1659
|
+
connection?.close();
|
|
1660
|
+
} catch {
|
|
1661
|
+
}
|
|
1662
|
+
closeResolve?.();
|
|
1663
|
+
});
|
|
1664
|
+
child.on("error", (error) => {
|
|
1665
|
+
fail(new PermanentError(`Failed to start ${command}: ${error.message}`));
|
|
1666
|
+
});
|
|
1285
1667
|
const handleUpdate = (notification) => {
|
|
1286
|
-
|
|
1668
|
+
const turn = active;
|
|
1669
|
+
if (!turn || notification.sessionId !== sessionId) return;
|
|
1287
1670
|
const update = notification.update;
|
|
1288
1671
|
switch (update.sessionUpdate) {
|
|
1289
1672
|
case "agent_message_chunk": {
|
|
1290
1673
|
const text = textOf(update.content);
|
|
1291
|
-
if (text === void 0) {
|
|
1292
|
-
|
|
1293
|
-
|
|
1674
|
+
if (text === void 0) emit({ type: "raw", vendor: "acp", payload: update });
|
|
1675
|
+
else {
|
|
1676
|
+
turn.output += text;
|
|
1677
|
+
emit({ type: "message_delta", role: "agent", text });
|
|
1294
1678
|
}
|
|
1295
|
-
agentText += text;
|
|
1296
|
-
emit({ type: "message_delta", role: "agent", text });
|
|
1297
|
-
break;
|
|
1298
|
-
}
|
|
1299
|
-
case "user_message_chunk": {
|
|
1300
|
-
const text = textOf(update.content);
|
|
1301
|
-
if (text === void 0) {
|
|
1302
|
-
emit({ type: "raw", vendor: "acp", payload: update });
|
|
1303
|
-
break;
|
|
1304
|
-
}
|
|
1305
|
-
emit({ type: "message_delta", role: "user", text });
|
|
1306
1679
|
break;
|
|
1307
1680
|
}
|
|
1681
|
+
case "user_message_chunk":
|
|
1308
1682
|
case "agent_thought_chunk": {
|
|
1309
1683
|
const text = textOf(update.content);
|
|
1310
|
-
if (text === void 0) {
|
|
1311
|
-
|
|
1312
|
-
|
|
1684
|
+
if (text === void 0) emit({ type: "raw", vendor: "acp", payload: update });
|
|
1685
|
+
else {
|
|
1686
|
+
emit({
|
|
1687
|
+
type: "message_delta",
|
|
1688
|
+
role: update.sessionUpdate === "user_message_chunk" ? "user" : "thought",
|
|
1689
|
+
text
|
|
1690
|
+
});
|
|
1313
1691
|
}
|
|
1314
|
-
emit({ type: "message_delta", role: "thought", text });
|
|
1315
1692
|
break;
|
|
1316
1693
|
}
|
|
1317
|
-
case "tool_call":
|
|
1694
|
+
case "tool_call":
|
|
1318
1695
|
emit({
|
|
1319
1696
|
type: "tool_call",
|
|
1320
1697
|
id: update.toolCallId,
|
|
@@ -1322,8 +1699,7 @@ ${task.prompt}` : task.prompt;
|
|
|
1322
1699
|
input: update.rawInput
|
|
1323
1700
|
});
|
|
1324
1701
|
break;
|
|
1325
|
-
|
|
1326
|
-
case "tool_call_update": {
|
|
1702
|
+
case "tool_call_update":
|
|
1327
1703
|
emit({
|
|
1328
1704
|
type: "tool_result",
|
|
1329
1705
|
id: update.toolCallId,
|
|
@@ -1331,29 +1707,21 @@ ${task.prompt}` : task.prompt;
|
|
|
1331
1707
|
isError: update.status === "failed"
|
|
1332
1708
|
});
|
|
1333
1709
|
break;
|
|
1334
|
-
|
|
1335
|
-
case "plan": {
|
|
1710
|
+
case "plan":
|
|
1336
1711
|
emit({ type: "plan", entries: update.entries.map((entry3) => entry3.content) });
|
|
1337
1712
|
break;
|
|
1338
|
-
}
|
|
1339
1713
|
case "usage_update": {
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
lastUsage = { contextTokens, contextSize };
|
|
1344
|
-
emit({ type: "usage", contextTokens, contextSize });
|
|
1714
|
+
if (typeof update.used === "number" && typeof update.size === "number") {
|
|
1715
|
+
turn.usage = { contextTokens: update.used, contextSize: update.size };
|
|
1716
|
+
emit({ type: "usage", ...turn.usage });
|
|
1345
1717
|
}
|
|
1346
1718
|
const cost = isRecord(update.cost) ? update.cost : void 0;
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
if (amount !== void 0 && currency !== void 0) {
|
|
1350
|
-
lastCost = { amount, currency };
|
|
1351
|
-
const costUsd = currency.toLowerCase() === "usd" ? amount : void 0;
|
|
1719
|
+
if (typeof cost?.amount === "number" && typeof cost.currency === "string") {
|
|
1720
|
+
turn.cost = { amount: cost.amount, currency: cost.currency };
|
|
1352
1721
|
emit({
|
|
1353
1722
|
type: "cost",
|
|
1354
|
-
|
|
1355
|
-
currency
|
|
1356
|
-
...costUsd !== void 0 ? { costUsd } : {}
|
|
1723
|
+
...turn.cost,
|
|
1724
|
+
...cost.currency.toLowerCase() === "usd" ? { costUsd: cost.amount } : {}
|
|
1357
1725
|
});
|
|
1358
1726
|
}
|
|
1359
1727
|
break;
|
|
@@ -1363,154 +1731,372 @@ ${task.prompt}` : task.prompt;
|
|
|
1363
1731
|
}
|
|
1364
1732
|
};
|
|
1365
1733
|
const app = acp.client({ name: "multiclaw" });
|
|
1366
|
-
app.onNotification(acp.methods.client.session.update, (
|
|
1367
|
-
handleUpdate(
|
|
1734
|
+
app.onNotification(acp.methods.client.session.update, (context) => {
|
|
1735
|
+
handleUpdate(context.params);
|
|
1368
1736
|
});
|
|
1369
|
-
app.onRequest(acp.methods.client.session.requestPermission, (
|
|
1370
|
-
const {
|
|
1737
|
+
app.onRequest(acp.methods.client.session.requestPermission, async (context) => {
|
|
1738
|
+
const cancelled = { outcome: { outcome: "cancelled" } };
|
|
1739
|
+
const turn = active;
|
|
1740
|
+
const params = context.params;
|
|
1741
|
+
if (state !== "running" || !turn || params.sessionId !== sessionId || context.signal.aborted)
|
|
1742
|
+
return cancelled;
|
|
1743
|
+
const mode = turn.options.permissionMode ?? "auto";
|
|
1744
|
+
const { response, rejected } = decidePermission(params, options.tools, sessionId);
|
|
1371
1745
|
if (rejected) {
|
|
1746
|
+
warn("Tool permission denied by the configured tools policy.");
|
|
1372
1747
|
emit({
|
|
1373
1748
|
type: "raw",
|
|
1374
1749
|
vendor: "acp",
|
|
1375
1750
|
payload: { type: "permission_rejected", ...rejected }
|
|
1376
1751
|
});
|
|
1752
|
+
return response;
|
|
1753
|
+
}
|
|
1754
|
+
if (mode === "auto") return response;
|
|
1755
|
+
if (mode !== "ask" || !turn.options.onPermission) {
|
|
1756
|
+
warn(
|
|
1757
|
+
mode === "ask" ? "Tool permission cancelled: no approval handler is available." : "Tool permission denied by the configured approval mode."
|
|
1758
|
+
);
|
|
1759
|
+
return cancelled;
|
|
1760
|
+
}
|
|
1761
|
+
const signal = AbortSignal.any([turn.decisions.signal, context.signal]);
|
|
1762
|
+
const offered = new Set(params.options.map((option) => option.optionId));
|
|
1763
|
+
const request = {
|
|
1764
|
+
requestId: randomUUID(),
|
|
1765
|
+
runId: turn.runId,
|
|
1766
|
+
agentId: options.agentId,
|
|
1767
|
+
seq: turn.seq++,
|
|
1768
|
+
sessionId,
|
|
1769
|
+
toolCallId: params.toolCall.toolCallId,
|
|
1770
|
+
title: params.toolCall.title ?? params.toolCall.name ?? "Tool permission",
|
|
1771
|
+
toolName: params.toolCall.name ?? void 0,
|
|
1772
|
+
kind: params.toolCall.kind ?? void 0,
|
|
1773
|
+
input: params.toolCall.rawInput,
|
|
1774
|
+
options: params.options.map(({ optionId, name, kind }) => ({ optionId, name, kind }))
|
|
1775
|
+
};
|
|
1776
|
+
let onAbort;
|
|
1777
|
+
const aborted = new Promise((resolve4) => {
|
|
1778
|
+
onAbort = () => resolve4(void 0);
|
|
1779
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1780
|
+
if (signal.aborted) onAbort();
|
|
1781
|
+
});
|
|
1782
|
+
try {
|
|
1783
|
+
const optionId = await Promise.race([
|
|
1784
|
+
Promise.resolve().then(
|
|
1785
|
+
() => signal.aborted ? void 0 : turn.options.onPermission?.(request, signal)
|
|
1786
|
+
),
|
|
1787
|
+
aborted
|
|
1788
|
+
]);
|
|
1789
|
+
if (signal.aborted || active !== turn || state !== "running") return cancelled;
|
|
1790
|
+
return optionId !== void 0 && offered.has(optionId) ? { outcome: { outcome: "selected", optionId } } : cancelled;
|
|
1791
|
+
} catch {
|
|
1792
|
+
return cancelled;
|
|
1793
|
+
} finally {
|
|
1794
|
+
signal.removeEventListener("abort", onAbort);
|
|
1377
1795
|
}
|
|
1378
|
-
return response;
|
|
1379
|
-
});
|
|
1380
|
-
const stderrDecoder = new StringDecoder("utf8");
|
|
1381
|
-
child.stderr.on("data", (d) => {
|
|
1382
|
-
errorOutput += stderrDecoder.write(d);
|
|
1383
1796
|
});
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1797
|
+
app.onRequest(acp.methods.client.elicitation.create, async (context) => {
|
|
1798
|
+
const cancelled = {
|
|
1799
|
+
action: "cancel"
|
|
1800
|
+
};
|
|
1801
|
+
const turn = active;
|
|
1802
|
+
const params = context.params;
|
|
1803
|
+
if (state !== "running" || !turn || context.signal.aborted || !("sessionId" in params) || params.sessionId !== sessionId)
|
|
1804
|
+
return cancelled;
|
|
1805
|
+
if (params.mode !== "form" || !supportedQuestionSchema(params.requestedSchema)) {
|
|
1806
|
+
warn("Structured question cancelled: unsupported form mode or schema.");
|
|
1807
|
+
return cancelled;
|
|
1808
|
+
}
|
|
1809
|
+
if (!questionForms || !turn.options.onQuestion) {
|
|
1810
|
+
warn("Structured question cancelled: no negotiated question handler is available.");
|
|
1811
|
+
return cancelled;
|
|
1812
|
+
}
|
|
1813
|
+
const schema = structuredClone(params.requestedSchema);
|
|
1814
|
+
const signal = AbortSignal.any([turn.decisions.signal, context.signal]);
|
|
1815
|
+
const request = {
|
|
1816
|
+
requestId: randomUUID(),
|
|
1817
|
+
runId: turn.runId,
|
|
1818
|
+
agentId: options.agentId,
|
|
1819
|
+
seq: turn.seq++,
|
|
1820
|
+
sessionId,
|
|
1821
|
+
toolCallId: "toolCallId" in params && typeof params.toolCallId === "string" ? params.toolCallId : void 0,
|
|
1822
|
+
message: params.message,
|
|
1823
|
+
requestedSchema: structuredClone(schema)
|
|
1824
|
+
};
|
|
1825
|
+
let onAbort;
|
|
1826
|
+
const aborted = new Promise((resolve4) => {
|
|
1827
|
+
onAbort = () => resolve4(void 0);
|
|
1828
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1829
|
+
if (signal.aborted) onAbort();
|
|
1830
|
+
});
|
|
1391
1831
|
try {
|
|
1392
|
-
|
|
1832
|
+
const response = await Promise.race([
|
|
1833
|
+
Promise.resolve().then(
|
|
1834
|
+
() => signal.aborted ? void 0 : turn.options.onQuestion?.(request, signal)
|
|
1835
|
+
),
|
|
1836
|
+
aborted
|
|
1837
|
+
]);
|
|
1838
|
+
if (signal.aborted || active !== turn || state !== "running" || !response)
|
|
1839
|
+
return cancelled;
|
|
1840
|
+
if (response.action === "accept") {
|
|
1841
|
+
if (!validateRuntimeQuestionContent(schema, response.content)) {
|
|
1842
|
+
warn("Structured question cancelled: the answer does not match the requested schema.");
|
|
1843
|
+
return cancelled;
|
|
1844
|
+
}
|
|
1845
|
+
return { action: "accept", content: structuredClone(response.content) };
|
|
1846
|
+
}
|
|
1847
|
+
return response.action === "decline" ? { action: "decline" } : cancelled;
|
|
1393
1848
|
} catch {
|
|
1849
|
+
return cancelled;
|
|
1850
|
+
} finally {
|
|
1851
|
+
signal.removeEventListener("abort", onAbort);
|
|
1394
1852
|
}
|
|
1395
|
-
|
|
1853
|
+
});
|
|
1854
|
+
connection = app.connect(
|
|
1855
|
+
acp.ndJsonStream(
|
|
1856
|
+
Writable.toWeb(child.stdin),
|
|
1857
|
+
Readable.toWeb(child.stdout)
|
|
1858
|
+
)
|
|
1859
|
+
);
|
|
1860
|
+
const ctx = connection.agent;
|
|
1861
|
+
const onConnectionClosed = () => {
|
|
1862
|
+
if (!closePromise) fail(new Error(`${command} ACP connection closed: ${errorOutput}`));
|
|
1396
1863
|
};
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1864
|
+
connection.closed?.then(onConnectionClosed, onConnectionClosed);
|
|
1865
|
+
const cancelTurn = () => {
|
|
1866
|
+
const turn = active;
|
|
1867
|
+
if (!turn) return closePromise ?? Promise.resolve();
|
|
1868
|
+
if (turn.cancellation) return turn.cancellation;
|
|
1869
|
+
state = "cancelling";
|
|
1870
|
+
turn.decisions.abort();
|
|
1871
|
+
const waitForCleanup = async () => {
|
|
1872
|
+
if (closePromise) await closePromise;
|
|
1873
|
+
};
|
|
1874
|
+
turn.cancellation = turn.promise.then(waitForCleanup, waitForCleanup);
|
|
1875
|
+
turn.cancelTimer = setTimeout(() => {
|
|
1876
|
+
fail(turn.error ?? abortError());
|
|
1877
|
+
}, CANCEL_GRACE_MS);
|
|
1878
|
+
void Promise.resolve().then(() => {
|
|
1879
|
+
if (active === turn) return ctx.notify(acp.methods.agent.session.cancel, { sessionId });
|
|
1880
|
+
}).catch((error) => {
|
|
1881
|
+
if (active === turn) fail(withAuthHint(acp, error));
|
|
1882
|
+
});
|
|
1883
|
+
return turn.cancellation;
|
|
1884
|
+
};
|
|
1885
|
+
const supplement = async (text, supplementOptions = {}) => {
|
|
1886
|
+
if (!steeringSupported) {
|
|
1887
|
+
throw new Error(`${command} does not accept mid-turn input`);
|
|
1888
|
+
}
|
|
1889
|
+
const turn = active;
|
|
1890
|
+
if (!turn || state !== "running") return { status: "idle" };
|
|
1891
|
+
if (supplementOptions.signal?.aborted) throw abortError();
|
|
1892
|
+
let timer;
|
|
1893
|
+
try {
|
|
1894
|
+
const response = await Promise.race([
|
|
1895
|
+
ctx.request(STEER_METHOD, {
|
|
1896
|
+
sessionId,
|
|
1897
|
+
prompt: [{ type: "text", text }],
|
|
1898
|
+
_meta: { steering: { idleBehavior: "promptRequired" } }
|
|
1899
|
+
}),
|
|
1900
|
+
new Promise((_, reject) => {
|
|
1901
|
+
timer = setTimeout(
|
|
1902
|
+
() => reject(new Error(`steering was not acknowledged within ${STEER_ACK_MS}ms`)),
|
|
1903
|
+
STEER_ACK_MS
|
|
1904
|
+
);
|
|
1905
|
+
})
|
|
1906
|
+
]);
|
|
1907
|
+
if (active !== turn) return { status: "idle" };
|
|
1908
|
+
const outcome = response?.outcome;
|
|
1909
|
+
if (outcome === "injected") return { status: "injected" };
|
|
1910
|
+
if (outcome === "promptRequired") return { status: "idle" };
|
|
1911
|
+
if (outcome === "startedNewTurn") {
|
|
1912
|
+
await Promise.resolve().then(() => ctx.notify(acp.methods.agent.session.cancel, { sessionId })).catch(() => {
|
|
1402
1913
|
});
|
|
1914
|
+
return { status: "idle" };
|
|
1403
1915
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
errorOutput += stderrDecoder.end();
|
|
1411
|
-
if (settled) return;
|
|
1412
|
-
if (task.signal?.aborted) {
|
|
1413
|
-
settleReject(abortError());
|
|
1414
|
-
return;
|
|
1916
|
+
if (outcome === "failed") {
|
|
1917
|
+
throw new Error("the agent could not apply the mid-turn message");
|
|
1918
|
+
}
|
|
1919
|
+
throw new Error(`unexpected steering outcome: ${JSON.stringify(outcome ?? null)}`);
|
|
1920
|
+
} finally {
|
|
1921
|
+
if (timer) clearTimeout(timer);
|
|
1415
1922
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1923
|
+
};
|
|
1924
|
+
const prompt = (text, promptOptions = {}) => {
|
|
1925
|
+
if (state !== "idle") {
|
|
1926
|
+
return Promise.reject(new Error(`Cannot prompt ACP session while ${state}`));
|
|
1419
1927
|
}
|
|
1420
|
-
if (
|
|
1421
|
-
|
|
1422
|
-
return;
|
|
1928
|
+
if (options.signal?.aborted || promptOptions.signal?.aborted) {
|
|
1929
|
+
return Promise.reject(abortError());
|
|
1423
1930
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1931
|
+
let duration;
|
|
1932
|
+
try {
|
|
1933
|
+
duration = timeoutMs(promptOptions.timeout ?? defaultTimeout);
|
|
1934
|
+
} catch (error) {
|
|
1935
|
+
return Promise.reject(error);
|
|
1936
|
+
}
|
|
1937
|
+
let resolve4;
|
|
1938
|
+
let reject;
|
|
1939
|
+
const promise = new Promise((res, rej) => {
|
|
1940
|
+
resolve4 = res;
|
|
1941
|
+
reject = rej;
|
|
1942
|
+
});
|
|
1943
|
+
const turn = {
|
|
1944
|
+
runId: randomUUID(),
|
|
1945
|
+
seq: 0,
|
|
1946
|
+
options: promptOptions,
|
|
1947
|
+
output: "",
|
|
1948
|
+
decisions: new AbortController(),
|
|
1949
|
+
promise,
|
|
1950
|
+
resolve: resolve4,
|
|
1951
|
+
reject
|
|
1952
|
+
};
|
|
1953
|
+
active = turn;
|
|
1954
|
+
state = "running";
|
|
1955
|
+
turn.onAbort = () => {
|
|
1956
|
+
turn.error = abortError();
|
|
1957
|
+
void cancelTurn().catch(() => {
|
|
1958
|
+
});
|
|
1959
|
+
};
|
|
1960
|
+
promptOptions.signal?.addEventListener("abort", turn.onAbort, { once: true });
|
|
1961
|
+
turn.timer = setTimeout(() => {
|
|
1962
|
+
turn.error = new Error(`acp timed out (${duration}ms)`);
|
|
1963
|
+
fail(turn.error);
|
|
1964
|
+
}, duration);
|
|
1965
|
+
const run = async () => {
|
|
1966
|
+
emit({ type: "run_started", model: options.model });
|
|
1967
|
+
if (active !== turn) return;
|
|
1968
|
+
const response = await ctx.request(acp.methods.agent.session.prompt, {
|
|
1969
|
+
sessionId,
|
|
1970
|
+
prompt: [
|
|
1971
|
+
{
|
|
1972
|
+
type: "text",
|
|
1973
|
+
text: meta === void 0 && options.systemPrompt ? `${options.systemPrompt}
|
|
1974
|
+
|
|
1975
|
+
---
|
|
1976
|
+
|
|
1977
|
+
${text}` : text
|
|
1978
|
+
}
|
|
1979
|
+
]
|
|
1980
|
+
});
|
|
1981
|
+
if (active !== turn) return;
|
|
1982
|
+
if (turn.error) {
|
|
1983
|
+
settleTurn(turn, void 0, turn.error);
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
const stopReason = mapStopReason(response.stopReason);
|
|
1987
|
+
settleTurn(turn, {
|
|
1988
|
+
output: turn.output,
|
|
1989
|
+
exitCode: stopReason === "success" ? 0 : 1,
|
|
1990
|
+
sessionId,
|
|
1991
|
+
stopReason,
|
|
1992
|
+
...turn.usage ? { usage: turn.usage } : {},
|
|
1993
|
+
...turn.cost?.currency.toLowerCase() === "usd" ? { costUsd: turn.cost.amount } : {}
|
|
1994
|
+
});
|
|
1995
|
+
emitForTurn(turn, { type: "run_finished", stopReason });
|
|
1996
|
+
};
|
|
1997
|
+
void run().catch((error) => {
|
|
1998
|
+
if (active === turn) fail(withAuthHint(acp, error));
|
|
1999
|
+
});
|
|
2000
|
+
return promise;
|
|
2001
|
+
};
|
|
2002
|
+
const session = {
|
|
2003
|
+
get sessionId() {
|
|
2004
|
+
return sessionId;
|
|
2005
|
+
},
|
|
2006
|
+
get state() {
|
|
2007
|
+
return state;
|
|
2008
|
+
},
|
|
2009
|
+
get capabilities() {
|
|
2010
|
+
return { continuation: true, supplement: steeringSupported };
|
|
2011
|
+
},
|
|
2012
|
+
prompt,
|
|
2013
|
+
supplement,
|
|
2014
|
+
cancelTurn,
|
|
2015
|
+
close
|
|
2016
|
+
};
|
|
2017
|
+
const warn = (message) => {
|
|
2018
|
+
if (options.onWarning) options.onWarning(message);
|
|
2019
|
+
else process.stderr.write(`[multiclaw] Warning: ${message}
|
|
2020
|
+
`);
|
|
2021
|
+
};
|
|
2022
|
+
const initialize = async () => {
|
|
1430
2023
|
const init = await ctx.request(acp.methods.agent.initialize, {
|
|
1431
|
-
protocolVersion: acp.PROTOCOL_VERSION
|
|
2024
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
2025
|
+
...questionForms ? { clientCapabilities: { elicitation: { form: {} } } } : {}
|
|
1432
2026
|
});
|
|
1433
|
-
|
|
1434
|
-
|
|
2027
|
+
if (terminalError) throw terminalError;
|
|
2028
|
+
steeringSupported = advertisesSteering(init);
|
|
2029
|
+
if (options.sessionId) {
|
|
1435
2030
|
if (init.agentCapabilities?.loadSession !== true) {
|
|
1436
2031
|
throw new PermanentError(
|
|
1437
|
-
`Agent does not support session/load (loadSession=false); cannot resume session ${
|
|
2032
|
+
`Agent does not support session/load (loadSession=false); cannot resume session ${options.sessionId}`
|
|
1438
2033
|
);
|
|
1439
2034
|
}
|
|
1440
2035
|
await ctx.request(acp.methods.agent.session.load, {
|
|
1441
|
-
sessionId:
|
|
1442
|
-
cwd:
|
|
2036
|
+
sessionId: options.sessionId,
|
|
2037
|
+
cwd: options.workDir,
|
|
1443
2038
|
mcpServers: [],
|
|
1444
|
-
// Both session-defining requests carry `_meta`: the bridge's own
|
|
1445
|
-
// session fingerprint includes it, so a changed prompt rebuilds the
|
|
1446
|
-
// session on resume.
|
|
1447
2039
|
...meta ? { _meta: meta } : {}
|
|
1448
2040
|
});
|
|
1449
|
-
sessionId =
|
|
2041
|
+
sessionId = options.sessionId;
|
|
1450
2042
|
} else {
|
|
1451
2043
|
const created = await ctx.request(acp.methods.agent.session.new, {
|
|
1452
|
-
cwd:
|
|
2044
|
+
cwd: options.workDir,
|
|
1453
2045
|
mcpServers: [],
|
|
1454
2046
|
...meta ? { _meta: meta } : {}
|
|
1455
2047
|
});
|
|
1456
2048
|
sessionId = created.sessionId;
|
|
1457
2049
|
const modelOpt = created.configOptions?.find(
|
|
1458
|
-
(
|
|
2050
|
+
(option) => option.category === "model" || option.id === "model"
|
|
1459
2051
|
);
|
|
1460
2052
|
if (modelOpt && "options" in modelOpt && Array.isArray(modelOpt.options)) {
|
|
1461
|
-
|
|
1462
|
-
|
|
2053
|
+
discoveredModels.set(
|
|
2054
|
+
command,
|
|
2055
|
+
(modelOpt.options ?? []).map(
|
|
2056
|
+
(option) => option.value ?? String(option)
|
|
2057
|
+
)
|
|
1463
2058
|
);
|
|
1464
|
-
discoveredModels.set(command, ids);
|
|
1465
2059
|
}
|
|
1466
2060
|
}
|
|
1467
|
-
if (
|
|
2061
|
+
if (terminalError) throw terminalError;
|
|
2062
|
+
if (options.model && spec?.modelSelection === "config-option") {
|
|
1468
2063
|
const known = discoveredModels.get(command);
|
|
1469
|
-
if (known && !known.includes(
|
|
2064
|
+
if (known && !known.includes(options.model)) {
|
|
1470
2065
|
warn(
|
|
1471
|
-
`model "${
|
|
2066
|
+
`model "${options.model}" is not offered by ${command}; running with the provider's default.`
|
|
1472
2067
|
);
|
|
1473
2068
|
} else {
|
|
1474
2069
|
try {
|
|
1475
2070
|
await ctx.request(acp.methods.agent.session.setConfigOption, {
|
|
1476
2071
|
sessionId,
|
|
1477
2072
|
configId: "model",
|
|
1478
|
-
value:
|
|
2073
|
+
value: options.model
|
|
1479
2074
|
});
|
|
1480
|
-
} catch (
|
|
2075
|
+
} catch (error) {
|
|
2076
|
+
if (terminalError) throw terminalError;
|
|
1481
2077
|
warn(
|
|
1482
|
-
`Could not set model "${
|
|
2078
|
+
`Could not set model "${options.model}" on ${command}; running with the provider's default (${errorMessage(error)}).`
|
|
1483
2079
|
);
|
|
1484
2080
|
}
|
|
1485
2081
|
}
|
|
1486
2082
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
prompt: [{ type: "text", text: promptText }]
|
|
1490
|
-
});
|
|
1491
|
-
if (task.signal?.aborted) {
|
|
1492
|
-
settleReject(abortError());
|
|
1493
|
-
teardown();
|
|
1494
|
-
return;
|
|
1495
|
-
}
|
|
1496
|
-
const stopReason = mapStopReason(response.stopReason);
|
|
1497
|
-
emit({ type: "run_finished", stopReason });
|
|
1498
|
-
settleResolve({
|
|
1499
|
-
output: agentText,
|
|
1500
|
-
exitCode: 0,
|
|
1501
|
-
sessionId,
|
|
1502
|
-
stopReason,
|
|
1503
|
-
...lastUsage ? { usage: lastUsage } : {},
|
|
1504
|
-
...lastCost?.currency.toLowerCase() === "usd" ? { costUsd: lastCost.amount } : {}
|
|
1505
|
-
});
|
|
1506
|
-
teardown();
|
|
2083
|
+
if (terminalError) throw terminalError;
|
|
2084
|
+
if (options.signal?.aborted) throw abortError();
|
|
1507
2085
|
};
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
2086
|
+
await Promise.race([
|
|
2087
|
+
initialize().catch((error) => {
|
|
2088
|
+
throw withAuthHint(acp, error);
|
|
2089
|
+
}),
|
|
2090
|
+
startupFailure
|
|
2091
|
+
]);
|
|
2092
|
+
clearTimeout(startupTimer);
|
|
2093
|
+
return session;
|
|
2094
|
+
} catch (error) {
|
|
2095
|
+
clearTimeout(startupTimer);
|
|
2096
|
+
options.signal?.removeEventListener("abort", onSessionAbort);
|
|
2097
|
+
if (cleanup) await cleanup();
|
|
2098
|
+
throw error;
|
|
2099
|
+
}
|
|
1514
2100
|
}
|
|
1515
2101
|
};
|
|
1516
2102
|
|
|
@@ -1610,16 +2196,18 @@ function retryBudgetMs(perAttemptMs, retries, delayMs = DEFAULT_RETRY_DELAY_MS)
|
|
|
1610
2196
|
}
|
|
1611
2197
|
|
|
1612
2198
|
// src/utils/timeout.ts
|
|
1613
|
-
function withTimeout(factory, ms, label, signal) {
|
|
2199
|
+
function withTimeout(factory, ms, label, signal, options) {
|
|
1614
2200
|
if (signal?.aborted) return Promise.reject(abortError());
|
|
1615
2201
|
const controller = new AbortController();
|
|
1616
2202
|
const forward = () => controller.abort();
|
|
1617
2203
|
signal?.addEventListener("abort", forward, { once: true });
|
|
1618
2204
|
let timer;
|
|
2205
|
+
let timeoutError;
|
|
1619
2206
|
const timeout = new Promise((_, reject) => {
|
|
1620
2207
|
timer = setTimeout(() => {
|
|
2208
|
+
timeoutError = new Error(`[${label}] timed out (${ms / 1e3}s)`);
|
|
1621
2209
|
controller.abort();
|
|
1622
|
-
|
|
2210
|
+
if (!options?.waitForCleanup) reject(timeoutError);
|
|
1623
2211
|
}, ms);
|
|
1624
2212
|
});
|
|
1625
2213
|
let onCancel;
|
|
@@ -1628,7 +2216,18 @@ function withTimeout(factory, ms, label, signal) {
|
|
|
1628
2216
|
onCancel = () => reject(abortError());
|
|
1629
2217
|
signal.addEventListener("abort", onCancel, { once: true });
|
|
1630
2218
|
});
|
|
1631
|
-
|
|
2219
|
+
const work = factory(controller.signal);
|
|
2220
|
+
const completion = options?.waitForCleanup ? work.then(
|
|
2221
|
+
(result) => {
|
|
2222
|
+
if (timeoutError) throw timeoutError;
|
|
2223
|
+
return result;
|
|
2224
|
+
},
|
|
2225
|
+
(error) => {
|
|
2226
|
+
if (timeoutError && isAbortError(error)) throw timeoutError;
|
|
2227
|
+
throw error;
|
|
2228
|
+
}
|
|
2229
|
+
) : work;
|
|
2230
|
+
return Promise.race([completion, timeout, cancelled]).finally(() => {
|
|
1632
2231
|
clearTimeout(timer);
|
|
1633
2232
|
signal?.removeEventListener("abort", forward);
|
|
1634
2233
|
if (onCancel) signal?.removeEventListener("abort", onCancel);
|
|
@@ -1662,13 +2261,14 @@ function summarizeToolOutput(output) {
|
|
|
1662
2261
|
|
|
1663
2262
|
// src/orchestrator/AgentRunner.ts
|
|
1664
2263
|
var AgentRunner = class {
|
|
1665
|
-
constructor(eventBus, contextManager, globalWorkDir, config) {
|
|
2264
|
+
constructor(eventBus, contextManager, globalWorkDir, config, sessionManager) {
|
|
1666
2265
|
this.eventBus = eventBus;
|
|
1667
2266
|
this.contextManager = contextManager;
|
|
1668
2267
|
this.globalWorkDir = globalWorkDir;
|
|
1669
2268
|
this.config = config;
|
|
2269
|
+
this.sessionManager = sessionManager;
|
|
1670
2270
|
}
|
|
1671
|
-
async run(agent, signal) {
|
|
2271
|
+
async run(agent, signal, instruction) {
|
|
1672
2272
|
const startTime = /* @__PURE__ */ new Date();
|
|
1673
2273
|
const model = resolveModel(agent, this.config);
|
|
1674
2274
|
this.eventBus.emit({
|
|
@@ -1697,6 +2297,7 @@ var AgentRunner = class {
|
|
|
1697
2297
|
(innerSignal) => withRetry(
|
|
1698
2298
|
async (attempt) => {
|
|
1699
2299
|
attempts = attempt;
|
|
2300
|
+
if (signal?.aborted) throw abortError();
|
|
1700
2301
|
const agentContext = {
|
|
1701
2302
|
...this.contextManager.snapshot(),
|
|
1702
2303
|
// Every built-in task template references both. The leader only
|
|
@@ -1707,15 +2308,28 @@ var AgentRunner = class {
|
|
|
1707
2308
|
agentScope: agent.agentScope ?? "Not narrowed down \u2014 cover the whole task.",
|
|
1708
2309
|
agentPlan: agent.agentPlan ?? "No step-by-step plan was provided \u2014 use your judgement."
|
|
1709
2310
|
};
|
|
1710
|
-
|
|
1711
|
-
|
|
2311
|
+
let prompt = await renderPrompt(agent.taskPrompt, agentContext, this.globalWorkDir);
|
|
2312
|
+
const requirement = this.contextManager.get("requirement") ?? this.config.context?.requirement;
|
|
2313
|
+
if (agent.id !== this.config.leader?.id && requirement !== void 0) {
|
|
2314
|
+
prompt = `## Current task
|
|
2315
|
+
${requirement}
|
|
2316
|
+
|
|
2317
|
+
${prompt}`;
|
|
2318
|
+
}
|
|
2319
|
+
if (instruction) prompt += `
|
|
2320
|
+
|
|
2321
|
+
${instruction}`;
|
|
2322
|
+
const task = {
|
|
1712
2323
|
agentId: agent.id,
|
|
1713
2324
|
systemPrompt: agent.systemPrompt,
|
|
1714
2325
|
prompt,
|
|
1715
2326
|
tools,
|
|
2327
|
+
permissionMode: this.config.permissions?.mode,
|
|
2328
|
+
onPermission: this.config.permissions?.onRequest,
|
|
2329
|
+
onQuestion: this.config.questions?.onRequest,
|
|
1716
2330
|
workDir,
|
|
1717
2331
|
timeout,
|
|
1718
|
-
signal: innerSignal,
|
|
2332
|
+
signal: runtime.openSession && signal ? AbortSignal.any([innerSignal, signal]) : innerSignal,
|
|
1719
2333
|
model,
|
|
1720
2334
|
// Only the caller knows which session to continue; absent means a
|
|
1721
2335
|
// fresh one. It must stay `undefined` rather than `""` — the ACP
|
|
@@ -1766,7 +2380,8 @@ var AgentRunner = class {
|
|
|
1766
2380
|
);
|
|
1767
2381
|
}
|
|
1768
2382
|
}
|
|
1769
|
-
}
|
|
2383
|
+
};
|
|
2384
|
+
return await (this.sessionManager ? this.sessionManager.execute(runtime, task) : runtime.execute(task));
|
|
1770
2385
|
},
|
|
1771
2386
|
retries,
|
|
1772
2387
|
(attempt, err) => {
|
|
@@ -1783,7 +2398,7 @@ var AgentRunner = class {
|
|
|
1783
2398
|
});
|
|
1784
2399
|
},
|
|
1785
2400
|
DEFAULT_RETRY_DELAY_MS,
|
|
1786
|
-
innerSignal,
|
|
2401
|
+
runtime.openSession && signal ? AbortSignal.any([innerSignal, signal]) : innerSignal,
|
|
1787
2402
|
// A permanent failure (a missing binary, an unsupported capability)
|
|
1788
2403
|
// will fail the same way on every attempt, so let the loop fail fast
|
|
1789
2404
|
// instead of burning the whole backoff budget on a doomed run.
|
|
@@ -1791,17 +2406,25 @@ var AgentRunner = class {
|
|
|
1791
2406
|
),
|
|
1792
2407
|
retryBudgetMs(timeout + KILL_ESCALATION_MS, retries),
|
|
1793
2408
|
agent.name,
|
|
1794
|
-
|
|
2409
|
+
// Session runtimes acknowledge cancellation only after process cleanup.
|
|
2410
|
+
// Neither caller cancellation nor the outer timeout may race that barrier.
|
|
2411
|
+
runtime.openSession ? void 0 : signal,
|
|
2412
|
+
{ waitForCleanup: !!runtime.openSession }
|
|
1795
2413
|
);
|
|
1796
2414
|
output = completed.output;
|
|
1797
2415
|
usage = completed.usage;
|
|
1798
2416
|
costUsd = completed.costUsd;
|
|
1799
2417
|
const sessionId = completed.sessionId;
|
|
2418
|
+
const incomplete = completed.stopReason !== void 0 && completed.stopReason !== "success";
|
|
2419
|
+
const cancelled = completed.stopReason === "cancelled";
|
|
2420
|
+
const failed = completed.exitCode !== 0 || incomplete;
|
|
2421
|
+
const error = failed ? `Runtime turn did not complete (${completed.stopReason ?? `exit code ${completed.exitCode}`})` : void 0;
|
|
1800
2422
|
const endTime = /* @__PURE__ */ new Date();
|
|
1801
2423
|
const result = {
|
|
1802
2424
|
agentId: agent.id,
|
|
1803
2425
|
agentName: agent.name,
|
|
1804
|
-
status: "success",
|
|
2426
|
+
status: cancelled ? "cancelled" : failed ? "failed" : "success",
|
|
2427
|
+
...error ? { error } : {},
|
|
1805
2428
|
output,
|
|
1806
2429
|
duration: endTime.getTime() - startTime.getTime(),
|
|
1807
2430
|
attempts,
|
|
@@ -1813,7 +2436,7 @@ var AgentRunner = class {
|
|
|
1813
2436
|
...sessionId ? { sessionId } : {}
|
|
1814
2437
|
};
|
|
1815
2438
|
this.eventBus.emit({
|
|
1816
|
-
type: "agent:complete",
|
|
2439
|
+
type: failed ? "agent:failed" : "agent:complete",
|
|
1817
2440
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1818
2441
|
payload: result
|
|
1819
2442
|
});
|
|
@@ -1902,7 +2525,7 @@ function buildStages(agents2) {
|
|
|
1902
2525
|
|
|
1903
2526
|
// src/orchestrator/Orchestrator.ts
|
|
1904
2527
|
var Orchestrator = class {
|
|
1905
|
-
constructor(config) {
|
|
2528
|
+
constructor(config, sessions) {
|
|
1906
2529
|
this.config = config;
|
|
1907
2530
|
this.eventBus = new EventBus();
|
|
1908
2531
|
this.workDir = path5.resolve(config.workDir);
|
|
@@ -1910,11 +2533,22 @@ var Orchestrator = class {
|
|
|
1910
2533
|
this.contextManager = new ContextManager(config.context);
|
|
1911
2534
|
this.contextManager.set("conversation", renderConversation(config.history));
|
|
1912
2535
|
this.contextManager.set("upstreamOutputs", "");
|
|
1913
|
-
this.runner = new AgentRunner(
|
|
2536
|
+
this.runner = new AgentRunner(
|
|
2537
|
+
this.eventBus,
|
|
2538
|
+
this.contextManager,
|
|
2539
|
+
this.workDir,
|
|
2540
|
+
config,
|
|
2541
|
+
sessions
|
|
2542
|
+
);
|
|
1914
2543
|
}
|
|
1915
2544
|
async preview(options = {}) {
|
|
2545
|
+
const policyError = this.reviewPolicyError();
|
|
2546
|
+
if (policyError) throw new Error(policyError);
|
|
1916
2547
|
this.clearStaleTaskPlan();
|
|
1917
2548
|
const resolution = await this.resolveTurn(options.signal);
|
|
2549
|
+
if (resolution.kind === "cancelled" || options.signal?.aborted) {
|
|
2550
|
+
return { name: this.config.name, totalAgents: 0, stages: [], ...this.requirementField() };
|
|
2551
|
+
}
|
|
1918
2552
|
if (resolution.kind === "message") {
|
|
1919
2553
|
return {
|
|
1920
2554
|
name: this.config.name,
|
|
@@ -1927,10 +2561,12 @@ var Orchestrator = class {
|
|
|
1927
2561
|
const agents2 = resolution.agents;
|
|
1928
2562
|
this.contextManager.set("pipelineAgents", agents2.map((a) => `${a.id}: ${a.name}`).join("\n"));
|
|
1929
2563
|
const stages = this.buildStagesOrFallback(agents2, "Could not order the configured agents");
|
|
2564
|
+
const reviewer2 = this.config.agents.find((agent) => agent.id === this.config.review?.reviewer);
|
|
2565
|
+
if (reviewer2 && agents2.length) stages.push([reviewer2]);
|
|
1930
2566
|
const stageInfos = this.stageInfos(stages);
|
|
1931
2567
|
return {
|
|
1932
2568
|
name: this.config.name,
|
|
1933
|
-
totalAgents: agents2.length,
|
|
2569
|
+
totalAgents: agents2.length + (reviewer2 && agents2.length ? 1 : 0),
|
|
1934
2570
|
stages: stageInfos,
|
|
1935
2571
|
...this.reasonField(resolution.reason),
|
|
1936
2572
|
...this.requirementField()
|
|
@@ -1938,13 +2574,39 @@ var Orchestrator = class {
|
|
|
1938
2574
|
}
|
|
1939
2575
|
async run(options = {}) {
|
|
1940
2576
|
const startTime = /* @__PURE__ */ new Date();
|
|
2577
|
+
if (options.signal?.aborted) return this.finishCancelled(startTime);
|
|
2578
|
+
const policyError = this.reviewPolicyError();
|
|
2579
|
+
if (policyError) {
|
|
2580
|
+
this.warn(policyError);
|
|
2581
|
+
const result2 = this.finishEmpty(startTime, {
|
|
2582
|
+
status: "failed",
|
|
2583
|
+
reviewer: this.config.review?.reviewer ?? "",
|
|
2584
|
+
repairRounds: 0,
|
|
2585
|
+
reviews: [],
|
|
2586
|
+
reason: policyError
|
|
2587
|
+
});
|
|
2588
|
+
return result2;
|
|
2589
|
+
}
|
|
1941
2590
|
this.clearStaleTaskPlan();
|
|
1942
2591
|
const resolution = await this.resolveTurn(options.signal);
|
|
2592
|
+
if (resolution.kind === "cancelled" || options.signal?.aborted) {
|
|
2593
|
+
return this.finishCancelled(startTime);
|
|
2594
|
+
}
|
|
1943
2595
|
if (resolution.kind === "message") {
|
|
1944
2596
|
return this.finishWithMessage(resolution.mode, resolution.message, startTime);
|
|
1945
2597
|
}
|
|
1946
2598
|
const agents2 = resolution.agents;
|
|
1947
|
-
if (agents2.length === 0)
|
|
2599
|
+
if (agents2.length === 0)
|
|
2600
|
+
return this.finishEmpty(
|
|
2601
|
+
startTime,
|
|
2602
|
+
this.config.review ? {
|
|
2603
|
+
status: "failed",
|
|
2604
|
+
reviewer: this.config.review.reviewer,
|
|
2605
|
+
repairRounds: 0,
|
|
2606
|
+
reviews: [],
|
|
2607
|
+
reason: "No worker was selected for the review-gated task."
|
|
2608
|
+
} : void 0
|
|
2609
|
+
);
|
|
1948
2610
|
this.contextManager.set("pipelineAgents", agents2.map((a) => `${a.id}: ${a.name}`).join("\n"));
|
|
1949
2611
|
let stages = this.buildStagesOrFallback(agents2, "Could not order the configured agents");
|
|
1950
2612
|
const toStageInfos = (list) => this.stageInfos(list);
|
|
@@ -1953,7 +2615,7 @@ var Orchestrator = class {
|
|
|
1953
2615
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1954
2616
|
payload: {
|
|
1955
2617
|
name: this.config.name,
|
|
1956
|
-
requirement: this.
|
|
2618
|
+
requirement: this.contextManager.get("requirement"),
|
|
1957
2619
|
totalAgents: agents2.length,
|
|
1958
2620
|
stages: toStageInfos(stages),
|
|
1959
2621
|
...this.reasonField(resolution.reason)
|
|
@@ -1963,7 +2625,13 @@ var Orchestrator = class {
|
|
|
1963
2625
|
let allSuccess = true;
|
|
1964
2626
|
let cancelled = false;
|
|
1965
2627
|
let planApplied = false;
|
|
2628
|
+
const executedIds = /* @__PURE__ */ new Set();
|
|
1966
2629
|
for (let i = 0; i < stages.length; i++) {
|
|
2630
|
+
if (options.signal?.aborted) {
|
|
2631
|
+
cancelled = true;
|
|
2632
|
+
allSuccess = false;
|
|
2633
|
+
break;
|
|
2634
|
+
}
|
|
1967
2635
|
const stage = stages[i];
|
|
1968
2636
|
this.eventBus.emit({
|
|
1969
2637
|
type: "stage:start",
|
|
@@ -1972,24 +2640,32 @@ var Orchestrator = class {
|
|
|
1972
2640
|
});
|
|
1973
2641
|
const stageResults = await this.runStage(stage, options.signal);
|
|
1974
2642
|
results.push(...stageResults);
|
|
2643
|
+
for (const result2 of stageResults) executedIds.add(result2.agentId);
|
|
1975
2644
|
this.contextManager.set(
|
|
1976
2645
|
"upstreamOutputs",
|
|
1977
2646
|
renderUpstreamOutputs(
|
|
1978
2647
|
results.filter((r) => r.status === "success").map((r) => ({ agentId: r.agentId, name: r.agentName, output: r.output }))
|
|
1979
2648
|
)
|
|
1980
2649
|
);
|
|
1981
|
-
if (options.signal?.aborted) {
|
|
2650
|
+
if (options.signal?.aborted || stageResults.some((r) => r.status === "cancelled")) {
|
|
1982
2651
|
cancelled = true;
|
|
1983
2652
|
allSuccess = false;
|
|
1984
2653
|
break;
|
|
1985
2654
|
}
|
|
1986
|
-
if (!planApplied && stage.some((a) => this.isPlanner(a)) && stageResults.some(
|
|
2655
|
+
if (!planApplied && stage.some((a) => this.isPlanner(a)) && stageResults.some(
|
|
2656
|
+
(r) => r.status === "success" && stage.some((a) => a.id === r.agentId && this.isPlanner(a))
|
|
2657
|
+
)) {
|
|
1987
2658
|
planApplied = true;
|
|
1988
|
-
await this.applyTaskPlan(agents2);
|
|
1989
|
-
|
|
1990
|
-
|
|
2659
|
+
await this.applyTaskPlan(agents2, executedIds);
|
|
2660
|
+
const remaining = agents2.filter((a) => !executedIds.has(a.id)).map((a) => ({
|
|
2661
|
+
...a,
|
|
2662
|
+
dependsOn: a.dependsOn?.filter((dep) => !executedIds.has(dep))
|
|
2663
|
+
}));
|
|
2664
|
+
const rebuilt = this.buildStagesOrFallback(
|
|
2665
|
+
remaining,
|
|
1991
2666
|
"The task plan's dependencies could not be satisfied"
|
|
1992
2667
|
);
|
|
2668
|
+
stages = [...stages.slice(0, i + 1), ...rebuilt];
|
|
1993
2669
|
this.eventBus.emit({
|
|
1994
2670
|
type: "orchestration:stages",
|
|
1995
2671
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2020,11 +2696,25 @@ var Orchestrator = class {
|
|
|
2020
2696
|
break;
|
|
2021
2697
|
}
|
|
2022
2698
|
}
|
|
2699
|
+
let acceptance;
|
|
2700
|
+
if (this.config.review) {
|
|
2701
|
+
acceptance = await this.reviewDelivery(
|
|
2702
|
+
agents2,
|
|
2703
|
+
results,
|
|
2704
|
+
stages,
|
|
2705
|
+
allSuccess,
|
|
2706
|
+
cancelled,
|
|
2707
|
+
options.signal
|
|
2708
|
+
);
|
|
2709
|
+
cancelled ||= acceptance.status === "cancelled";
|
|
2710
|
+
allSuccess = allSuccess && acceptance.status === "approved";
|
|
2711
|
+
}
|
|
2023
2712
|
const endTime = /* @__PURE__ */ new Date();
|
|
2024
2713
|
const totalCostUsd = sumCosts(results);
|
|
2025
2714
|
const result = {
|
|
2026
2715
|
name: this.config.name,
|
|
2027
2716
|
success: allSuccess,
|
|
2717
|
+
...acceptance ? { acceptance } : {},
|
|
2028
2718
|
totalDuration: endTime.getTime() - startTime.getTime(),
|
|
2029
2719
|
agentResults: results,
|
|
2030
2720
|
startTime: startTime.toISOString(),
|
|
@@ -2041,6 +2731,137 @@ var Orchestrator = class {
|
|
|
2041
2731
|
});
|
|
2042
2732
|
return result;
|
|
2043
2733
|
}
|
|
2734
|
+
reviewPolicyError() {
|
|
2735
|
+
const policy = this.config.review;
|
|
2736
|
+
if (!policy) return void 0;
|
|
2737
|
+
const ids = this.config.agents.map((agent) => agent.id);
|
|
2738
|
+
if (new Set(ids).size !== ids.length) return "Review policy requires unique agent ids.";
|
|
2739
|
+
if (!ids.includes(policy.reviewer) || policy.reviewer === this.config.leader?.id) {
|
|
2740
|
+
return "Review policy must name a configured non-leader reviewer.";
|
|
2741
|
+
}
|
|
2742
|
+
if (!Array.isArray(policy.repairAgents) || !policy.repairAgents.length || new Set(policy.repairAgents).size !== policy.repairAgents.length || policy.repairAgents.some(
|
|
2743
|
+
(id) => !ids.includes(id) || id === policy.reviewer || id === this.config.leader?.id
|
|
2744
|
+
)) {
|
|
2745
|
+
return "Review policy must name unique configured repair workers, excluding the reviewer and leader.";
|
|
2746
|
+
}
|
|
2747
|
+
const limit = policy.maxRepairRounds ?? 1;
|
|
2748
|
+
if (!Number.isInteger(limit) || limit < 0 || limit > 5) {
|
|
2749
|
+
return "Review policy maxRepairRounds must be an integer from 0 to 5.";
|
|
2750
|
+
}
|
|
2751
|
+
return void 0;
|
|
2752
|
+
}
|
|
2753
|
+
async reviewDelivery(agents2, results, stages, executionSuccess, cancelled, signal) {
|
|
2754
|
+
const policy = this.config.review;
|
|
2755
|
+
if (!policy) throw new Error("Delivery review requires a configured policy.");
|
|
2756
|
+
const acceptance = {
|
|
2757
|
+
status: "failed",
|
|
2758
|
+
reviewer: policy.reviewer,
|
|
2759
|
+
repairRounds: 0,
|
|
2760
|
+
reviews: []
|
|
2761
|
+
};
|
|
2762
|
+
if (cancelled || signal?.aborted) return { ...acceptance, status: "cancelled" };
|
|
2763
|
+
if (!executionSuccess)
|
|
2764
|
+
return { ...acceptance, reason: "Pipeline execution failed; delivery was not reviewed." };
|
|
2765
|
+
const reviewer2 = this.config.agents.find((agent) => agent.id === policy.reviewer);
|
|
2766
|
+
if (!reviewer2) return { ...acceptance, reason: "Configured reviewer is unavailable." };
|
|
2767
|
+
const repairs = agents2.filter((agent) => policy.repairAgents.includes(agent.id));
|
|
2768
|
+
const repairIds = new Set(repairs.map((agent) => agent.id));
|
|
2769
|
+
const repairStages = this.buildStagesOrFallback(
|
|
2770
|
+
repairs.map((agent) => ({
|
|
2771
|
+
...agent,
|
|
2772
|
+
dependsOn: agent.dependsOn?.filter((id) => repairIds.has(id))
|
|
2773
|
+
})),
|
|
2774
|
+
"Could not order repair workers"
|
|
2775
|
+
).flatMap((stage) => stage.map((agent) => [agent]));
|
|
2776
|
+
let stageIndex = stages.length;
|
|
2777
|
+
const execute = async (stage, instruction) => {
|
|
2778
|
+
if (signal?.aborted) {
|
|
2779
|
+
acceptance.status = "cancelled";
|
|
2780
|
+
return false;
|
|
2781
|
+
}
|
|
2782
|
+
stages.push(stage);
|
|
2783
|
+
this.eventBus.emit({
|
|
2784
|
+
type: "orchestration:stages",
|
|
2785
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2786
|
+
payload: {
|
|
2787
|
+
totalAgents: new Set(stages.flat().map((agent) => agent.id)).size,
|
|
2788
|
+
stages: this.stageInfos(stages)
|
|
2789
|
+
}
|
|
2790
|
+
});
|
|
2791
|
+
const index = stageIndex++;
|
|
2792
|
+
this.eventBus.emit({
|
|
2793
|
+
type: "stage:start",
|
|
2794
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2795
|
+
payload: { stageIndex: index, agentIds: stage.map((agent) => agent.id) }
|
|
2796
|
+
});
|
|
2797
|
+
for (const agent of stage) {
|
|
2798
|
+
if (signal?.aborted) {
|
|
2799
|
+
acceptance.status = "cancelled";
|
|
2800
|
+
return false;
|
|
2801
|
+
}
|
|
2802
|
+
const result = await this.runner.run(
|
|
2803
|
+
agent,
|
|
2804
|
+
signal,
|
|
2805
|
+
`${instruction}
|
|
2806
|
+
|
|
2807
|
+
## Latest worker outputs
|
|
2808
|
+
${this.contextManager.get("upstreamOutputs") ?? ""}`
|
|
2809
|
+
);
|
|
2810
|
+
results.push(result);
|
|
2811
|
+
const latest = /* @__PURE__ */ new Map();
|
|
2812
|
+
for (const item of results) latest.set(item.agentId, item);
|
|
2813
|
+
this.contextManager.set(
|
|
2814
|
+
"upstreamOutputs",
|
|
2815
|
+
renderUpstreamOutputs(
|
|
2816
|
+
[...latest.values()].filter((item) => item.status === "success" && item.agentId !== policy.reviewer).map((item) => ({ agentId: item.agentId, name: item.agentName, output: item.output }))
|
|
2817
|
+
)
|
|
2818
|
+
);
|
|
2819
|
+
if (result.status !== "success" || signal?.aborted) {
|
|
2820
|
+
acceptance.status = result.status === "cancelled" || signal?.aborted ? "cancelled" : "failed";
|
|
2821
|
+
acceptance.reason = result.error ?? `${agent.name} did not complete.`;
|
|
2822
|
+
return false;
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
this.eventBus.emit({
|
|
2826
|
+
type: "stage:complete",
|
|
2827
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2828
|
+
payload: { stageIndex: index }
|
|
2829
|
+
});
|
|
2830
|
+
return true;
|
|
2831
|
+
};
|
|
2832
|
+
while (true) {
|
|
2833
|
+
if (!await execute(
|
|
2834
|
+
[reviewer2],
|
|
2835
|
+
`## Delivery acceptance review
|
|
2836
|
+
Inspect the current workspace against the complete task and verify the changes. Do not repair files. Return ONLY a JSON object with this exact contract:
|
|
2837
|
+
{"verdict":"approved"|"changes_required","summary":"non-empty explanation","issues":["actionable unresolved issue"]}
|
|
2838
|
+
Approval requires an empty issues array; changes_required requires at least one issue. A successful process exit is not approval.`
|
|
2839
|
+
))
|
|
2840
|
+
return acceptance;
|
|
2841
|
+
const verdict = parseReviewVerdict(results[results.length - 1].output);
|
|
2842
|
+
if (!verdict) {
|
|
2843
|
+
acceptance.reason = "Reviewer returned an invalid delivery verdict; acceptance is blocked.";
|
|
2844
|
+
this.warn(acceptance.reason);
|
|
2845
|
+
return acceptance;
|
|
2846
|
+
}
|
|
2847
|
+
acceptance.reviews.push(verdict);
|
|
2848
|
+
if (verdict.verdict === "approved") return { ...acceptance, status: "approved" };
|
|
2849
|
+
acceptance.status = "changes_required";
|
|
2850
|
+
if (acceptance.repairRounds >= (policy.maxRepairRounds ?? 1) || !repairs.length) {
|
|
2851
|
+
acceptance.reason = !repairs.length ? "No configured repair worker was selected for this task." : "Repair budget exhausted.";
|
|
2852
|
+
return acceptance;
|
|
2853
|
+
}
|
|
2854
|
+
acceptance.repairRounds++;
|
|
2855
|
+
this.warn(
|
|
2856
|
+
`Delivery requires changes \u2014 repair pass ${acceptance.repairRounds}/${policy.maxRepairRounds ?? 1}.`
|
|
2857
|
+
);
|
|
2858
|
+
const instruction = `## Repair requested by delivery review
|
|
2859
|
+
${JSON.stringify(verdict)}
|
|
2860
|
+
Address these findings within your assigned scope, preserve the complete task requirements, and report what you changed and verified.`;
|
|
2861
|
+
for (const stage of repairStages) if (!await execute(stage, instruction)) return acceptance;
|
|
2862
|
+
acceptance.status = "failed";
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2044
2865
|
stageInfos(stages) {
|
|
2045
2866
|
const getModel = (agent) => resolveModel(agent, this.config);
|
|
2046
2867
|
return stages.map((stage, i) => ({
|
|
@@ -2069,7 +2890,7 @@ var Orchestrator = class {
|
|
|
2069
2890
|
return buildStages(agents2);
|
|
2070
2891
|
} catch (err) {
|
|
2071
2892
|
this.warn(`${context} (${errorMessage2(err)}) \u2014 running the agents in declaration order.`);
|
|
2072
|
-
return [
|
|
2893
|
+
return agents2.map((agent) => [agent]);
|
|
2073
2894
|
}
|
|
2074
2895
|
}
|
|
2075
2896
|
/**
|
|
@@ -2092,6 +2913,7 @@ var Orchestrator = class {
|
|
|
2092
2913
|
* job, and that is what `message` records.
|
|
2093
2914
|
*/
|
|
2094
2915
|
async resolveTurn(signal) {
|
|
2916
|
+
if (signal?.aborted) return { kind: "cancelled" };
|
|
2095
2917
|
if (this.config.resume) {
|
|
2096
2918
|
return {
|
|
2097
2919
|
kind: "run",
|
|
@@ -2103,9 +2925,10 @@ var Orchestrator = class {
|
|
|
2103
2925
|
}
|
|
2104
2926
|
this.contextManager.set(
|
|
2105
2927
|
"availableAgents",
|
|
2106
|
-
this.
|
|
2928
|
+
this.configuredAgents().map((a) => `${a.id}: ${a.name}`).join("\n")
|
|
2107
2929
|
);
|
|
2108
2930
|
const result = await this.runner.run(this.config.leader, signal);
|
|
2931
|
+
if (signal?.aborted || result.status === "cancelled") return { kind: "cancelled" };
|
|
2109
2932
|
if (result.status !== "success") {
|
|
2110
2933
|
this.warn("The leader could not be reached \u2014 running the full configured team.");
|
|
2111
2934
|
return { kind: "run", agents: this.configuredAgents() };
|
|
@@ -2115,6 +2938,7 @@ var Orchestrator = class {
|
|
|
2115
2938
|
if (decision.mode !== "run") {
|
|
2116
2939
|
return { kind: "message", mode: decision.mode, message: decision.message };
|
|
2117
2940
|
}
|
|
2941
|
+
if (decision.requirement) this.contextManager.set("requirement", decision.requirement);
|
|
2118
2942
|
return { kind: "run", agents: this.selectAgents(decision.run), reason: decision.reason };
|
|
2119
2943
|
}
|
|
2120
2944
|
const message = result.output.trim();
|
|
@@ -2134,8 +2958,11 @@ var Orchestrator = class {
|
|
|
2134
2958
|
* prevent, but for the in-memory fields rather than the file.
|
|
2135
2959
|
*/
|
|
2136
2960
|
configuredAgents() {
|
|
2137
|
-
return this.config.agents.map(
|
|
2138
|
-
(a) => a.dependsOn ? {
|
|
2961
|
+
return this.config.agents.filter((a) => a.id !== this.config.review?.reviewer).map(
|
|
2962
|
+
(a) => a.dependsOn ? {
|
|
2963
|
+
...a,
|
|
2964
|
+
dependsOn: a.dependsOn.filter((id) => id !== this.config.review?.reviewer)
|
|
2965
|
+
} : { ...a }
|
|
2139
2966
|
);
|
|
2140
2967
|
}
|
|
2141
2968
|
/**
|
|
@@ -2150,7 +2977,7 @@ var Orchestrator = class {
|
|
|
2150
2977
|
* reader looking in the wrong place.
|
|
2151
2978
|
*/
|
|
2152
2979
|
selectAgents(run, namedBy = "The leader") {
|
|
2153
|
-
const runSet = new Set(run);
|
|
2980
|
+
const runSet = new Set(run.filter((id) => id !== this.config.review?.reviewer));
|
|
2154
2981
|
const known = new Set(this.config.agents.map((a) => a.id));
|
|
2155
2982
|
for (const id of run) {
|
|
2156
2983
|
if (!known.has(id)) {
|
|
@@ -2158,34 +2985,41 @@ var Orchestrator = class {
|
|
|
2158
2985
|
}
|
|
2159
2986
|
}
|
|
2160
2987
|
const skippedIds = new Set(this.config.agents.filter((a) => !runSet.has(a.id)).map((a) => a.id));
|
|
2161
|
-
for (const agent of this.config.agents.filter(
|
|
2988
|
+
for (const agent of this.config.agents.filter(
|
|
2989
|
+
(a) => skippedIds.has(a.id) && a.id !== this.config.review?.reviewer
|
|
2990
|
+
)) {
|
|
2162
2991
|
this.eventBus.emit({
|
|
2163
2992
|
type: "agent:skipped",
|
|
2164
2993
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2165
2994
|
payload: { agentId: agent.id, agentName: agent.name, icon: agent.icon }
|
|
2166
2995
|
});
|
|
2167
2996
|
}
|
|
2168
|
-
return this.config.agents.filter((a) => runSet.has(a.id)).map((a) => ({
|
|
2997
|
+
return this.config.agents.filter((a) => runSet.has(a.id) && a.id !== this.config.review?.reviewer).map((a) => ({
|
|
2169
2998
|
...a,
|
|
2170
|
-
dependsOn: a.dependsOn?.filter(
|
|
2999
|
+
dependsOn: a.dependsOn?.filter(
|
|
3000
|
+
(dep) => !skippedIds.has(dep) && dep !== this.config.review?.reviewer
|
|
3001
|
+
)
|
|
2171
3002
|
}));
|
|
2172
3003
|
}
|
|
2173
3004
|
async runStage(agents2, signal) {
|
|
2174
3005
|
const { maxConcurrency } = this.config;
|
|
2175
|
-
|
|
2176
|
-
return Promise.all(agents2.map((a) => this.runner.run(a, signal)));
|
|
2177
|
-
}
|
|
2178
|
-
const results = new Array(agents2.length);
|
|
3006
|
+
const results = /* @__PURE__ */ new Map();
|
|
2179
3007
|
let next = 0;
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
3008
|
+
let cancelled = false;
|
|
3009
|
+
const workers = Array.from(
|
|
3010
|
+
{ length: Math.min(maxConcurrency || agents2.length, agents2.length) },
|
|
3011
|
+
async () => {
|
|
3012
|
+
while (next < agents2.length && !signal?.aborted && !cancelled) {
|
|
3013
|
+
const index = next;
|
|
3014
|
+
next += 1;
|
|
3015
|
+
const result = await this.runner.run(agents2[index], signal);
|
|
3016
|
+
results.set(index, result);
|
|
3017
|
+
if (result.status === "cancelled") cancelled = true;
|
|
3018
|
+
}
|
|
2185
3019
|
}
|
|
2186
|
-
|
|
3020
|
+
);
|
|
2187
3021
|
await Promise.all(workers);
|
|
2188
|
-
return results;
|
|
3022
|
+
return [...results.entries()].sort(([a], [b]) => a - b).map(([, result]) => result);
|
|
2189
3023
|
}
|
|
2190
3024
|
/**
|
|
2191
3025
|
* Whether `agent` owns task decomposition.
|
|
@@ -2197,7 +3031,7 @@ var Orchestrator = class {
|
|
|
2197
3031
|
isPlanner(agent) {
|
|
2198
3032
|
return agent.role === "planner" || agent.id === "architect";
|
|
2199
3033
|
}
|
|
2200
|
-
async applyTaskPlan(agents2) {
|
|
3034
|
+
async applyTaskPlan(agents2, executedIds) {
|
|
2201
3035
|
const taskPlanPath = path5.join(this.workDir, "task-plan.json");
|
|
2202
3036
|
if (!fs4.existsSync(taskPlanPath)) return;
|
|
2203
3037
|
let taskPlan;
|
|
@@ -2209,16 +3043,37 @@ var Orchestrator = class {
|
|
|
2209
3043
|
);
|
|
2210
3044
|
return;
|
|
2211
3045
|
}
|
|
2212
|
-
if (!Array.isArray(taskPlan.tasks)) {
|
|
3046
|
+
if (typeof taskPlan !== "object" || taskPlan === null || !("tasks" in taskPlan) || !Array.isArray(taskPlan.tasks)) {
|
|
2213
3047
|
this.warn("task-plan.json has no `tasks` array \u2014 ignoring it.");
|
|
2214
3048
|
return;
|
|
2215
3049
|
}
|
|
2216
|
-
const taskMap = new Map(
|
|
3050
|
+
const taskMap = /* @__PURE__ */ new Map();
|
|
2217
3051
|
const selected = new Set(agents2.map((a) => a.id));
|
|
3052
|
+
for (const [index, entry3] of taskPlan.tasks.entries()) {
|
|
3053
|
+
if (typeof entry3 !== "object" || entry3 === null || Array.isArray(entry3)) {
|
|
3054
|
+
this.warn(`task-plan.json contains a malformed task at index ${index} \u2014 ignoring it.`);
|
|
3055
|
+
continue;
|
|
3056
|
+
}
|
|
3057
|
+
const task = entry3;
|
|
3058
|
+
if (typeof task.id !== "string" || !task.id.trim() || task.title !== void 0 && typeof task.title !== "string" || task.scope !== void 0 && typeof task.scope !== "string" || task.dependsOn !== void 0 && (!Array.isArray(task.dependsOn) || !task.dependsOn.every((dep) => typeof dep === "string")) || task.plan !== void 0 && (!Array.isArray(task.plan) || !task.plan.every((step) => typeof step === "string"))) {
|
|
3059
|
+
this.warn(`task-plan.json contains a malformed task at index ${index} \u2014 ignoring it.`);
|
|
3060
|
+
continue;
|
|
3061
|
+
}
|
|
3062
|
+
if (!selected.has(task.id)) {
|
|
3063
|
+
this.warn(
|
|
3064
|
+
`The task plan names "${task.id}", which is not in this run \u2014 ignoring that task.`
|
|
3065
|
+
);
|
|
3066
|
+
continue;
|
|
3067
|
+
}
|
|
3068
|
+
if (taskMap.has(task.id)) {
|
|
3069
|
+
this.warn(`The task plan contains a duplicate task for "${task.id}" \u2014 ignoring that task.`);
|
|
3070
|
+
continue;
|
|
3071
|
+
}
|
|
3072
|
+
taskMap.set(task.id, task);
|
|
3073
|
+
}
|
|
2218
3074
|
for (const agent of agents2) {
|
|
2219
3075
|
const task = taskMap.get(agent.id);
|
|
2220
3076
|
if (task) {
|
|
2221
|
-
agent.taskTitle = task.title;
|
|
2222
3077
|
const dependsOn = task.dependsOn?.filter((dep) => selected.has(dep));
|
|
2223
3078
|
for (const dep of task.dependsOn ?? []) {
|
|
2224
3079
|
if (!selected.has(dep)) {
|
|
@@ -2227,6 +3082,8 @@ var Orchestrator = class {
|
|
|
2227
3082
|
);
|
|
2228
3083
|
}
|
|
2229
3084
|
}
|
|
3085
|
+
if (executedIds.has(agent.id)) continue;
|
|
3086
|
+
agent.taskTitle = task.title;
|
|
2230
3087
|
agent.dependsOn = dependsOn;
|
|
2231
3088
|
agent.agentScope = task.scope;
|
|
2232
3089
|
if (task.plan?.length) {
|
|
@@ -2243,7 +3100,7 @@ var Orchestrator = class {
|
|
|
2243
3100
|
* and a requirement that never reaches the report cannot be resumed.
|
|
2244
3101
|
*/
|
|
2245
3102
|
requirementField() {
|
|
2246
|
-
const requirement = this.
|
|
3103
|
+
const requirement = this.contextManager.get("requirement");
|
|
2247
3104
|
return requirement !== void 0 ? { requirement } : {};
|
|
2248
3105
|
}
|
|
2249
3106
|
/**
|
|
@@ -2288,6 +3145,25 @@ var Orchestrator = class {
|
|
|
2288
3145
|
payload: { message }
|
|
2289
3146
|
});
|
|
2290
3147
|
}
|
|
3148
|
+
finishCancelled(startTime) {
|
|
3149
|
+
const endTime = /* @__PURE__ */ new Date();
|
|
3150
|
+
const result = {
|
|
3151
|
+
name: this.config.name,
|
|
3152
|
+
success: false,
|
|
3153
|
+
cancelled: true,
|
|
3154
|
+
totalDuration: endTime.getTime() - startTime.getTime(),
|
|
3155
|
+
agentResults: [],
|
|
3156
|
+
startTime: startTime.toISOString(),
|
|
3157
|
+
endTime: endTime.toISOString(),
|
|
3158
|
+
...this.requirementField()
|
|
3159
|
+
};
|
|
3160
|
+
this.eventBus.emit({
|
|
3161
|
+
type: "orchestration:complete",
|
|
3162
|
+
timestamp: endTime.toISOString(),
|
|
3163
|
+
payload: result
|
|
3164
|
+
});
|
|
3165
|
+
return result;
|
|
3166
|
+
}
|
|
2291
3167
|
/**
|
|
2292
3168
|
* Close out a turn that scheduled no agents.
|
|
2293
3169
|
*
|
|
@@ -2295,15 +3171,17 @@ var Orchestrator = class {
|
|
|
2295
3171
|
* only honest outcome is to say that none was done. Running the whole team
|
|
2296
3172
|
* instead would contradict the leader's explicit choice.
|
|
2297
3173
|
*/
|
|
2298
|
-
finishEmpty(startTime) {
|
|
3174
|
+
finishEmpty(startTime, acceptance) {
|
|
2299
3175
|
const usedLeader = this.config.leader !== void 0 && this.config.useLeader !== false;
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
3176
|
+
if (!acceptance?.reason)
|
|
3177
|
+
this.warn(
|
|
3178
|
+
usedLeader ? "The leader selected none of the configured agents \u2014 nothing to run." : "The config lists no agents \u2014 nothing to run."
|
|
3179
|
+
);
|
|
2303
3180
|
const endTime = /* @__PURE__ */ new Date();
|
|
2304
3181
|
const result = {
|
|
2305
3182
|
name: this.config.name,
|
|
2306
3183
|
success: false,
|
|
3184
|
+
...acceptance ? { acceptance } : {},
|
|
2307
3185
|
totalDuration: endTime.getTime() - startTime.getTime(),
|
|
2308
3186
|
agentResults: [],
|
|
2309
3187
|
startTime: startTime.toISOString(),
|
|
@@ -2318,6 +3196,31 @@ var Orchestrator = class {
|
|
|
2318
3196
|
return result;
|
|
2319
3197
|
}
|
|
2320
3198
|
};
|
|
3199
|
+
function parseReviewVerdict(output) {
|
|
3200
|
+
let text = output.trim();
|
|
3201
|
+
if (text.includes("```")) {
|
|
3202
|
+
const blocks = [...text.matchAll(/```(?:json)?\s*\n([\s\S]*?)\n```/g)];
|
|
3203
|
+
if (blocks.length !== 1 || !text.endsWith(blocks[0][0])) return void 0;
|
|
3204
|
+
const prefix = text.slice(0, blocks[0].index);
|
|
3205
|
+
if (prefix.includes("```") || /"verdict"\s*:/.test(prefix)) return void 0;
|
|
3206
|
+
text = blocks[0][1];
|
|
3207
|
+
}
|
|
3208
|
+
try {
|
|
3209
|
+
const value = JSON.parse(text);
|
|
3210
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
3211
|
+
const item = value;
|
|
3212
|
+
if (item.verdict !== "approved" && item.verdict !== "changes_required" || typeof item.summary !== "string" || !item.summary.trim() || !Array.isArray(item.issues) || !item.issues.every((issue) => typeof issue === "string" && issue.trim()))
|
|
3213
|
+
return void 0;
|
|
3214
|
+
if (item.verdict === "approved" !== (item.issues.length === 0)) return void 0;
|
|
3215
|
+
return {
|
|
3216
|
+
verdict: item.verdict,
|
|
3217
|
+
summary: item.summary.trim(),
|
|
3218
|
+
issues: item.issues.map((issue) => issue.trim())
|
|
3219
|
+
};
|
|
3220
|
+
} catch {
|
|
3221
|
+
return void 0;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
2321
3224
|
function errorMessage2(err) {
|
|
2322
3225
|
return err instanceof Error ? err.message : String(err);
|
|
2323
3226
|
}
|
|
@@ -2335,6 +3238,14 @@ function sumCosts(results) {
|
|
|
2335
3238
|
|
|
2336
3239
|
// src/providers/detect.ts
|
|
2337
3240
|
import { spawn as spawn2 } from "child_process";
|
|
3241
|
+
function parseVersion(raw) {
|
|
3242
|
+
if (!raw) return void 0;
|
|
3243
|
+
const plain = raw.replace(SGR_RE, "");
|
|
3244
|
+
const firstLine = plain.split("\n")[0].trim();
|
|
3245
|
+
if (!firstLine) return void 0;
|
|
3246
|
+
return firstLine.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/)?.[0] ?? firstLine;
|
|
3247
|
+
}
|
|
3248
|
+
var SGR_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
2338
3249
|
async function versionFor(command) {
|
|
2339
3250
|
return new Promise((resolve4) => {
|
|
2340
3251
|
let out = "";
|
|
@@ -2374,7 +3285,7 @@ async function detectProviders(opts) {
|
|
|
2374
3285
|
const v = await versionFor(exe);
|
|
2375
3286
|
if (v && spec.confirm(v)) {
|
|
2376
3287
|
matched = cmd;
|
|
2377
|
-
version = v;
|
|
3288
|
+
version = parseVersion(v);
|
|
2378
3289
|
source = bundled ? "bundled" : "path";
|
|
2379
3290
|
resolvedPath = bundled ? exe : void 0;
|
|
2380
3291
|
break;
|
|
@@ -2386,7 +3297,7 @@ async function detectProviders(opts) {
|
|
|
2386
3297
|
source = bundled ? "bundled" : "path";
|
|
2387
3298
|
resolvedPath = bundled ? exe : void 0;
|
|
2388
3299
|
if (opts?.withVersion) {
|
|
2389
|
-
version = await versionFor(exe);
|
|
3300
|
+
version = parseVersion(await versionFor(exe));
|
|
2390
3301
|
}
|
|
2391
3302
|
break;
|
|
2392
3303
|
}
|
|
@@ -2435,6 +3346,7 @@ function describeResumePlan({ agents: agents2, sessions = {}, team }) {
|
|
|
2435
3346
|
export {
|
|
2436
3347
|
ABORT_ERROR_NAME,
|
|
2437
3348
|
AcpRuntime,
|
|
3349
|
+
AgentSessionManager,
|
|
2438
3350
|
BRIDGE_PACKAGES,
|
|
2439
3351
|
EventBus,
|
|
2440
3352
|
Orchestrator,
|
|
@@ -2461,5 +3373,6 @@ export {
|
|
|
2461
3373
|
runtimeRegistry,
|
|
2462
3374
|
specForCommand,
|
|
2463
3375
|
specForProvider,
|
|
2464
|
-
tierModel
|
|
3376
|
+
tierModel,
|
|
3377
|
+
validateRuntimeQuestionContent
|
|
2465
3378
|
};
|