pi-agent-squad 0.7.0 → 0.8.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/README.md +48 -15
- 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 +437 -135
- package/message.ts +605 -112
- package/orchestrator.md +121 -124
- 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;
|
|
@@ -185,21 +193,19 @@ function sessionRoot(sessionId: string): string {
|
|
|
185
193
|
|
|
186
194
|
const ORCHESTRATOR_MODE_ENTRY = "orchestrator-mode";
|
|
187
195
|
const ORCHESTRATOR_PROMPT_MARKER = "# You are the Orchestrator";
|
|
188
|
-
const
|
|
189
|
-
"# Subagent Usage
|
|
196
|
+
const SUBAGENT_USAGE_DISABLED_GUARD = [
|
|
197
|
+
"# Subagent Usage Policy: Automatic Delegation Disabled",
|
|
190
198
|
"",
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
"2. Orchestrator mode is enabled.",
|
|
199
|
+
"Automatic subagent delegation has been explicitly disabled for this session.",
|
|
200
|
+
"Do not use subagents unless the user's current request explicitly asks you to use, call, spawn, delegate to, or communicate with one.",
|
|
194
201
|
"",
|
|
195
|
-
"When
|
|
202
|
+
"When the user has not explicitly requested subagent involvement:",
|
|
196
203
|
"- Do not call the `subagent` tool.",
|
|
197
204
|
"- Do not call `send_message` to contact or assign work to a subagent.",
|
|
198
205
|
"- Do not initiate or continue a planner/actor/reviewer workflow.",
|
|
199
206
|
"- Perform the task yourself using the normal tools available to the main agent.",
|
|
200
207
|
"",
|
|
201
|
-
"
|
|
202
|
-
"A generic request to plan, review, test, or implement something is not authorization unless the user explicitly requests subagent involvement.",
|
|
208
|
+
"The user can still explicitly request any combination of planner, actor, or reviewer while automatic delegation is disabled.",
|
|
203
209
|
].join("\n");
|
|
204
210
|
|
|
205
211
|
function orchestratorPromptPath(): string {
|
|
@@ -222,7 +228,11 @@ function readOrchestratorPrompt(): string {
|
|
|
222
228
|
return orchestratorPromptCache;
|
|
223
229
|
}
|
|
224
230
|
|
|
225
|
-
/**
|
|
231
|
+
/**
|
|
232
|
+
* Read the current session's adaptive-orchestration flag (the last entry wins).
|
|
233
|
+
* Adaptive orchestration is disabled by default; `/orchestrate on` is an
|
|
234
|
+
* explicit per-session opt-in.
|
|
235
|
+
*/
|
|
226
236
|
function isOrchestratorMode(ctx: { sessionManager?: { getEntries?: () => unknown[] } }): boolean {
|
|
227
237
|
try {
|
|
228
238
|
const entries = (ctx.sessionManager?.getEntries?.() ?? []) as Array<{
|
|
@@ -248,10 +258,15 @@ function agentFilePath(agentName: string): string {
|
|
|
248
258
|
return path.join(here, "agents", `${agentName}.md`);
|
|
249
259
|
}
|
|
250
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
|
+
|
|
251
266
|
/** Available model ids (provider/model) for the current session */
|
|
252
267
|
function availableModelIds(ctx: {
|
|
253
|
-
scopedModels?:
|
|
254
|
-
modelRegistry?: { getAvailable?: () =>
|
|
268
|
+
scopedModels?: ReadonlyArray<{ model?: { provider?: string; id?: string } }>;
|
|
269
|
+
modelRegistry?: { getAvailable?: () => ReadonlyArray<{ provider?: string; id?: string }> };
|
|
255
270
|
}): string[] {
|
|
256
271
|
const scoped = ctx.scopedModels;
|
|
257
272
|
if (Array.isArray(scoped) && scoped.length > 0) {
|
|
@@ -267,18 +282,37 @@ function availableModelIds(ctx: {
|
|
|
267
282
|
|
|
268
283
|
/** Update a `model` or `thinking` line in an agent's frontmatter */
|
|
269
284
|
function updateAgentConfig(agentName: string, key: "model" | "thinking", value: string): boolean {
|
|
270
|
-
const
|
|
271
|
-
|
|
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;
|
|
272
289
|
try {
|
|
273
|
-
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] ?? "";
|
|
274
294
|
const lineRe = new RegExp(`^(${key}:).*$`, "m");
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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;
|
|
280
315
|
}
|
|
281
|
-
fs.writeFileSync(file, content, "utf-8");
|
|
282
316
|
return true;
|
|
283
317
|
} catch {
|
|
284
318
|
return false;
|
|
@@ -292,8 +326,9 @@ function updateAgentConfig(agentName: string, key: "model" | "thinking", value:
|
|
|
292
326
|
interface AsyncTask {
|
|
293
327
|
runId: string;
|
|
294
328
|
agent: string;
|
|
295
|
-
|
|
329
|
+
address: string;
|
|
296
330
|
startedAt: number;
|
|
331
|
+
sessionGeneration: number;
|
|
297
332
|
}
|
|
298
333
|
|
|
299
334
|
export interface RunningSubagentActivity {
|
|
@@ -575,8 +610,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
575
610
|
|
|
576
611
|
let cwd = process.cwd();
|
|
577
612
|
let messageRoot = sessionRoot("ephemeral");
|
|
613
|
+
let sessionGeneration = 0;
|
|
578
614
|
const tasks = new Map<string, AsyncTask>();
|
|
579
|
-
const
|
|
615
|
+
const runControllers = new Map<string, AbortController>();
|
|
616
|
+
let pool = new SubagentPool(messageRoot);
|
|
617
|
+
let poolDisposed = false;
|
|
618
|
+
const activeRuns = new ActiveRunRegistry();
|
|
580
619
|
const runningWidget = new RunningSubagentWidgetController();
|
|
581
620
|
const sessionHandles = new Map<string, SubagentSessionHandle>();
|
|
582
621
|
const waitGraph = new MessageWaitGraph();
|
|
@@ -587,9 +626,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
587
626
|
let sessionContext: any;
|
|
588
627
|
let terminalInputUnsubscribe: (() => void) | undefined;
|
|
589
628
|
let sessionOverlayOpen = false;
|
|
629
|
+
let closeSessionOverlay: (() => void) | undefined;
|
|
590
630
|
let pendingOpenActivityId: string | undefined;
|
|
591
631
|
let lastNavigation: { direction: -1 | 1; at: number } | undefined;
|
|
592
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
|
+
|
|
593
641
|
function moveWidgetSelection(direction: -1 | 1): void {
|
|
594
642
|
const now = Date.now();
|
|
595
643
|
if (
|
|
@@ -623,7 +671,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
623
671
|
pendingOpenActivityId = undefined;
|
|
624
672
|
runningWidget.clearSelection();
|
|
625
673
|
sessionOverlayOpen = true;
|
|
626
|
-
void openSubagentSessionOverlay(sessionContext, session)
|
|
674
|
+
void openSubagentSessionOverlay(sessionContext, session, (close) => {
|
|
675
|
+
closeSessionOverlay = close;
|
|
676
|
+
})
|
|
627
677
|
.catch((error) => {
|
|
628
678
|
sessionContext?.ui?.notify?.(
|
|
629
679
|
`Failed to open ${selected.agent} session: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -631,18 +681,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
631
681
|
);
|
|
632
682
|
})
|
|
633
683
|
.finally(() => {
|
|
684
|
+
closeSessionOverlay = undefined;
|
|
634
685
|
sessionOverlayOpen = false;
|
|
635
686
|
});
|
|
636
687
|
}
|
|
637
688
|
|
|
638
689
|
pi.on("session_start", (event, ctx) => {
|
|
690
|
+
sessionGeneration++;
|
|
691
|
+
if (poolDisposed) {
|
|
692
|
+
pool = new SubagentPool(messageRoot);
|
|
693
|
+
bindPoolLifecycle();
|
|
694
|
+
poolDisposed = false;
|
|
695
|
+
}
|
|
639
696
|
cwd = ctx.cwd;
|
|
640
697
|
const sessionId =
|
|
641
698
|
ctx.sessionManager?.getSessionId?.() ??
|
|
642
699
|
(event as any).sessionId ??
|
|
643
700
|
"ephemeral";
|
|
644
701
|
messageRoot = sessionRoot(sessionId);
|
|
645
|
-
|
|
702
|
+
ensureChannelRoot(MESSAGE_ROOT_BASE);
|
|
703
|
+
sweepMessageRoots(MESSAGE_ROOT_BASE);
|
|
704
|
+
ensureChannelRoot(messageRoot);
|
|
646
705
|
pool.setIntercomRoot(messageRoot);
|
|
647
706
|
pool.setWorkingDirectory(cwd);
|
|
648
707
|
runningWidget.attach(ctx);
|
|
@@ -675,11 +734,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
675
734
|
});
|
|
676
735
|
|
|
677
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;
|
|
678
743
|
terminalInputUnsubscribe?.();
|
|
679
744
|
terminalInputUnsubscribe = undefined;
|
|
680
745
|
sessionContext = undefined;
|
|
681
746
|
pendingOpenActivityId = undefined;
|
|
682
747
|
sessionHandles.clear();
|
|
748
|
+
activeRuns.clear();
|
|
683
749
|
for (const pending of pendingMainReplyEdges.values()) {
|
|
684
750
|
if (pending.timer) clearTimeout(pending.timer);
|
|
685
751
|
pending.release();
|
|
@@ -688,6 +754,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
688
754
|
waitGraph.clear();
|
|
689
755
|
runningWidget.shutdown();
|
|
690
756
|
pool.dispose();
|
|
757
|
+
poolDisposed = true;
|
|
758
|
+
sweepMessageRoots(MESSAGE_ROOT_BASE);
|
|
691
759
|
});
|
|
692
760
|
|
|
693
761
|
function releaseMainReplyEdge(messageId: string): void {
|
|
@@ -711,12 +779,76 @@ export default function (pi: ExtensionAPI) {
|
|
|
711
779
|
pendingMainReplyEdges.set(msg.id, { release, timer });
|
|
712
780
|
}
|
|
713
781
|
|
|
714
|
-
// ---- 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. ----
|
|
715
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
|
+
|
|
716
847
|
pi.on("session_start", () => {
|
|
717
848
|
router?.dispose();
|
|
849
|
+
const routeRoot = messageRoot;
|
|
718
850
|
router = createMessageRouter(pi, {
|
|
719
|
-
root:
|
|
851
|
+
root: routeRoot,
|
|
720
852
|
matchesContext: () => true,
|
|
721
853
|
// to main: inject into the main session; the main model replies with reply_message
|
|
722
854
|
onMainMessage: (msg: MessageRequest) => {
|
|
@@ -724,7 +856,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
724
856
|
if (msg.expectsReply) {
|
|
725
857
|
const acquired = waitGraph.acquire(msg.from, MAIN_AGENT);
|
|
726
858
|
if (acquired.cycle) {
|
|
727
|
-
if (!writeReplyTo(msg, deadlockMessage(acquired.cycle))) {
|
|
859
|
+
if (!writeReplyTo(msg, deadlockMessage(acquired.cycle), routeRoot)) {
|
|
728
860
|
throw new Error("Failed to write deadlock-prevention reply.");
|
|
729
861
|
}
|
|
730
862
|
return;
|
|
@@ -762,6 +894,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
762
894
|
writeReplyTo(
|
|
763
895
|
msg,
|
|
764
896
|
`Failed to deliver message to main: ${error instanceof Error ? error.message : String(error)}`,
|
|
897
|
+
routeRoot,
|
|
765
898
|
);
|
|
766
899
|
}
|
|
767
900
|
throw error;
|
|
@@ -769,53 +902,71 @@ export default function (pi: ExtensionAPI) {
|
|
|
769
902
|
},
|
|
770
903
|
// to another subagent: route to its resident process, collect its reply, write it back
|
|
771
904
|
onChildMessage: async (msg: MessageRequest, signal?: AbortSignal) => {
|
|
905
|
+
const routePool = pool;
|
|
772
906
|
const agents = discoverAgents(cwd);
|
|
773
|
-
|
|
774
|
-
|
|
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) {
|
|
775
917
|
const reply = `Unknown subagent "${msg.to}"`;
|
|
776
|
-
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);
|
|
777
941
|
if (msg.from === MAIN_AGENT) throw new Error(reply);
|
|
778
942
|
return reply;
|
|
779
943
|
}
|
|
780
944
|
let releaseWait: (() => void) | undefined;
|
|
781
945
|
if (msg.expectsReply) {
|
|
782
|
-
const acquired = waitGraph.acquire(msg.from,
|
|
946
|
+
const acquired = waitGraph.acquire(msg.from, resolvedTargetRun.address);
|
|
783
947
|
if (acquired.cycle) {
|
|
784
948
|
const reply = deadlockMessage(acquired.cycle);
|
|
785
|
-
completeRoutedMessage(msg, reply);
|
|
949
|
+
completeRoutedMessage(msg, reply, routeRoot);
|
|
786
950
|
if (msg.from === MAIN_AGENT) throw new Error(reply);
|
|
787
951
|
return reply;
|
|
788
952
|
}
|
|
789
953
|
releaseWait = acquired.release;
|
|
790
954
|
}
|
|
791
955
|
const activityId = `message:${msg.id}`;
|
|
792
|
-
runningWidget.start(activityId,
|
|
956
|
+
runningWidget.start(activityId, resolvedTargetRun.address, msg.content, "message");
|
|
793
957
|
try {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
[
|
|
803
|
-
`You received a message from ${msg.from === MAIN_AGENT ? "the main agent" : `subagent ${msg.from}`} (message id ${msg.id}):`,
|
|
804
|
-
``,
|
|
805
|
-
msg.content,
|
|
806
|
-
``,
|
|
807
|
-
`Process this message and give your reply. Your reply will be sent back to ${msg.from === MAIN_AGENT ? "the main agent" : msg.from}.`,
|
|
808
|
-
].join("\n"),
|
|
809
|
-
timeoutMs,
|
|
810
|
-
undefined,
|
|
811
|
-
signal,
|
|
812
|
-
);
|
|
813
|
-
const replyText = getFinalOutput(result.messages) || `(subagent ${msg.to} gave no reply)`;
|
|
814
|
-
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);
|
|
815
966
|
return replyText;
|
|
816
967
|
} catch (e) {
|
|
817
|
-
const reply = `Target subagent failed to process the message: ${e instanceof Error ? e.message : String(e)}`;
|
|
818
|
-
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);
|
|
819
970
|
if (msg.from === MAIN_AGENT) throw new Error(reply);
|
|
820
971
|
return reply;
|
|
821
972
|
} finally {
|
|
@@ -837,9 +988,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
837
988
|
});
|
|
838
989
|
|
|
839
990
|
/** Write a reply back to the message sender */
|
|
840
|
-
function writeReplyTo(msg: MessageRequest, content: string): boolean {
|
|
991
|
+
function writeReplyTo(msg: MessageRequest, content: string, root = messageRoot): boolean {
|
|
841
992
|
try {
|
|
842
|
-
const dir = channelDir(
|
|
993
|
+
const dir = channelDir(root, msg.fromRunId, msg.fromAgent, msg.fromChildIndex);
|
|
843
994
|
writeReply(dir, msg.id, content);
|
|
844
995
|
removeRequestFile(msg);
|
|
845
996
|
releaseMainReplyEdge(msg.id);
|
|
@@ -849,52 +1000,149 @@ export default function (pi: ExtensionAPI) {
|
|
|
849
1000
|
}
|
|
850
1001
|
}
|
|
851
1002
|
|
|
852
|
-
function completeRoutedMessage(msg: MessageRequest, content: string) {
|
|
1003
|
+
function completeRoutedMessage(msg: MessageRequest, content: string, root = messageRoot) {
|
|
853
1004
|
if (msg.from === MAIN_AGENT) return;
|
|
854
|
-
if (msg.expectsReply)
|
|
855
|
-
|
|
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
|
+
}
|
|
856
1015
|
}
|
|
857
1016
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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".`,
|
|
869
1047
|
);
|
|
870
|
-
} catch {
|
|
871
|
-
/* ignore */
|
|
872
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 };
|
|
873
1089
|
}
|
|
874
1090
|
|
|
875
|
-
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 } {
|
|
876
1099
|
const runId = randomUUID();
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
runningWidget.start(runId, agent.name, taskText, "background");
|
|
880
|
-
|
|
881
|
-
spawnInteractiveSubagent({
|
|
1100
|
+
const controller = new AbortController();
|
|
1101
|
+
const direct = createDirectRun(
|
|
882
1102
|
agent,
|
|
883
|
-
task: taskText,
|
|
884
|
-
cwd: cwdOverride,
|
|
885
|
-
messageRoot,
|
|
886
1103
|
runId,
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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()
|
|
891
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);
|
|
892
1142
|
const task = tasks.get(runId);
|
|
893
|
-
if (!task) return;
|
|
1143
|
+
if (!task || task.sessionGeneration !== sessionGeneration) return;
|
|
894
1144
|
const finalStatus: "done" | "error" =
|
|
895
1145
|
result.exitCode === 0 && result.stopReason !== "error" ? "done" : "error";
|
|
896
|
-
task.status = finalStatus;
|
|
897
|
-
persistTasks();
|
|
898
1146
|
const output = getFinalOutput(result.messages) || "(no text output)";
|
|
899
1147
|
const failureReason =
|
|
900
1148
|
result.exitCode !== 0
|
|
@@ -923,6 +1171,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
923
1171
|
display: true,
|
|
924
1172
|
details: {
|
|
925
1173
|
agent: agent.name,
|
|
1174
|
+
address: direct.address,
|
|
926
1175
|
status: finalStatus,
|
|
927
1176
|
body: displayBody,
|
|
928
1177
|
elapsedMs: Date.now() - task.startedAt,
|
|
@@ -936,13 +1185,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
936
1185
|
}
|
|
937
1186
|
})
|
|
938
1187
|
.catch((err) => {
|
|
1188
|
+
activeRuns.remove(runId);
|
|
1189
|
+
direct.rejectSession(err instanceof Error ? err : new Error(String(err)));
|
|
939
1190
|
const task = tasks.get(runId);
|
|
940
|
-
if (task) {
|
|
941
|
-
task.status = "error";
|
|
942
|
-
persistTasks();
|
|
943
|
-
}
|
|
944
1191
|
const errorText = err instanceof Error ? err.message : String(err);
|
|
945
1192
|
try {
|
|
1193
|
+
if (task && task.sessionGeneration !== sessionGeneration) return;
|
|
946
1194
|
pi.sendMessage(
|
|
947
1195
|
{
|
|
948
1196
|
customType: BACKGROUND_EVENT_TYPE,
|
|
@@ -950,6 +1198,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
950
1198
|
display: true,
|
|
951
1199
|
details: {
|
|
952
1200
|
agent: agent.name,
|
|
1201
|
+
address: direct.address,
|
|
953
1202
|
status: "error",
|
|
954
1203
|
body: errorText,
|
|
955
1204
|
elapsedMs: task ? Date.now() - task.startedAt : undefined,
|
|
@@ -963,15 +1212,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
963
1212
|
}
|
|
964
1213
|
})
|
|
965
1214
|
.finally(() => {
|
|
1215
|
+
runControllers.delete(runId);
|
|
1216
|
+
tasks.delete(runId);
|
|
1217
|
+
activeRuns.remove(runId);
|
|
966
1218
|
if (pendingOpenActivityId === runId) pendingOpenActivityId = undefined;
|
|
967
1219
|
sessionHandles.delete(runId);
|
|
968
1220
|
runningWidget.finish(runId);
|
|
969
1221
|
});
|
|
970
1222
|
|
|
971
|
-
return runId;
|
|
1223
|
+
return { runId, address: direct.address };
|
|
972
1224
|
}
|
|
973
1225
|
|
|
974
|
-
// ----
|
|
1226
|
+
// ---- adaptive orchestration prompt; `/orchestrate on` installs the opt-in prompt ----
|
|
975
1227
|
pi.on("before_agent_start", (event, ctx) => {
|
|
976
1228
|
// Treat an explicitly appended orchestrator prompt (CLI
|
|
977
1229
|
// --append-system-prompt) as orchestrator mode too.
|
|
@@ -982,7 +1234,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
982
1234
|
if (!prompt) return;
|
|
983
1235
|
return { systemPrompt: event.systemPrompt + "\n\n" + prompt };
|
|
984
1236
|
}
|
|
985
|
-
return { systemPrompt: event.systemPrompt + "\n\n" +
|
|
1237
|
+
return { systemPrompt: event.systemPrompt + "\n\n" + SUBAGENT_USAGE_DISABLED_GUARD };
|
|
986
1238
|
});
|
|
987
1239
|
|
|
988
1240
|
// ---- subagent tool ----
|
|
@@ -991,16 +1243,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
991
1243
|
label: "Subagent",
|
|
992
1244
|
description: [
|
|
993
1245
|
"Delegate a task to a subagent (isolated context, separate process).",
|
|
994
|
-
"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;",
|
|
995
1247
|
"async=true: run in background, return a runId immediately, inject the result into the conversation when done (non-blocking);",
|
|
996
1248
|
"async=false (default): wait synchronously for the result.",
|
|
997
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};`,
|
|
998
|
-
"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.",
|
|
999
1251
|
].join(" "),
|
|
1000
1252
|
parameters: Type.Object({
|
|
1001
1253
|
agent: Type.String({ description: "Subagent name" }),
|
|
1002
1254
|
task: Type.String({ description: "Task description for the subagent" }),
|
|
1003
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
|
+
),
|
|
1004
1266
|
async: Type.Optional(Type.Boolean({ description: "true=run in background without blocking (default false)" })),
|
|
1005
1267
|
timeoutSeconds: Type.Optional(
|
|
1006
1268
|
Type.Integer({
|
|
@@ -1018,6 +1280,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1018
1280
|
const available = agents.map((a) => a.name).join(", ") || "none";
|
|
1019
1281
|
return {
|
|
1020
1282
|
content: [{ type: "text", text: `Unknown subagent "${params.agent}". Available: ${available}` }],
|
|
1283
|
+
details: undefined,
|
|
1021
1284
|
};
|
|
1022
1285
|
}
|
|
1023
1286
|
const timeoutSeconds = Math.min(
|
|
@@ -1028,48 +1291,88 @@ export default function (pi: ExtensionAPI) {
|
|
|
1028
1291
|
|
|
1029
1292
|
if (params.async) {
|
|
1030
1293
|
try {
|
|
1031
|
-
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;
|
|
1032
1297
|
return {
|
|
1033
1298
|
content: [
|
|
1034
1299
|
{
|
|
1035
1300
|
type: "text",
|
|
1036
|
-
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.`,
|
|
1037
1302
|
},
|
|
1038
1303
|
],
|
|
1039
|
-
details: { mode: "async", runId },
|
|
1304
|
+
details: { mode: "async", runId, address },
|
|
1040
1305
|
};
|
|
1041
1306
|
} catch (e) {
|
|
1042
1307
|
return {
|
|
1043
1308
|
content: [{ type: "text", text: `Failed to start background subagent: ${e instanceof Error ? e.message : String(e)}` }],
|
|
1309
|
+
details: undefined,
|
|
1044
1310
|
};
|
|
1045
1311
|
}
|
|
1046
1312
|
}
|
|
1047
1313
|
|
|
1048
|
-
onUpdate?.({
|
|
1314
|
+
onUpdate?.({
|
|
1315
|
+
content: [{ type: "text", text: `Starting ${params.agent} subagent...` }],
|
|
1316
|
+
details: undefined,
|
|
1317
|
+
});
|
|
1049
1318
|
const runId = randomUUID();
|
|
1050
|
-
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);
|
|
1051
1337
|
if (acquired.cycle) {
|
|
1338
|
+
signal?.removeEventListener("abort", forwardAbort);
|
|
1339
|
+
runControllers.delete(runId);
|
|
1340
|
+
activeRuns.remove(runId);
|
|
1052
1341
|
return {
|
|
1053
1342
|
content: [{ type: "text", text: deadlockMessage(acquired.cycle) }],
|
|
1343
|
+
details: undefined,
|
|
1054
1344
|
};
|
|
1055
1345
|
}
|
|
1056
1346
|
runningWidget.start(runId, agent.name, params.task, "task");
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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);
|
|
1068
1371
|
acquired.release?.();
|
|
1069
1372
|
if (pendingOpenActivityId === runId) pendingOpenActivityId = undefined;
|
|
1070
1373
|
sessionHandles.delete(runId);
|
|
1071
1374
|
runningWidget.finish(runId);
|
|
1072
|
-
}
|
|
1375
|
+
}
|
|
1073
1376
|
const output = getFinalOutput(result.messages);
|
|
1074
1377
|
|
|
1075
1378
|
if (result.exitCode !== 0) {
|
|
@@ -1093,7 +1396,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1093
1396
|
|
|
1094
1397
|
// ---- commands ----
|
|
1095
1398
|
pi.registerCommand("orchestrate", {
|
|
1096
|
-
description: "
|
|
1399
|
+
description: "Enable/disable adaptive subagent delegation (on|off|status, default off)",
|
|
1097
1400
|
handler: async (args, ctx) => {
|
|
1098
1401
|
const first = Array.isArray(args) ? (args[0] ?? "on") : String(args ?? "on").trim().split(/\s+/)[0] ?? "on";
|
|
1099
1402
|
const arg = String(first).toLowerCase();
|
|
@@ -1102,8 +1405,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1102
1405
|
const on = isOrchestratorMode(ctx);
|
|
1103
1406
|
ctx.ui.notify(
|
|
1104
1407
|
on
|
|
1105
|
-
? "
|
|
1106
|
-
: "
|
|
1408
|
+
? "Adaptive orchestration: ON (main decides whether and how to delegate; use /orchestrate off to disable automatic delegation)"
|
|
1409
|
+
: "Adaptive orchestration: OFF (subagents require an explicit user request; use /orchestrate on to restore main-agent discretion)",
|
|
1107
1410
|
"info",
|
|
1108
1411
|
);
|
|
1109
1412
|
return;
|
|
@@ -1117,8 +1420,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1117
1420
|
}
|
|
1118
1421
|
ctx.ui.notify(
|
|
1119
1422
|
enabled
|
|
1120
|
-
? "
|
|
1121
|
-
: "
|
|
1423
|
+
? "Adaptive orchestration enabled — main will decide whether to work directly or delegate to any useful combination of specialists"
|
|
1424
|
+
: "Adaptive orchestration disabled — subagents now require an explicit user request",
|
|
1122
1425
|
"info",
|
|
1123
1426
|
);
|
|
1124
1427
|
},
|
|
@@ -1130,7 +1433,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1130
1433
|
const agents = discoverAgents(ctx.cwd);
|
|
1131
1434
|
const lines = agents.map(
|
|
1132
1435
|
(a) =>
|
|
1133
|
-
`- ${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(",")}]` : ""}`,
|
|
1134
1437
|
);
|
|
1135
1438
|
ctx.ui.notify(lines.length ? `Available subagents:\n${lines.join("\n")}` : "No subagents found", "info");
|
|
1136
1439
|
},
|
|
@@ -1159,6 +1462,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1159
1462
|
ctx.ui.notify(`No built-in agent named "${agentName}"`, "warning");
|
|
1160
1463
|
return;
|
|
1161
1464
|
}
|
|
1465
|
+
pool.restartAgent(agentName);
|
|
1162
1466
|
ctx.ui.notify(`Subagent ${agentName}: thinking -> ${level}`, "info");
|
|
1163
1467
|
} else {
|
|
1164
1468
|
const choices = availableModelIds(ctx);
|
|
@@ -1172,33 +1476,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
1172
1476
|
ctx.ui.notify(`No built-in agent named "${agentName}"`, "warning");
|
|
1173
1477
|
return;
|
|
1174
1478
|
}
|
|
1479
|
+
pool.restartAgent(agentName);
|
|
1175
1480
|
ctx.ui.notify(`Subagent ${agentName}: model -> ${model}`, "info");
|
|
1176
1481
|
}
|
|
1177
1482
|
},
|
|
1178
1483
|
});
|
|
1179
1484
|
|
|
1180
1485
|
pi.registerCommand("subagent-status", {
|
|
1181
|
-
description: "Show
|
|
1486
|
+
description: "Show active subagent runs and runtime addresses",
|
|
1182
1487
|
handler: async (_args, ctx) => {
|
|
1183
1488
|
const list = [...tasks.values()];
|
|
1184
1489
|
const lines = list.map((t) => {
|
|
1185
|
-
|
|
1186
|
-
|
|
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)`;
|
|
1187
1492
|
});
|
|
1188
|
-
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
|
+
);
|
|
1189
1497
|
ctx.ui.notify(
|
|
1190
1498
|
[
|
|
1191
1499
|
lines.length ? `Background tasks:\n${lines.join("\n")}` : "Background tasks: none",
|
|
1192
|
-
`
|
|
1500
|
+
activeLines.length ? `Active run registry:\n${activeLines.join("\n")}` : "Active run registry: none",
|
|
1193
1501
|
].join("\n"),
|
|
1194
1502
|
"info",
|
|
1195
1503
|
);
|
|
1196
1504
|
},
|
|
1197
1505
|
});
|
|
1198
1506
|
}
|
|
1199
|
-
|
|
1200
|
-
function* poolAliveNames(pool: SubagentPool): Generator<string> {
|
|
1201
|
-
for (const n of ["planner", "reviewer", "actor"]) {
|
|
1202
|
-
if (pool.isAlive(n)) yield n;
|
|
1203
|
-
}
|
|
1204
|
-
}
|