pi-agent-squad 0.8.0 → 0.8.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 +39 -12
- package/active-runs.ts +87 -0
- package/agents/actor.md +14 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents.ts +27 -6
- package/index.ts +425 -125
- package/message.ts +592 -111
- package/orchestrator.md +118 -134
- package/package.json +1 -1
- package/pool.ts +387 -167
- package/session-ui.ts +44 -3
- package/session.ts +7 -0
- package/spawn.ts +232 -69
package/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
3
4
|
import * as path from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { Type } from "typebox";
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
getCompactMarkdownTheme,
|
|
17
18
|
normalizeCompactCodeBlockLines,
|
|
18
19
|
} from "pi-compact-ui";
|
|
19
|
-
import { discoverAgents, type AgentConfig } from "./agents.ts";
|
|
20
|
+
import { discoverAgents, isSafeAgentName, type AgentConfig, userAgentsDir } from "./agents.ts";
|
|
20
21
|
import {
|
|
21
22
|
createMessageRouter,
|
|
22
23
|
ENV_ROLE,
|
|
@@ -25,6 +26,8 @@ import {
|
|
|
25
26
|
writeReply,
|
|
26
27
|
channelDir,
|
|
27
28
|
removeRequestFile,
|
|
29
|
+
ensureChannelRoot,
|
|
30
|
+
sweepMessageRoots,
|
|
28
31
|
type MessageRequest,
|
|
29
32
|
} from "./message.ts";
|
|
30
33
|
import { SubagentPool } from "./pool.ts";
|
|
@@ -32,8 +35,12 @@ import type { SubagentSessionHandle } from "./session.ts";
|
|
|
32
35
|
import { openSubagentSessionOverlay } from "./session-ui.ts";
|
|
33
36
|
import { getFinalOutput, spawnInteractiveSubagent } from "./spawn.ts";
|
|
34
37
|
import { deadlockMessage, MessageWaitGraph } from "./wait-graph.ts";
|
|
38
|
+
import { ActiveRunRegistry, type ActiveRun } from "./active-runs.ts";
|
|
35
39
|
|
|
36
|
-
const MESSAGE_ROOT_BASE =
|
|
40
|
+
const MESSAGE_ROOT_BASE = path.join(
|
|
41
|
+
os.tmpdir(),
|
|
42
|
+
`pi-subagents-${typeof process.getuid === "function" ? process.getuid() : "user"}-${randomUUID()}`,
|
|
43
|
+
);
|
|
37
44
|
const DEFAULT_SUBAGENT_TIMEOUT_SECONDS = 6 * 60 * 60;
|
|
38
45
|
const MIN_SUBAGENT_TIMEOUT_SECONDS = 10;
|
|
39
46
|
const MAX_SUBAGENT_TIMEOUT_SECONDS = 3 * 24 * 60 * 60;
|
|
@@ -56,6 +63,7 @@ interface IncomingMessageDetails {
|
|
|
56
63
|
|
|
57
64
|
interface BackgroundEventDetails {
|
|
58
65
|
agent?: string;
|
|
66
|
+
address?: string;
|
|
59
67
|
status?: "done" | "error";
|
|
60
68
|
body?: string;
|
|
61
69
|
elapsedMs?: number;
|
|
@@ -222,8 +230,8 @@ function readOrchestratorPrompt(): string {
|
|
|
222
230
|
|
|
223
231
|
/**
|
|
224
232
|
* Read the current session's adaptive-orchestration flag (the last entry wins).
|
|
225
|
-
* Adaptive orchestration is
|
|
226
|
-
* explicit per-session opt-
|
|
233
|
+
* Adaptive orchestration is disabled by default; `/orchestrate on` is an
|
|
234
|
+
* explicit per-session opt-in.
|
|
227
235
|
*/
|
|
228
236
|
function isOrchestratorMode(ctx: { sessionManager?: { getEntries?: () => unknown[] } }): boolean {
|
|
229
237
|
try {
|
|
@@ -232,7 +240,7 @@ function isOrchestratorMode(ctx: { sessionManager?: { getEntries?: () => unknown
|
|
|
232
240
|
customType?: string;
|
|
233
241
|
data?: { enabled?: boolean };
|
|
234
242
|
}>;
|
|
235
|
-
let enabled =
|
|
243
|
+
let enabled = false;
|
|
236
244
|
for (const e of entries) {
|
|
237
245
|
if (e.type === "custom" && e.customType === ORCHESTRATOR_MODE_ENTRY && typeof e.data?.enabled === "boolean") {
|
|
238
246
|
enabled = e.data.enabled;
|
|
@@ -240,7 +248,7 @@ function isOrchestratorMode(ctx: { sessionManager?: { getEntries?: () => unknown
|
|
|
240
248
|
}
|
|
241
249
|
return enabled;
|
|
242
250
|
} catch {
|
|
243
|
-
return
|
|
251
|
+
return false;
|
|
244
252
|
}
|
|
245
253
|
}
|
|
246
254
|
|
|
@@ -250,10 +258,15 @@ function agentFilePath(agentName: string): string {
|
|
|
250
258
|
return path.join(here, "agents", `${agentName}.md`);
|
|
251
259
|
}
|
|
252
260
|
|
|
261
|
+
function userAgentFilePath(agentName: string): string {
|
|
262
|
+
if (!isSafeAgentName(agentName)) throw new Error(`Invalid agent name "${agentName}".`);
|
|
263
|
+
return path.join(userAgentsDir(), `${agentName}.md`);
|
|
264
|
+
}
|
|
265
|
+
|
|
253
266
|
/** Available model ids (provider/model) for the current session */
|
|
254
267
|
function availableModelIds(ctx: {
|
|
255
|
-
scopedModels?:
|
|
256
|
-
modelRegistry?: { getAvailable?: () =>
|
|
268
|
+
scopedModels?: ReadonlyArray<{ model?: { provider?: string; id?: string } }>;
|
|
269
|
+
modelRegistry?: { getAvailable?: () => ReadonlyArray<{ provider?: string; id?: string }> };
|
|
257
270
|
}): string[] {
|
|
258
271
|
const scoped = ctx.scopedModels;
|
|
259
272
|
if (Array.isArray(scoped) && scoped.length > 0) {
|
|
@@ -269,18 +282,37 @@ function availableModelIds(ctx: {
|
|
|
269
282
|
|
|
270
283
|
/** Update a `model` or `thinking` line in an agent's frontmatter */
|
|
271
284
|
function updateAgentConfig(agentName: string, key: "model" | "thinking", value: string): boolean {
|
|
272
|
-
const
|
|
273
|
-
|
|
285
|
+
const builtinFile = agentFilePath(agentName);
|
|
286
|
+
const userFile = userAgentFilePath(agentName);
|
|
287
|
+
const sourceFile = fs.existsSync(userFile) ? userFile : builtinFile;
|
|
288
|
+
if (!fs.existsSync(sourceFile)) return false;
|
|
274
289
|
try {
|
|
275
|
-
let content = fs.readFileSync(
|
|
290
|
+
let content = fs.readFileSync(sourceFile, "utf-8");
|
|
291
|
+
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
292
|
+
if (!frontmatter) return false;
|
|
293
|
+
const header = frontmatter[1] ?? "";
|
|
276
294
|
const lineRe = new RegExp(`^(${key}:).*$`, "m");
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
295
|
+
const nextHeader = lineRe.test(header)
|
|
296
|
+
? header.replace(lineRe, `${key}: ${value}`)
|
|
297
|
+
: `${header}\n${key}: ${value}`;
|
|
298
|
+
content = `${content.slice(0, frontmatter.index!)}---\n${nextHeader.trim()}\n---${content.slice(frontmatter[0].length)}`;
|
|
299
|
+
const userDir = userAgentsDir();
|
|
300
|
+
fs.mkdirSync(userDir, { recursive: true, mode: 0o700 });
|
|
301
|
+
fs.chmodSync(userDir, 0o700);
|
|
302
|
+
const dirStat = fs.lstatSync(userDir);
|
|
303
|
+
if (typeof process.getuid === "function" && dirStat.uid !== process.getuid()) return false;
|
|
304
|
+
const tempFile = `${userFile}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
|
|
305
|
+
try {
|
|
306
|
+
fs.writeFileSync(tempFile, content, { encoding: "utf-8", mode: 0o600 });
|
|
307
|
+
fs.renameSync(tempFile, userFile);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
try {
|
|
310
|
+
fs.unlinkSync(tempFile);
|
|
311
|
+
} catch {
|
|
312
|
+
/* ignore cleanup failure */
|
|
313
|
+
}
|
|
314
|
+
throw error;
|
|
282
315
|
}
|
|
283
|
-
fs.writeFileSync(file, content, "utf-8");
|
|
284
316
|
return true;
|
|
285
317
|
} catch {
|
|
286
318
|
return false;
|
|
@@ -294,8 +326,9 @@ function updateAgentConfig(agentName: string, key: "model" | "thinking", value:
|
|
|
294
326
|
interface AsyncTask {
|
|
295
327
|
runId: string;
|
|
296
328
|
agent: string;
|
|
297
|
-
|
|
329
|
+
address: string;
|
|
298
330
|
startedAt: number;
|
|
331
|
+
sessionGeneration: number;
|
|
299
332
|
}
|
|
300
333
|
|
|
301
334
|
export interface RunningSubagentActivity {
|
|
@@ -577,8 +610,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
577
610
|
|
|
578
611
|
let cwd = process.cwd();
|
|
579
612
|
let messageRoot = sessionRoot("ephemeral");
|
|
613
|
+
let sessionGeneration = 0;
|
|
580
614
|
const tasks = new Map<string, AsyncTask>();
|
|
581
|
-
const
|
|
615
|
+
const runControllers = new Map<string, AbortController>();
|
|
616
|
+
let pool = new SubagentPool(messageRoot);
|
|
617
|
+
let poolDisposed = false;
|
|
618
|
+
const activeRuns = new ActiveRunRegistry();
|
|
582
619
|
const runningWidget = new RunningSubagentWidgetController();
|
|
583
620
|
const sessionHandles = new Map<string, SubagentSessionHandle>();
|
|
584
621
|
const waitGraph = new MessageWaitGraph();
|
|
@@ -589,9 +626,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
589
626
|
let sessionContext: any;
|
|
590
627
|
let terminalInputUnsubscribe: (() => void) | undefined;
|
|
591
628
|
let sessionOverlayOpen = false;
|
|
629
|
+
let closeSessionOverlay: (() => void) | undefined;
|
|
592
630
|
let pendingOpenActivityId: string | undefined;
|
|
593
631
|
let lastNavigation: { direction: -1 | 1; at: number } | undefined;
|
|
594
632
|
|
|
633
|
+
const bindPoolLifecycle = (): void => {
|
|
634
|
+
pool.setProcessExitHandler((agentName, runId) => {
|
|
635
|
+
const run = activeRuns.resolveExact(runId);
|
|
636
|
+
if (run?.agent === agentName) activeRuns.remove(run);
|
|
637
|
+
});
|
|
638
|
+
};
|
|
639
|
+
bindPoolLifecycle();
|
|
640
|
+
|
|
595
641
|
function moveWidgetSelection(direction: -1 | 1): void {
|
|
596
642
|
const now = Date.now();
|
|
597
643
|
if (
|
|
@@ -625,7 +671,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
625
671
|
pendingOpenActivityId = undefined;
|
|
626
672
|
runningWidget.clearSelection();
|
|
627
673
|
sessionOverlayOpen = true;
|
|
628
|
-
void openSubagentSessionOverlay(sessionContext, session)
|
|
674
|
+
void openSubagentSessionOverlay(sessionContext, session, (close) => {
|
|
675
|
+
closeSessionOverlay = close;
|
|
676
|
+
})
|
|
629
677
|
.catch((error) => {
|
|
630
678
|
sessionContext?.ui?.notify?.(
|
|
631
679
|
`Failed to open ${selected.agent} session: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -633,18 +681,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
633
681
|
);
|
|
634
682
|
})
|
|
635
683
|
.finally(() => {
|
|
684
|
+
closeSessionOverlay = undefined;
|
|
636
685
|
sessionOverlayOpen = false;
|
|
637
686
|
});
|
|
638
687
|
}
|
|
639
688
|
|
|
640
689
|
pi.on("session_start", (event, ctx) => {
|
|
690
|
+
sessionGeneration++;
|
|
691
|
+
if (poolDisposed) {
|
|
692
|
+
pool = new SubagentPool(messageRoot);
|
|
693
|
+
bindPoolLifecycle();
|
|
694
|
+
poolDisposed = false;
|
|
695
|
+
}
|
|
641
696
|
cwd = ctx.cwd;
|
|
642
697
|
const sessionId =
|
|
643
698
|
ctx.sessionManager?.getSessionId?.() ??
|
|
644
699
|
(event as any).sessionId ??
|
|
645
700
|
"ephemeral";
|
|
646
701
|
messageRoot = sessionRoot(sessionId);
|
|
647
|
-
|
|
702
|
+
ensureChannelRoot(MESSAGE_ROOT_BASE);
|
|
703
|
+
sweepMessageRoots(MESSAGE_ROOT_BASE);
|
|
704
|
+
ensureChannelRoot(messageRoot);
|
|
648
705
|
pool.setIntercomRoot(messageRoot);
|
|
649
706
|
pool.setWorkingDirectory(cwd);
|
|
650
707
|
runningWidget.attach(ctx);
|
|
@@ -677,11 +734,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
677
734
|
});
|
|
678
735
|
|
|
679
736
|
pi.on("session_shutdown", () => {
|
|
737
|
+
sessionGeneration++;
|
|
738
|
+
for (const controller of runControllers.values()) controller.abort();
|
|
739
|
+
runControllers.clear();
|
|
740
|
+
closeSessionOverlay?.();
|
|
741
|
+
closeSessionOverlay = undefined;
|
|
742
|
+
sessionOverlayOpen = false;
|
|
680
743
|
terminalInputUnsubscribe?.();
|
|
681
744
|
terminalInputUnsubscribe = undefined;
|
|
682
745
|
sessionContext = undefined;
|
|
683
746
|
pendingOpenActivityId = undefined;
|
|
684
747
|
sessionHandles.clear();
|
|
748
|
+
activeRuns.clear();
|
|
685
749
|
for (const pending of pendingMainReplyEdges.values()) {
|
|
686
750
|
if (pending.timer) clearTimeout(pending.timer);
|
|
687
751
|
pending.release();
|
|
@@ -690,6 +754,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
690
754
|
waitGraph.clear();
|
|
691
755
|
runningWidget.shutdown();
|
|
692
756
|
pool.dispose();
|
|
757
|
+
poolDisposed = true;
|
|
758
|
+
sweepMessageRoots(MESSAGE_ROOT_BASE);
|
|
693
759
|
});
|
|
694
760
|
|
|
695
761
|
function releaseMainReplyEdge(messageId: string): void {
|
|
@@ -713,12 +779,76 @@ export default function (pi: ExtensionAPI) {
|
|
|
713
779
|
pendingMainReplyEdges.set(msg.id, { release, timer });
|
|
714
780
|
}
|
|
715
781
|
|
|
716
|
-
// ---- message router: subagent -> main injects into main session;
|
|
782
|
+
// ---- message router: subagent -> main injects into main session; child
|
|
783
|
+
// messages resolve through the active-run registry before being delivered
|
|
784
|
+
// to a direct/background session or the resident pool. ----
|
|
717
785
|
let router: ReturnType<typeof createMessageRouter> | undefined;
|
|
786
|
+
const routedTimeout = (msg: MessageRequest): number =>
|
|
787
|
+
Math.min(
|
|
788
|
+
MAX_SUBAGENT_TIMEOUT_SECONDS * 1000,
|
|
789
|
+
Math.max(
|
|
790
|
+
MIN_SUBAGENT_TIMEOUT_SECONDS * 1000,
|
|
791
|
+
msg.timeoutMs ?? DEFAULT_SUBAGENT_TIMEOUT_SECONDS * 1000,
|
|
792
|
+
),
|
|
793
|
+
);
|
|
794
|
+
const routedPrompt = (msg: MessageRequest): string =>
|
|
795
|
+
[
|
|
796
|
+
`You received a message from ${msg.from === MAIN_AGENT ? "the main agent" : `subagent ${msg.from}`} (message id ${msg.id}):`,
|
|
797
|
+
"",
|
|
798
|
+
msg.content,
|
|
799
|
+
"",
|
|
800
|
+
`Process this message and give your reply. Your reply will be sent back to ${msg.from === MAIN_AGENT ? "the main agent" : msg.from}.`,
|
|
801
|
+
].join("\n");
|
|
802
|
+
const normalizeCwd = (value: string): string => {
|
|
803
|
+
const resolved = path.resolve(value);
|
|
804
|
+
try {
|
|
805
|
+
return fs.realpathSync(resolved);
|
|
806
|
+
} catch {
|
|
807
|
+
return resolved;
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Keep the resident pool in the same address registry as direct/background
|
|
813
|
+
* runs. The logical name remains the resident address; direct runs get a
|
|
814
|
+
* unique address such as actor#01ab23cd.
|
|
815
|
+
*/
|
|
816
|
+
function registerResidentRun(agent: AgentConfig): ActiveRun | undefined {
|
|
817
|
+
const residentPool = pool;
|
|
818
|
+
const identity = residentPool.getProcessIdentity(agent.name);
|
|
819
|
+
if (!identity) return undefined;
|
|
820
|
+
const address = agent.name;
|
|
821
|
+
const existing = activeRuns.resolveExact(address);
|
|
822
|
+
if (existing?.mode === "resident" && existing.runId === identity.runId) return existing;
|
|
823
|
+
if (existing?.mode === "resident") activeRuns.remove(existing);
|
|
824
|
+
const run: ActiveRun = {
|
|
825
|
+
runId: identity.runId,
|
|
826
|
+
agent: agent.name,
|
|
827
|
+
address,
|
|
828
|
+
mode: "resident",
|
|
829
|
+
readOnly: agent.readOnly === true,
|
|
830
|
+
cwd: normalizeCwd(cwd),
|
|
831
|
+
startedAt: Date.now(),
|
|
832
|
+
route: async (msg, signal) => {
|
|
833
|
+
const result = await residentPool.runTask(
|
|
834
|
+
agent,
|
|
835
|
+
routedPrompt(msg),
|
|
836
|
+
routedTimeout(msg),
|
|
837
|
+
undefined,
|
|
838
|
+
signal,
|
|
839
|
+
);
|
|
840
|
+
return getFinalOutput(result.messages) || `(subagent ${agent.name} gave no reply)`;
|
|
841
|
+
},
|
|
842
|
+
};
|
|
843
|
+
activeRuns.register(run);
|
|
844
|
+
return run;
|
|
845
|
+
}
|
|
846
|
+
|
|
718
847
|
pi.on("session_start", () => {
|
|
719
848
|
router?.dispose();
|
|
849
|
+
const routeRoot = messageRoot;
|
|
720
850
|
router = createMessageRouter(pi, {
|
|
721
|
-
root:
|
|
851
|
+
root: routeRoot,
|
|
722
852
|
matchesContext: () => true,
|
|
723
853
|
// to main: inject into the main session; the main model replies with reply_message
|
|
724
854
|
onMainMessage: (msg: MessageRequest) => {
|
|
@@ -726,7 +856,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
726
856
|
if (msg.expectsReply) {
|
|
727
857
|
const acquired = waitGraph.acquire(msg.from, MAIN_AGENT);
|
|
728
858
|
if (acquired.cycle) {
|
|
729
|
-
if (!writeReplyTo(msg, deadlockMessage(acquired.cycle))) {
|
|
859
|
+
if (!writeReplyTo(msg, deadlockMessage(acquired.cycle), routeRoot)) {
|
|
730
860
|
throw new Error("Failed to write deadlock-prevention reply.");
|
|
731
861
|
}
|
|
732
862
|
return;
|
|
@@ -764,6 +894,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
764
894
|
writeReplyTo(
|
|
765
895
|
msg,
|
|
766
896
|
`Failed to deliver message to main: ${error instanceof Error ? error.message : String(error)}`,
|
|
897
|
+
routeRoot,
|
|
767
898
|
);
|
|
768
899
|
}
|
|
769
900
|
throw error;
|
|
@@ -771,53 +902,71 @@ export default function (pi: ExtensionAPI) {
|
|
|
771
902
|
},
|
|
772
903
|
// to another subagent: route to its resident process, collect its reply, write it back
|
|
773
904
|
onChildMessage: async (msg: MessageRequest, signal?: AbortSignal) => {
|
|
905
|
+
const routePool = pool;
|
|
774
906
|
const agents = discoverAgents(cwd);
|
|
775
|
-
|
|
776
|
-
|
|
907
|
+
let targetRun = activeRuns.resolve(msg.to);
|
|
908
|
+
let target = targetRun
|
|
909
|
+
? agents.find((a) => a.name === targetRun?.agent)
|
|
910
|
+
: agents.find((a) => a.name === msg.to);
|
|
911
|
+
if (!target && targetRun) {
|
|
912
|
+
// The definition may have been reloaded while an existing
|
|
913
|
+
// run is still alive; its route is still unambiguous.
|
|
914
|
+
target = agents.find((a) => a.name === targetRun?.agent);
|
|
915
|
+
}
|
|
916
|
+
if (!target && !targetRun) {
|
|
777
917
|
const reply = `Unknown subagent "${msg.to}"`;
|
|
778
|
-
completeRoutedMessage(msg, reply);
|
|
918
|
+
completeRoutedMessage(msg, reply, routeRoot);
|
|
919
|
+
if (msg.from === MAIN_AGENT) throw new Error(reply);
|
|
920
|
+
return reply;
|
|
921
|
+
}
|
|
922
|
+
if (targetRun?.mode === "resident" && target) {
|
|
923
|
+
await pool.ensureProcess(target);
|
|
924
|
+
targetRun = registerResidentRun(target);
|
|
925
|
+
}
|
|
926
|
+
if (!targetRun && target) {
|
|
927
|
+
await pool.ensureProcess(target);
|
|
928
|
+
targetRun = registerResidentRun(target);
|
|
929
|
+
}
|
|
930
|
+
if (!targetRun) {
|
|
931
|
+
const reply = `Subagent "${msg.to}" has no active process.`;
|
|
932
|
+
completeRoutedMessage(msg, reply, routeRoot);
|
|
933
|
+
if (msg.from === MAIN_AGENT) throw new Error(reply);
|
|
934
|
+
return reply;
|
|
935
|
+
}
|
|
936
|
+
const resolvedTargetRun = targetRun;
|
|
937
|
+
const senderRun = activeRuns.findSender(msg);
|
|
938
|
+
if (senderRun?.runId === resolvedTargetRun.runId) {
|
|
939
|
+
const reply = `Cannot send a message to the same active run (${resolvedTargetRun.address}).`;
|
|
940
|
+
completeRoutedMessage(msg, reply, routeRoot);
|
|
779
941
|
if (msg.from === MAIN_AGENT) throw new Error(reply);
|
|
780
942
|
return reply;
|
|
781
943
|
}
|
|
782
944
|
let releaseWait: (() => void) | undefined;
|
|
783
945
|
if (msg.expectsReply) {
|
|
784
|
-
const acquired = waitGraph.acquire(msg.from,
|
|
946
|
+
const acquired = waitGraph.acquire(msg.from, resolvedTargetRun.address);
|
|
785
947
|
if (acquired.cycle) {
|
|
786
948
|
const reply = deadlockMessage(acquired.cycle);
|
|
787
|
-
completeRoutedMessage(msg, reply);
|
|
949
|
+
completeRoutedMessage(msg, reply, routeRoot);
|
|
788
950
|
if (msg.from === MAIN_AGENT) throw new Error(reply);
|
|
789
951
|
return reply;
|
|
790
952
|
}
|
|
791
953
|
releaseWait = acquired.release;
|
|
792
954
|
}
|
|
793
955
|
const activityId = `message:${msg.id}`;
|
|
794
|
-
runningWidget.start(activityId,
|
|
956
|
+
runningWidget.start(activityId, resolvedTargetRun.address, msg.content, "message");
|
|
795
957
|
try {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
[
|
|
805
|
-
`You received a message from ${msg.from === MAIN_AGENT ? "the main agent" : `subagent ${msg.from}`} (message id ${msg.id}):`,
|
|
806
|
-
``,
|
|
807
|
-
msg.content,
|
|
808
|
-
``,
|
|
809
|
-
`Process this message and give your reply. Your reply will be sent back to ${msg.from === MAIN_AGENT ? "the main agent" : msg.from}.`,
|
|
810
|
-
].join("\n"),
|
|
811
|
-
timeoutMs,
|
|
812
|
-
undefined,
|
|
813
|
-
signal,
|
|
814
|
-
);
|
|
815
|
-
const replyText = getFinalOutput(result.messages) || `(subagent ${msg.to} gave no reply)`;
|
|
816
|
-
completeRoutedMessage(msg, replyText);
|
|
958
|
+
if (resolvedTargetRun.session) {
|
|
959
|
+
const session = await resolvedTargetRun.session;
|
|
960
|
+
registerSessionHandle(activityId, session);
|
|
961
|
+
} else if (resolvedTargetRun.mode === "resident" && target) {
|
|
962
|
+
registerSessionHandle(activityId, routePool.getSessionHandle(target));
|
|
963
|
+
}
|
|
964
|
+
const replyText = await resolvedTargetRun.route(msg, signal);
|
|
965
|
+
completeRoutedMessage(msg, replyText, routeRoot);
|
|
817
966
|
return replyText;
|
|
818
967
|
} catch (e) {
|
|
819
|
-
const reply = `Target subagent failed to process the message: ${e instanceof Error ? e.message : String(e)}`;
|
|
820
|
-
completeRoutedMessage(msg, reply);
|
|
968
|
+
const reply = `Target subagent ${resolvedTargetRun.address} failed to process the message: ${e instanceof Error ? e.message : String(e)}`;
|
|
969
|
+
completeRoutedMessage(msg, reply, routeRoot);
|
|
821
970
|
if (msg.from === MAIN_AGENT) throw new Error(reply);
|
|
822
971
|
return reply;
|
|
823
972
|
} finally {
|
|
@@ -839,9 +988,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
839
988
|
});
|
|
840
989
|
|
|
841
990
|
/** Write a reply back to the message sender */
|
|
842
|
-
function writeReplyTo(msg: MessageRequest, content: string): boolean {
|
|
991
|
+
function writeReplyTo(msg: MessageRequest, content: string, root = messageRoot): boolean {
|
|
843
992
|
try {
|
|
844
|
-
const dir = channelDir(
|
|
993
|
+
const dir = channelDir(root, msg.fromRunId, msg.fromAgent, msg.fromChildIndex);
|
|
845
994
|
writeReply(dir, msg.id, content);
|
|
846
995
|
removeRequestFile(msg);
|
|
847
996
|
releaseMainReplyEdge(msg.id);
|
|
@@ -851,52 +1000,149 @@ export default function (pi: ExtensionAPI) {
|
|
|
851
1000
|
}
|
|
852
1001
|
}
|
|
853
1002
|
|
|
854
|
-
function completeRoutedMessage(msg: MessageRequest, content: string) {
|
|
1003
|
+
function completeRoutedMessage(msg: MessageRequest, content: string, root = messageRoot) {
|
|
855
1004
|
if (msg.from === MAIN_AGENT) return;
|
|
856
|
-
if (msg.expectsReply)
|
|
857
|
-
|
|
1005
|
+
if (msg.expectsReply) {
|
|
1006
|
+
if (!writeReplyTo(msg, content, root)) {
|
|
1007
|
+
throw new Error(`Failed to write reply for message ${msg.id}.`);
|
|
1008
|
+
}
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
removeRequestFile(msg);
|
|
1012
|
+
if (msg.requestFile && fs.existsSync(msg.requestFile)) {
|
|
1013
|
+
throw new Error(`Failed to remove request file for message ${msg.id}.`);
|
|
1014
|
+
}
|
|
858
1015
|
}
|
|
859
1016
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1017
|
+
function directRunAddress(agent: AgentConfig, runId: string): string {
|
|
1018
|
+
return `${agent.name}#${runId.slice(0, 8)}`;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function createDirectRun(
|
|
1022
|
+
agent: AgentConfig,
|
|
1023
|
+
runId: string,
|
|
1024
|
+
mode: "background" | "task",
|
|
1025
|
+
requestedAddress?: string,
|
|
1026
|
+
readOnly = agent.readOnly === true,
|
|
1027
|
+
runCwd = cwd,
|
|
1028
|
+
): {
|
|
1029
|
+
address: string;
|
|
1030
|
+
session: Promise<SubagentSessionHandle>;
|
|
1031
|
+
resolveSession: (session: SubagentSessionHandle) => void;
|
|
1032
|
+
rejectSession: (error: Error) => void;
|
|
1033
|
+
} {
|
|
1034
|
+
const address = requestedAddress?.trim() || directRunAddress(agent, runId);
|
|
1035
|
+
if (
|
|
1036
|
+
(requestedAddress &&
|
|
1037
|
+
(!/^[A-Za-z0-9_.-]+$/.test(address) ||
|
|
1038
|
+
address === "." ||
|
|
1039
|
+
address === ".." ||
|
|
1040
|
+
discoverAgents(cwd).some((candidate) => candidate.name === address) ||
|
|
1041
|
+
address === MAIN_AGENT)) ||
|
|
1042
|
+
address === MAIN_AGENT ||
|
|
1043
|
+
activeRuns.hasAddress(address)
|
|
1044
|
+
) {
|
|
1045
|
+
throw new Error(
|
|
1046
|
+
`Invalid or unavailable runtime address "${address}". Use a unique name matching [A-Za-z0-9_.-] that is not a logical agent name or "main".`,
|
|
871
1047
|
);
|
|
872
|
-
} catch {
|
|
873
|
-
/* ignore */
|
|
874
1048
|
}
|
|
1049
|
+
let resolveSession!: (session: SubagentSessionHandle) => void;
|
|
1050
|
+
let rejectSession!: (error: Error) => void;
|
|
1051
|
+
const session = new Promise<SubagentSessionHandle>((resolve, reject) => {
|
|
1052
|
+
resolveSession = resolve;
|
|
1053
|
+
rejectSession = reject;
|
|
1054
|
+
});
|
|
1055
|
+
// A run can fail before any message is addressed to it. Keep the
|
|
1056
|
+
// bookkeeping promise from becoming an unhandled rejection while still
|
|
1057
|
+
// propagating the error to any router that is waiting on it.
|
|
1058
|
+
void session.catch(() => {});
|
|
1059
|
+
let routeTail = Promise.resolve();
|
|
1060
|
+
const run: ActiveRun = {
|
|
1061
|
+
runId,
|
|
1062
|
+
agent: agent.name,
|
|
1063
|
+
address,
|
|
1064
|
+
mode,
|
|
1065
|
+
readOnly,
|
|
1066
|
+
cwd: normalizeCwd(runCwd),
|
|
1067
|
+
startedAt: Date.now(),
|
|
1068
|
+
session,
|
|
1069
|
+
route: async (msg, signal) => {
|
|
1070
|
+
let release!: () => void;
|
|
1071
|
+
const current = new Promise<void>((resolve) => (release = resolve));
|
|
1072
|
+
const previous = routeTail;
|
|
1073
|
+
routeTail = previous.catch(() => {}).then(() => current);
|
|
1074
|
+
await previous.catch(() => {});
|
|
1075
|
+
try {
|
|
1076
|
+
const handle = await session;
|
|
1077
|
+
if (!handle.sendAndWait) {
|
|
1078
|
+
throw new Error("The active run does not support routed messages.");
|
|
1079
|
+
}
|
|
1080
|
+
const reply = await handle.sendAndWait(routedPrompt(msg), routedTimeout(msg), signal);
|
|
1081
|
+
return reply || `(subagent ${address} gave no reply)`;
|
|
1082
|
+
} finally {
|
|
1083
|
+
release();
|
|
1084
|
+
}
|
|
1085
|
+
},
|
|
1086
|
+
};
|
|
1087
|
+
activeRuns.register(run);
|
|
1088
|
+
return { address, session, resolveSession, rejectSession };
|
|
875
1089
|
}
|
|
876
1090
|
|
|
877
|
-
function launchBackground(
|
|
1091
|
+
function launchBackground(
|
|
1092
|
+
agent: AgentConfig,
|
|
1093
|
+
taskText: string,
|
|
1094
|
+
cwdOverride?: string,
|
|
1095
|
+
timeoutMs?: number,
|
|
1096
|
+
requestedAddress?: string,
|
|
1097
|
+
readOnly = agent.readOnly === true,
|
|
1098
|
+
): { runId: string; address: string } {
|
|
878
1099
|
const runId = randomUUID();
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
runningWidget.start(runId, agent.name, taskText, "background");
|
|
882
|
-
|
|
883
|
-
spawnInteractiveSubagent({
|
|
1100
|
+
const controller = new AbortController();
|
|
1101
|
+
const direct = createDirectRun(
|
|
884
1102
|
agent,
|
|
885
|
-
task: taskText,
|
|
886
|
-
cwd: cwdOverride,
|
|
887
|
-
messageRoot,
|
|
888
1103
|
runId,
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1104
|
+
"background",
|
|
1105
|
+
requestedAddress,
|
|
1106
|
+
readOnly,
|
|
1107
|
+
cwdOverride ?? cwd,
|
|
1108
|
+
);
|
|
1109
|
+
runControllers.set(runId, controller);
|
|
1110
|
+
tasks.set(runId, {
|
|
1111
|
+
runId,
|
|
1112
|
+
agent: agent.name,
|
|
1113
|
+
address: direct.address,
|
|
1114
|
+
startedAt: Date.now(),
|
|
1115
|
+
sessionGeneration,
|
|
1116
|
+
});
|
|
1117
|
+
runningWidget.start(runId, agent.name, taskText, "background");
|
|
1118
|
+
|
|
1119
|
+
const start = async () => {
|
|
1120
|
+
return await spawnInteractiveSubagent({
|
|
1121
|
+
agent,
|
|
1122
|
+
task: taskText,
|
|
1123
|
+
address: direct.address,
|
|
1124
|
+
cwd: cwdOverride,
|
|
1125
|
+
messageRoot,
|
|
1126
|
+
runId,
|
|
1127
|
+
childIndex: 0,
|
|
1128
|
+
signal: controller.signal,
|
|
1129
|
+
timeoutMs,
|
|
1130
|
+
onSession: (session) => {
|
|
1131
|
+
direct.resolveSession(session);
|
|
1132
|
+
registerSessionHandle(runId, session);
|
|
1133
|
+
},
|
|
1134
|
+
});
|
|
1135
|
+
};
|
|
1136
|
+
start()
|
|
893
1137
|
.then((result) => {
|
|
1138
|
+
// spawnInteractiveSubagent stops its direct process before the
|
|
1139
|
+
// promise resolves; remove the address before publishing the
|
|
1140
|
+
// completion event so no new message can target a dead process.
|
|
1141
|
+
activeRuns.remove(runId);
|
|
894
1142
|
const task = tasks.get(runId);
|
|
895
|
-
if (!task) return;
|
|
1143
|
+
if (!task || task.sessionGeneration !== sessionGeneration) return;
|
|
896
1144
|
const finalStatus: "done" | "error" =
|
|
897
1145
|
result.exitCode === 0 && result.stopReason !== "error" ? "done" : "error";
|
|
898
|
-
task.status = finalStatus;
|
|
899
|
-
persistTasks();
|
|
900
1146
|
const output = getFinalOutput(result.messages) || "(no text output)";
|
|
901
1147
|
const failureReason =
|
|
902
1148
|
result.exitCode !== 0
|
|
@@ -925,6 +1171,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
925
1171
|
display: true,
|
|
926
1172
|
details: {
|
|
927
1173
|
agent: agent.name,
|
|
1174
|
+
address: direct.address,
|
|
928
1175
|
status: finalStatus,
|
|
929
1176
|
body: displayBody,
|
|
930
1177
|
elapsedMs: Date.now() - task.startedAt,
|
|
@@ -938,13 +1185,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
938
1185
|
}
|
|
939
1186
|
})
|
|
940
1187
|
.catch((err) => {
|
|
1188
|
+
activeRuns.remove(runId);
|
|
1189
|
+
direct.rejectSession(err instanceof Error ? err : new Error(String(err)));
|
|
941
1190
|
const task = tasks.get(runId);
|
|
942
|
-
if (task) {
|
|
943
|
-
task.status = "error";
|
|
944
|
-
persistTasks();
|
|
945
|
-
}
|
|
946
1191
|
const errorText = err instanceof Error ? err.message : String(err);
|
|
947
1192
|
try {
|
|
1193
|
+
if (task && task.sessionGeneration !== sessionGeneration) return;
|
|
948
1194
|
pi.sendMessage(
|
|
949
1195
|
{
|
|
950
1196
|
customType: BACKGROUND_EVENT_TYPE,
|
|
@@ -952,6 +1198,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
952
1198
|
display: true,
|
|
953
1199
|
details: {
|
|
954
1200
|
agent: agent.name,
|
|
1201
|
+
address: direct.address,
|
|
955
1202
|
status: "error",
|
|
956
1203
|
body: errorText,
|
|
957
1204
|
elapsedMs: task ? Date.now() - task.startedAt : undefined,
|
|
@@ -965,15 +1212,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
965
1212
|
}
|
|
966
1213
|
})
|
|
967
1214
|
.finally(() => {
|
|
1215
|
+
runControllers.delete(runId);
|
|
1216
|
+
tasks.delete(runId);
|
|
1217
|
+
activeRuns.remove(runId);
|
|
968
1218
|
if (pendingOpenActivityId === runId) pendingOpenActivityId = undefined;
|
|
969
1219
|
sessionHandles.delete(runId);
|
|
970
1220
|
runningWidget.finish(runId);
|
|
971
1221
|
});
|
|
972
1222
|
|
|
973
|
-
return runId;
|
|
1223
|
+
return { runId, address: direct.address };
|
|
974
1224
|
}
|
|
975
1225
|
|
|
976
|
-
// ---- adaptive orchestration prompt; `/orchestrate
|
|
1226
|
+
// ---- adaptive orchestration prompt; `/orchestrate on` installs the opt-in prompt ----
|
|
977
1227
|
pi.on("before_agent_start", (event, ctx) => {
|
|
978
1228
|
// Treat an explicitly appended orchestrator prompt (CLI
|
|
979
1229
|
// --append-system-prompt) as orchestrator mode too.
|
|
@@ -993,16 +1243,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
993
1243
|
label: "Subagent",
|
|
994
1244
|
description: [
|
|
995
1245
|
"Delegate a task to a subagent (isolated context, separate process).",
|
|
996
|
-
"agent: subagent
|
|
1246
|
+
"agent: logical subagent identity (defined in the agents directory, e.g. planner/reviewer/actor); task: task description; cwd: optional; as: optional unique runtime address for this process;",
|
|
997
1247
|
"async=true: run in background, return a runId immediately, inject the result into the conversation when done (non-blocking);",
|
|
998
1248
|
"async=false (default): wait synchronously for the result.",
|
|
999
1249
|
`timeoutSeconds: optional; omit unless the user explicitly requested a time. Default ${DEFAULT_SUBAGENT_TIMEOUT_SECONDS}s (6 hours), range ${MIN_SUBAGENT_TIMEOUT_SECONDS}-${MAX_SUBAGENT_TIMEOUT_SECONDS};`,
|
|
1000
|
-
"While running, a subagent may send_message (to=main) to reach you, or contact other subagents — reply promptly with reply_message.",
|
|
1250
|
+
"While running, a subagent may send_message (to=main) to reach you, or contact other subagents — reply promptly with reply_message. Synchronous tasks must use wait=false when contacting main.",
|
|
1001
1251
|
].join(" "),
|
|
1002
1252
|
parameters: Type.Object({
|
|
1003
1253
|
agent: Type.String({ description: "Subagent name" }),
|
|
1004
1254
|
task: Type.String({ description: "Task description for the subagent" }),
|
|
1005
1255
|
cwd: Type.Optional(Type.String({ description: "Working directory for the subagent" })),
|
|
1256
|
+
as: Type.Optional(
|
|
1257
|
+
Type.String({
|
|
1258
|
+
description: "Optional unique runtime address for this run (for example actor-a); identity still comes from agent",
|
|
1259
|
+
}),
|
|
1260
|
+
),
|
|
1261
|
+
readonly: Type.Optional(
|
|
1262
|
+
Type.Boolean({
|
|
1263
|
+
description: "Declare whether this run is read-only; used as orchestration metadata, not as a write sandbox",
|
|
1264
|
+
}),
|
|
1265
|
+
),
|
|
1006
1266
|
async: Type.Optional(Type.Boolean({ description: "true=run in background without blocking (default false)" })),
|
|
1007
1267
|
timeoutSeconds: Type.Optional(
|
|
1008
1268
|
Type.Integer({
|
|
@@ -1020,6 +1280,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1020
1280
|
const available = agents.map((a) => a.name).join(", ") || "none";
|
|
1021
1281
|
return {
|
|
1022
1282
|
content: [{ type: "text", text: `Unknown subagent "${params.agent}". Available: ${available}` }],
|
|
1283
|
+
details: undefined,
|
|
1023
1284
|
};
|
|
1024
1285
|
}
|
|
1025
1286
|
const timeoutSeconds = Math.min(
|
|
@@ -1030,48 +1291,88 @@ export default function (pi: ExtensionAPI) {
|
|
|
1030
1291
|
|
|
1031
1292
|
if (params.async) {
|
|
1032
1293
|
try {
|
|
1033
|
-
const
|
|
1294
|
+
const readOnly = params.readonly ?? agent.readOnly === true;
|
|
1295
|
+
const launched = launchBackground(agent, params.task, params.cwd, timeoutMs, params.as, readOnly);
|
|
1296
|
+
const { runId, address } = launched;
|
|
1034
1297
|
return {
|
|
1035
1298
|
content: [
|
|
1036
1299
|
{
|
|
1037
1300
|
type: "text",
|
|
1038
|
-
text: `Started background subagent ${params.agent} (run ${runId.slice(0, 8)}).
|
|
1301
|
+
text: `Started background subagent ${params.agent} at address ${address} (run ${runId.slice(0, 8)}). Send messages to that address to reach this exact process; the logical name ${params.agent} resolves to the newest active run.`,
|
|
1039
1302
|
},
|
|
1040
1303
|
],
|
|
1041
|
-
details: { mode: "async", runId },
|
|
1304
|
+
details: { mode: "async", runId, address },
|
|
1042
1305
|
};
|
|
1043
1306
|
} catch (e) {
|
|
1044
1307
|
return {
|
|
1045
1308
|
content: [{ type: "text", text: `Failed to start background subagent: ${e instanceof Error ? e.message : String(e)}` }],
|
|
1309
|
+
details: undefined,
|
|
1046
1310
|
};
|
|
1047
1311
|
}
|
|
1048
1312
|
}
|
|
1049
1313
|
|
|
1050
|
-
onUpdate?.({
|
|
1314
|
+
onUpdate?.({
|
|
1315
|
+
content: [{ type: "text", text: `Starting ${params.agent} subagent...` }],
|
|
1316
|
+
details: undefined,
|
|
1317
|
+
});
|
|
1051
1318
|
const runId = randomUUID();
|
|
1052
|
-
const
|
|
1319
|
+
const readOnly = params.readonly ?? agent.readOnly === true;
|
|
1320
|
+
const runController = new AbortController();
|
|
1321
|
+
const forwardAbort = () => runController.abort();
|
|
1322
|
+
runControllers.set(runId, runController);
|
|
1323
|
+
if (signal?.aborted) forwardAbort();
|
|
1324
|
+
else signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
1325
|
+
let direct: ReturnType<typeof createDirectRun>;
|
|
1326
|
+
try {
|
|
1327
|
+
direct = createDirectRun(agent, runId, "task", params.as, readOnly, params.cwd ?? cwd);
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
signal?.removeEventListener("abort", forwardAbort);
|
|
1330
|
+
runControllers.delete(runId);
|
|
1331
|
+
return {
|
|
1332
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
1333
|
+
details: undefined,
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
const acquired = waitGraph.acquire(MAIN_AGENT, direct.address);
|
|
1053
1337
|
if (acquired.cycle) {
|
|
1338
|
+
signal?.removeEventListener("abort", forwardAbort);
|
|
1339
|
+
runControllers.delete(runId);
|
|
1340
|
+
activeRuns.remove(runId);
|
|
1054
1341
|
return {
|
|
1055
1342
|
content: [{ type: "text", text: deadlockMessage(acquired.cycle) }],
|
|
1343
|
+
details: undefined,
|
|
1056
1344
|
};
|
|
1057
1345
|
}
|
|
1058
1346
|
runningWidget.start(runId, agent.name, params.task, "task");
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1347
|
+
let result: Awaited<ReturnType<typeof spawnInteractiveSubagent>>;
|
|
1348
|
+
try {
|
|
1349
|
+
result = await spawnInteractiveSubagent({
|
|
1350
|
+
agent,
|
|
1351
|
+
task: params.task,
|
|
1352
|
+
address: direct.address,
|
|
1353
|
+
cwd: params.cwd,
|
|
1354
|
+
messageRoot,
|
|
1355
|
+
runId,
|
|
1356
|
+
childIndex: 0,
|
|
1357
|
+
signal: runController.signal,
|
|
1358
|
+
timeoutMs,
|
|
1359
|
+
onSession: (session) => {
|
|
1360
|
+
direct.resolveSession(session);
|
|
1361
|
+
registerSessionHandle(runId, session);
|
|
1362
|
+
},
|
|
1363
|
+
});
|
|
1364
|
+
} catch (error) {
|
|
1365
|
+
direct.rejectSession(error instanceof Error ? error : new Error(String(error)));
|
|
1366
|
+
throw error;
|
|
1367
|
+
} finally {
|
|
1368
|
+
signal?.removeEventListener("abort", forwardAbort);
|
|
1369
|
+
runControllers.delete(runId);
|
|
1370
|
+
activeRuns.remove(runId);
|
|
1070
1371
|
acquired.release?.();
|
|
1071
1372
|
if (pendingOpenActivityId === runId) pendingOpenActivityId = undefined;
|
|
1072
1373
|
sessionHandles.delete(runId);
|
|
1073
1374
|
runningWidget.finish(runId);
|
|
1074
|
-
}
|
|
1375
|
+
}
|
|
1075
1376
|
const output = getFinalOutput(result.messages);
|
|
1076
1377
|
|
|
1077
1378
|
if (result.exitCode !== 0) {
|
|
@@ -1095,7 +1396,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1095
1396
|
|
|
1096
1397
|
// ---- commands ----
|
|
1097
1398
|
pi.registerCommand("orchestrate", {
|
|
1098
|
-
description: "Enable/disable adaptive subagent delegation (on|off|status, default
|
|
1399
|
+
description: "Enable/disable adaptive subagent delegation (on|off|status, default off)",
|
|
1099
1400
|
handler: async (args, ctx) => {
|
|
1100
1401
|
const first = Array.isArray(args) ? (args[0] ?? "on") : String(args ?? "on").trim().split(/\s+/)[0] ?? "on";
|
|
1101
1402
|
const arg = String(first).toLowerCase();
|
|
@@ -1132,7 +1433,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1132
1433
|
const agents = discoverAgents(ctx.cwd);
|
|
1133
1434
|
const lines = agents.map(
|
|
1134
1435
|
(a) =>
|
|
1135
|
-
`- ${a.name}: ${a.description}${a.model ? ` (${a.model})` : ""}${a.thinking ? ` [thinking: ${a.thinking}]` : ""}${a.tools ? ` [tools: ${a.tools.join(",")}]` : ""}`,
|
|
1436
|
+
`- ${a.name}: ${a.description}${a.readOnly === true ? " [read-only]" : " [writer]"}${a.model ? ` (${a.model})` : ""}${a.thinking ? ` [thinking: ${a.thinking}]` : ""}${a.tools ? ` [tools: ${a.tools.join(",")}]` : ""}`,
|
|
1136
1437
|
);
|
|
1137
1438
|
ctx.ui.notify(lines.length ? `Available subagents:\n${lines.join("\n")}` : "No subagents found", "info");
|
|
1138
1439
|
},
|
|
@@ -1161,6 +1462,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1161
1462
|
ctx.ui.notify(`No built-in agent named "${agentName}"`, "warning");
|
|
1162
1463
|
return;
|
|
1163
1464
|
}
|
|
1465
|
+
pool.restartAgent(agentName);
|
|
1164
1466
|
ctx.ui.notify(`Subagent ${agentName}: thinking -> ${level}`, "info");
|
|
1165
1467
|
} else {
|
|
1166
1468
|
const choices = availableModelIds(ctx);
|
|
@@ -1174,33 +1476,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
1174
1476
|
ctx.ui.notify(`No built-in agent named "${agentName}"`, "warning");
|
|
1175
1477
|
return;
|
|
1176
1478
|
}
|
|
1479
|
+
pool.restartAgent(agentName);
|
|
1177
1480
|
ctx.ui.notify(`Subagent ${agentName}: model -> ${model}`, "info");
|
|
1178
1481
|
}
|
|
1179
1482
|
},
|
|
1180
1483
|
});
|
|
1181
1484
|
|
|
1182
1485
|
pi.registerCommand("subagent-status", {
|
|
1183
|
-
description: "Show
|
|
1486
|
+
description: "Show active subagent runs and runtime addresses",
|
|
1184
1487
|
handler: async (_args, ctx) => {
|
|
1185
1488
|
const list = [...tasks.values()];
|
|
1186
1489
|
const lines = list.map((t) => {
|
|
1187
|
-
|
|
1188
|
-
|
|
1490
|
+
const age = Math.round((Date.now() - t.startedAt) / 1000);
|
|
1491
|
+
return `- ${t.address} run ${t.runId.slice(0, 8)} ${t.agent} running (${age}s ago)`;
|
|
1189
1492
|
});
|
|
1190
|
-
const
|
|
1493
|
+
const active = activeRuns.list();
|
|
1494
|
+
const activeLines = active.map(
|
|
1495
|
+
(run) => `- ${run.address} -> ${run.agent} [${run.mode}] (run ${run.runId.slice(0, 8)})`,
|
|
1496
|
+
);
|
|
1191
1497
|
ctx.ui.notify(
|
|
1192
1498
|
[
|
|
1193
1499
|
lines.length ? `Background tasks:\n${lines.join("\n")}` : "Background tasks: none",
|
|
1194
|
-
`
|
|
1500
|
+
activeLines.length ? `Active run registry:\n${activeLines.join("\n")}` : "Active run registry: none",
|
|
1195
1501
|
].join("\n"),
|
|
1196
1502
|
"info",
|
|
1197
1503
|
);
|
|
1198
1504
|
},
|
|
1199
1505
|
});
|
|
1200
1506
|
}
|
|
1201
|
-
|
|
1202
|
-
function* poolAliveNames(pool: SubagentPool): Generator<string> {
|
|
1203
|
-
for (const n of ["planner", "reviewer", "actor"]) {
|
|
1204
|
-
if (pool.isAlive(n)) yield n;
|
|
1205
|
-
}
|
|
1206
|
-
}
|