oira666_pi-subagent 0.2.20 → 0.2.21

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.
Files changed (4) hide show
  1. package/index.ts +357 -23
  2. package/package.json +1 -1
  3. package/render.ts +13 -4
  4. package/runner.ts +67 -32
package/index.ts CHANGED
@@ -15,8 +15,8 @@ import {
15
15
  } from "@mariozechner/pi-ai";
16
16
  import { Type } from "@sinclair/typebox";
17
17
  import { type AgentConfig, discoverAgents } from "./agents.js";
18
- import { renderCall, renderResult } from "./render.js";
19
- import { runAgentSubprocess, executeParallelSubprocess } from "./runner.js";
18
+ import { renderCall, renderResult, setBroadcastNumberingActive } from "./render.js";
19
+ import { runAgentSubprocess, executeParallelSubprocess, type RunningSubagentHandle } from "./runner.js";
20
20
  import {
21
21
  SUBAGENT_RESUME_DISABLE_ENV,
22
22
  SUBAGENT_RESUME_PROMPT_ENV,
@@ -45,7 +45,9 @@ import {
45
45
  DEFAULT_DELEGATION_MODE,
46
46
  buildSubagentDetails,
47
47
  getFinalOutput,
48
+ getNestedSubagentResults,
48
49
  isResultError,
50
+ isSubagentDetails,
49
51
  } from "./types.js";
50
52
 
51
53
  // ---------------------------------------------------------------------------
@@ -610,6 +612,286 @@ export default function (pi: ExtensionAPI) {
610
612
  let pendingResumePlan: ResumableSubagentCall | null = null;
611
613
  let modelToRestoreAfterResume: any | undefined;
612
614
  const approvedProjectAgentDirsForSession = new Set<string>();
615
+ const activeSubagents = new Map<number, { agent: string; task: string; handle: RunningSubagentHandle }>();
616
+ const latestBroadcastTargets = {
617
+ all: [] as BroadcastTarget[],
618
+ youngest: [] as BroadcastTarget[],
619
+ };
620
+ let nextActiveSubagentId = 1;
621
+
622
+ const BROADCAST_STEER_PREFIX = "__PI_SUBAGENT_BROADCAST_STEER__";
623
+
624
+ interface BroadcastTarget {
625
+ display: string;
626
+ topLevelId: number;
627
+ restPath: number[];
628
+ }
629
+
630
+ interface ParsedBroadcastSelection {
631
+ targets: BroadcastTarget[];
632
+ errors: string[];
633
+ }
634
+
635
+ function parseBroadcastPath(raw: string): number[] | null {
636
+ const parts = raw.trim().split(".");
637
+ if (parts.length === 0) return null;
638
+ const path: number[] = [];
639
+ for (const part of parts) {
640
+ if (!/^\d+$/.test(part)) return null;
641
+ const value = Number(part);
642
+ if (!Number.isSafeInteger(value) || value < 1) return null;
643
+ path.push(value);
644
+ }
645
+ return path;
646
+ }
647
+
648
+ function parseBroadcastSelection(input: string, available: number[]): ParsedBroadcastSelection {
649
+ const normalized = input.trim().toUpperCase();
650
+ if (normalized === "ALL") {
651
+ return {
652
+ targets: available.map((id) => ({ display: String(id), topLevelId: id, restPath: [] })),
653
+ errors: [],
654
+ };
655
+ }
656
+
657
+ const errors: string[] = [];
658
+ const targetMap = new Map<string, BroadcastTarget>();
659
+ for (const rawPart of input.split(",")) {
660
+ const part = rawPart.trim();
661
+ if (!part) continue;
662
+
663
+ if (part.includes("-")) {
664
+ const [startRaw, endRaw, extra] = part.split("-").map((p) => p.trim());
665
+ const startPath = parseBroadcastPath(startRaw);
666
+ const endPath = parseBroadcastPath(endRaw);
667
+ if (extra !== undefined || !startPath || !endPath || startPath.length !== 1 || endPath.length !== 1) {
668
+ errors.push(`Invalid range "${part}". Use top-level ranges like 1-3.`);
669
+ continue;
670
+ }
671
+ const start = startPath[0];
672
+ const end = endPath[0];
673
+ for (let id = Math.min(start, end); id <= Math.max(start, end); id++) {
674
+ if (!available.includes(id)) {
675
+ errors.push(`Subagent ${id} is not running.`);
676
+ continue;
677
+ }
678
+ targetMap.set(String(id), { display: String(id), topLevelId: id, restPath: [] });
679
+ }
680
+ continue;
681
+ }
682
+
683
+ const path = parseBroadcastPath(part);
684
+ if (!path) {
685
+ errors.push(`Invalid target "${part}". Use ALL, numbers, paths, or top-level ranges (e.g. 1, 2, 4.1, 1-3).`);
686
+ continue;
687
+ }
688
+ const [topLevelId, ...restPath] = path;
689
+ if (!available.includes(topLevelId)) {
690
+ errors.push(`Subagent ${topLevelId} is not running.`);
691
+ continue;
692
+ }
693
+ const display = path.join(".");
694
+ targetMap.set(display, { display, topLevelId, restPath });
695
+ }
696
+
697
+ return {
698
+ targets: Array.from(targetMap.values()).sort((a, b) => a.display.localeCompare(b.display, undefined, { numeric: true })),
699
+ errors,
700
+ };
701
+ }
702
+
703
+ function encodeNestedBroadcast(message: string, path: number[]): string {
704
+ return `${BROADCAST_STEER_PREFIX}${JSON.stringify({ path, message })}`;
705
+ }
706
+
707
+ function decodeNestedBroadcast(text: string): { path: number[]; message: string } | null {
708
+ if (!text.startsWith(BROADCAST_STEER_PREFIX)) return null;
709
+ try {
710
+ const parsed = JSON.parse(text.slice(BROADCAST_STEER_PREFIX.length));
711
+ if (!Array.isArray(parsed?.path) || !parsed.path.every((n: unknown) => Number.isSafeInteger(n) && (n as number) >= 1)) return null;
712
+ if (typeof parsed?.message !== "string") return null;
713
+ return { path: parsed.path, message: parsed.message };
714
+ } catch {
715
+ return null;
716
+ }
717
+ }
718
+
719
+ function extractPendingSubagentTaskCounts(result: SingleResult): number[] {
720
+ const completedToolCallIds = new Set(getNestedSubagentResults(result.messages).map((nested) => nested.toolCallId));
721
+ const counts: number[] = [];
722
+ for (const message of result.messages as any[]) {
723
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
724
+ for (const part of message.content) {
725
+ if (part?.type !== "toolCall" || part?.name !== "subagent") continue;
726
+ const toolCallId = typeof part.toolCallId === "string" ? part.toolCallId : typeof part.id === "string" ? part.id : undefined;
727
+ if (toolCallId && completedToolCallIds.has(toolCallId)) continue;
728
+ const tasks = Array.isArray(part.arguments?.tasks) ? part.arguments.tasks : [];
729
+ if (tasks.length > 0) counts.push(tasks.length);
730
+ }
731
+ }
732
+ return counts;
733
+ }
734
+
735
+ function collectRunningBroadcastTargetsFromResult(
736
+ result: SingleResult,
737
+ path: number[],
738
+ targets: { all: BroadcastTarget[]; youngest: BroadcastTarget[] },
739
+ ): boolean {
740
+ const nestedRunningPaths: number[][] = [];
741
+
742
+ for (const nested of getNestedSubagentResults(result.messages)) {
743
+ if (!isSubagentDetails(nested.details)) continue;
744
+ nested.details.results.forEach((child, index) => {
745
+ const childPath = [...path, index + 1];
746
+ if (collectRunningBroadcastTargetsFromResult(child, childPath, targets)) {
747
+ nestedRunningPaths.push(childPath);
748
+ }
749
+ });
750
+ }
751
+
752
+ if (result.exitCode === -1) {
753
+ for (const taskCount of extractPendingSubagentTaskCounts(result)) {
754
+ for (let i = 1; i <= taskCount; i++) {
755
+ const childPath = [...path, i];
756
+ const [topLevelId, ...restPath] = childPath;
757
+ targets.all.push({ display: childPath.join("."), topLevelId, restPath });
758
+ targets.youngest.push({ display: childPath.join("."), topLevelId, restPath });
759
+ nestedRunningPaths.push(childPath);
760
+ }
761
+ }
762
+ }
763
+
764
+ const isRunning = result.exitCode === -1;
765
+ if (!isRunning) return nestedRunningPaths.length > 0;
766
+
767
+ const [topLevelId, ...restPath] = path;
768
+ const self = { display: path.join("."), topLevelId, restPath };
769
+ targets.all.push(self);
770
+ if (nestedRunningPaths.length === 0) targets.youngest.push(self);
771
+ return true;
772
+ }
773
+
774
+ function updateLatestBroadcastTargets(details: SubagentDetails | undefined): void {
775
+ latestBroadcastTargets.all = [];
776
+ latestBroadcastTargets.youngest = [];
777
+ if (!details) {
778
+ for (const id of activeSubagents.keys()) {
779
+ const target = { display: String(id), topLevelId: id, restPath: [] };
780
+ latestBroadcastTargets.all.push(target);
781
+ latestBroadcastTargets.youngest.push(target);
782
+ }
783
+ return;
784
+ }
785
+ details.results.forEach((result, index) => {
786
+ collectRunningBroadcastTargetsFromResult(result, [index + 1], latestBroadcastTargets);
787
+ });
788
+ const dedupe = (targets: BroadcastTarget[]) =>
789
+ Array.from(new Map(targets.map((target) => [target.display, target])).values())
790
+ .filter((target) => activeSubagents.has(target.topLevelId))
791
+ .sort((a, b) => a.display.localeCompare(b.display, undefined, { numeric: true }));
792
+ latestBroadcastTargets.all = dedupe(latestBroadcastTargets.all);
793
+ latestBroadcastTargets.youngest = dedupe(latestBroadcastTargets.youngest);
794
+ }
795
+
796
+ function getFallbackTopLevelTargets(): BroadcastTarget[] {
797
+ return Array.from(activeSubagents.keys())
798
+ .sort((a, b) => a - b)
799
+ .map((id) => ({ display: String(id), topLevelId: id, restPath: [] }));
800
+ }
801
+
802
+ function sendBroadcastToTargets(message: string, targets: BroadcastTarget[], ctx: any): void {
803
+ const delivered: string[] = [];
804
+ const missed: string[] = [];
805
+ for (const target of targets) {
806
+ const item = activeSubagents.get(target.topLevelId);
807
+ if (!item) {
808
+ missed.push(target.display);
809
+ continue;
810
+ }
811
+ item.handle.steer(
812
+ target.restPath.length > 0
813
+ ? encodeNestedBroadcast(message, target.restPath)
814
+ : message,
815
+ );
816
+ delivered.push(target.display);
817
+ }
818
+ if (delivered.length > 0) {
819
+ ctx.ui.notify(`Broadcasted steering message to subagent(s): ${delivered.join(", ")}`, "info");
820
+ }
821
+ if (missed.length > 0) {
822
+ ctx.ui.notify(`Some selected subagents are no longer running: ${missed.join(", ")}`, "warning");
823
+ }
824
+ }
825
+
826
+ async function askBroadcastForSteering(message: string, ctx: any): Promise<"continue" | "handled"> {
827
+ const nested = decodeNestedBroadcast(message);
828
+ if (nested) {
829
+ const available = Array.from(activeSubagents.keys()).sort((a, b) => a - b);
830
+ if (available.length === 0) return "handled";
831
+ const [nextId, ...restPath] = nested.path;
832
+ if (!available.includes(nextId)) return "handled";
833
+ sendBroadcastToTargets(nested.message, [{ display: nested.path.join("."), topLevelId: nextId, restPath }], ctx);
834
+ return "handled";
835
+ }
836
+
837
+ if (!ctx.hasUI || activeSubagents.size === 0) return "continue";
838
+
839
+ setBroadcastNumberingActive(true);
840
+ try {
841
+ const available = Array.from(activeSubagents.keys()).sort((a, b) => a - b);
842
+ const choice = await ctx.ui.select(
843
+ "Broadcast this steering message to subagents?",
844
+ ["No", "All (+nested)", "Youngest", "Numbers (e.g. 1, 2, 4.1)"],
845
+ );
846
+ if (choice === "All (+nested)" || choice === "Youngest") {
847
+ const fallback = getFallbackTopLevelTargets();
848
+ const selected = choice === "Youngest"
849
+ ? (latestBroadcastTargets.youngest.length > 0 ? latestBroadcastTargets.youngest : fallback)
850
+ : (latestBroadcastTargets.all.length > 0 ? latestBroadcastTargets.all : fallback);
851
+ const current = selected.filter((target) => activeSubagents.has(target.topLevelId));
852
+ if (current.length === 0) {
853
+ ctx.ui.notify("No selected subagents are still running. Continuing with normal steering.", "warning");
854
+ return "continue";
855
+ }
856
+ sendBroadcastToTargets(message, current, ctx);
857
+ return "handled";
858
+ }
859
+ if (choice !== "Numbers (e.g. 1, 2, 4.1)") return "continue";
860
+
861
+ const answer = await ctx.ui.input(
862
+ "Subagent numbers/ranges to broadcast to (e.g. 1, 2, 4.1)",
863
+ "",
864
+ );
865
+ if (!answer) return "continue";
866
+ const current = Array.from(new Set([
867
+ ...available,
868
+ ...latestBroadcastTargets.all.map((target) => target.topLevelId),
869
+ ])).sort((a, b) => a - b);
870
+ const parsed = parseBroadcastSelection(answer, current);
871
+ const knownDisplays = new Set(latestBroadcastTargets.all.map((target) => target.display));
872
+ const validatedTargets = knownDisplays.size > 0
873
+ ? parsed.targets.filter((target) => knownDisplays.has(target.display))
874
+ : parsed.targets;
875
+ const unknownNested = knownDisplays.size > 0
876
+ ? parsed.targets.filter((target) => !knownDisplays.has(target.display)).map((target) => target.display)
877
+ : [];
878
+ const errors = [
879
+ ...parsed.errors,
880
+ ...unknownNested.map((display) => `Subagent ${display} is not running.`),
881
+ ];
882
+ if (errors.length > 0) {
883
+ ctx.ui.notify(errors.slice(0, 4).join("\n"), "warning");
884
+ }
885
+ if (validatedTargets.length === 0) {
886
+ ctx.ui.notify("No valid running subagents selected. Continuing with normal steering.", "warning");
887
+ return "continue";
888
+ }
889
+ sendBroadcastToTargets(message, validatedTargets, ctx);
890
+ return "handled";
891
+ } finally {
892
+ setBroadcastNumberingActive(false);
893
+ }
894
+ }
613
895
 
614
896
  async function restoreModelAfterResumeFailure(ctx?: { ui?: { notify?: (message: string, type?: "info" | "warning" | "error") => void } }) {
615
897
  const restore = modelToRestoreAfterResume;
@@ -748,6 +1030,23 @@ export default function (pi: ExtensionAPI) {
748
1030
  }, RESUME_INTERACTIVE_DELAY_MS);
749
1031
  });
750
1032
 
1033
+ pi.on("input", async (event, ctx) => {
1034
+ try {
1035
+ // Pi emits this before it applies the built-in streaming behavior. When a
1036
+ // subagent tool is running, user input is a normal steering message to the
1037
+ // parent. Give the user a chance to route it to child RPC sessions instead.
1038
+ if (activeSubagents.size === 0) return { action: "continue" as const };
1039
+ if (ctx.isIdle()) return { action: "continue" as const };
1040
+ const result = await askBroadcastForSteering(event.text, ctx);
1041
+ return result === "handled"
1042
+ ? { action: "handled" as const }
1043
+ : { action: "continue" as const };
1044
+ } catch (err) {
1045
+ console.error("[pi-subagent] Error while handling steering broadcast:", err);
1046
+ return { action: "continue" as const };
1047
+ }
1048
+ });
1049
+
751
1050
  // Inject available agents into the system prompt
752
1051
  pi.on("before_agent_start", async (event) => {
753
1052
  try {
@@ -818,6 +1117,9 @@ calls one after another. Do NOT put dependent tasks in the same array.
818
1117
 
819
1118
  async execute(toolCallId, params, signal, onUpdate, ctx) {
820
1119
  try {
1120
+ activeSubagents.clear();
1121
+ updateLatestBroadcastTargets(undefined);
1122
+ nextActiveSubagentId = 1;
821
1123
  const discovery = discoverAgents(ctx.cwd, "both");
822
1124
  const { agents } = discovery;
823
1125
 
@@ -840,6 +1142,10 @@ calls one after another. Do NOT put dependent tasks in the same array.
840
1142
  }
841
1143
 
842
1144
  const executionMode = tasks.length === 1 ? "single" : "parallel";
1145
+ const trackedOnUpdate = (partial: any) => {
1146
+ if (isSubagentDetails(partial?.details)) updateLatestBroadcastTargets(partial.details);
1147
+ onUpdate?.(partial);
1148
+ };
843
1149
 
844
1150
  // Security: guard project-local agents before running
845
1151
  const requested = new Set<string>();
@@ -937,7 +1243,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
937
1243
  agents,
938
1244
  ctx.cwd,
939
1245
  signal,
940
- onUpdate,
1246
+ trackedOnUpdate,
941
1247
  makeDetails,
942
1248
  resumePlan?.details?.results[0],
943
1249
  getSessionDirForTask(resumePlan?.previousToolCallId ?? toolCallId, 0),
@@ -946,7 +1252,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
946
1252
  // active model (normal runs). This prevents children from
947
1253
  // defaulting to whatever settings.json says at spawn time, which
948
1254
  // can change while the parent session is long-running.
949
- formatModelFlag(modelToRestoreAfterResume ?? lastRestorableModel),
1255
+ formatModelFlag(modelToRestoreAfterResume ?? ctx.model ?? lastRestorableModel),
950
1256
  );
951
1257
  }
952
1258
 
@@ -955,12 +1261,12 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
955
1261
  agents,
956
1262
  ctx.cwd,
957
1263
  signal,
958
- onUpdate,
1264
+ trackedOnUpdate,
959
1265
  makeDetails,
960
1266
  resumePlan?.details?.results,
961
1267
  (index) => getSessionDirForTask(resumePlan?.previousToolCallId ?? toolCallId, index),
962
1268
  !!resumePlan,
963
- formatModelFlag(modelToRestoreAfterResume ?? lastRestorableModel),
1269
+ formatModelFlag(modelToRestoreAfterResume ?? ctx.model ?? lastRestorableModel),
964
1270
  );
965
1271
  } catch (err) {
966
1272
  const msg = err instanceof Error ? err.message : String(err);
@@ -1016,6 +1322,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
1016
1322
  };
1017
1323
  }
1018
1324
 
1325
+ let activeId: number | undefined;
1019
1326
  const result = await runAgentSubprocess({
1020
1327
  cwd: defaultCwd,
1021
1328
  agents,
@@ -1033,7 +1340,16 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
1033
1340
  resumeSession: resumeExistingSession,
1034
1341
  initialResult: previousResult,
1035
1342
  fallbackModel,
1343
+ onHandle: (handle) => {
1344
+ activeId = 1;
1345
+ activeSubagents.set(activeId, { agent: agentName, task, handle });
1346
+ updateLatestBroadcastTargets(undefined);
1347
+ },
1036
1348
  });
1349
+ if (activeId !== undefined) {
1350
+ activeSubagents.delete(activeId);
1351
+ updateLatestBroadcastTargets(undefined);
1352
+ }
1037
1353
 
1038
1354
  if (isResultError(result)) {
1039
1355
  const errorMsg =
@@ -1075,22 +1391,40 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
1075
1391
  resumeExistingSessions: boolean,
1076
1392
  fallbackModel?: string,
1077
1393
  ) {
1078
- return executeParallelSubprocess(
1079
- tasks,
1080
- agents,
1081
- defaultCwd,
1082
- currentDepth,
1083
- maxDepth,
1084
- ancestorAgentStack,
1085
- preventCycles,
1086
- signal,
1087
- onUpdate,
1088
- makeDetails("parallel"),
1089
- resumeResults,
1090
- (index) => getSessionDir(index),
1091
- resumeExistingSessions,
1092
- currentSubagentSessionRoot,
1093
- fallbackModel,
1094
- );
1394
+ const taskIds = new Map<number, number>();
1395
+ try {
1396
+ return await executeParallelSubprocess(
1397
+ tasks,
1398
+ agents,
1399
+ defaultCwd,
1400
+ currentDepth,
1401
+ maxDepth,
1402
+ ancestorAgentStack,
1403
+ preventCycles,
1404
+ signal,
1405
+ onUpdate,
1406
+ makeDetails("parallel"),
1407
+ resumeResults,
1408
+ (index) => getSessionDir(index),
1409
+ resumeExistingSessions,
1410
+ currentSubagentSessionRoot,
1411
+ fallbackModel,
1412
+ (index, task, handle) => {
1413
+ const id = index + 1;
1414
+ taskIds.set(index, id);
1415
+ activeSubagents.set(id, { agent: task.agent, task: task.task, handle });
1416
+ updateLatestBroadcastTargets(undefined);
1417
+ },
1418
+ (index) => {
1419
+ const id = taskIds.get(index);
1420
+ if (id !== undefined) {
1421
+ activeSubagents.delete(id);
1422
+ updateLatestBroadcastTargets(undefined);
1423
+ }
1424
+ },
1425
+ );
1426
+ } finally {
1427
+ for (const id of taskIds.values()) activeSubagents.delete(id);
1428
+ }
1095
1429
  }
1096
1430
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-subagent",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
4
4
  "description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/render.ts CHANGED
@@ -20,6 +20,12 @@ import {
20
20
 
21
21
  const OUTPUT_PREVIEW_LINE_COUNT = 6;
22
22
 
23
+ let broadcastNumberingActive = false;
24
+
25
+ export function setBroadcastNumberingActive(active: boolean): void {
26
+ broadcastNumberingActive = active;
27
+ }
28
+
23
29
  type ThemeFg = (color: string, text: string) => string;
24
30
  type NodeStatus = "running" | "success" | "error";
25
31
 
@@ -351,12 +357,15 @@ function renderTreeLines(
351
357
  theme: { fg: ThemeFg },
352
358
  showOutputPreview: boolean,
353
359
  depth = 0,
360
+ prefix = "",
354
361
  ): string[] {
355
362
  const lines: string[] = [];
356
363
 
357
- for (const node of nodes) {
364
+ nodes.forEach((node, index) => {
358
365
  const indent = " ".repeat(depth);
359
- let line = `${indent}${statusEmoji(node.status, theme)} ${theme.fg("accent", node.label)}`;
366
+ const number = prefix ? `${prefix}.${index + 1}` : `${index + 1}`;
367
+ const numberPrefix = broadcastNumberingActive ? `${number}. ` : "";
368
+ let line = `${indent}${numberPrefix}${statusEmoji(node.status, theme)} ${theme.fg("accent", node.label)}`;
360
369
  if (node.meta) line += ` ${theme.fg("dim", node.meta)}`;
361
370
  lines.push(line);
362
371
 
@@ -373,9 +382,9 @@ function renderTreeLines(
373
382
  }
374
383
 
375
384
  if (node.children.length > 0) {
376
- lines.push(...renderTreeLines(node.children, theme, false, depth + 1));
385
+ lines.push(...renderTreeLines(node.children, theme, false, depth + 1, number));
377
386
  }
378
- }
387
+ });
379
388
 
380
389
  return lines;
381
390
  }
package/runner.ts CHANGED
@@ -73,6 +73,12 @@ const SUBAGENT_FALLBACK_MODEL_ENV = "PI_SUBAGENT_FALLBACK_MODEL";
73
73
 
74
74
  type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
75
75
 
76
+ export interface RunningSubagentHandle {
77
+ steer(message: string): void;
78
+ }
79
+
80
+ export type RunningSubagentStartedCallback = (handle: RunningSubagentHandle) => void;
81
+
76
82
  // ---------------------------------------------------------------------------
77
83
  // Temp file helpers
78
84
  // ---------------------------------------------------------------------------
@@ -462,17 +468,16 @@ function buildPiArgs(
462
468
  sessionDir: string | undefined,
463
469
  resumeSession: boolean,
464
470
  fallbackModelOverride?: string,
465
- ): string[] {
471
+ ): { args: string[]; prompt: string } {
466
472
  const args: string[] = [
467
473
  "--mode",
468
- "json",
474
+ "rpc",
469
475
  ..._inheritedCliArgs.extensionArgs,
470
476
  ..._inheritedCliArgs.alwaysProxy,
471
477
  ];
472
478
 
473
479
  if (sessionDir) args.push("--session-dir", sessionDir);
474
480
  if (resumeSession) args.push("--continue");
475
- args.push("-p");
476
481
 
477
482
  // Agent config takes priority; fall back to parent CLI value
478
483
  const model = agent.model ?? fallbackModelOverride ?? process.env[SUBAGENT_FALLBACK_MODEL_ENV] ?? _inheritedCliArgs.fallbackModel;
@@ -500,12 +505,12 @@ function buildPiArgs(
500
505
  }
501
506
 
502
507
  if (systemPromptPath) args.push("--append-system-prompt", systemPromptPath);
503
- args.push(
504
- resumeSession
508
+ return {
509
+ args,
510
+ prompt: resumeSession
505
511
  ? `Continue the previous task from where you left off. Original task: ${task}`
506
512
  : `Task: ${task}`,
507
- );
508
- return args;
513
+ };
509
514
  }
510
515
 
511
516
  // ---------------------------------------------------------------------------
@@ -549,6 +554,8 @@ export interface RunAgentOptions {
549
554
  piCommandOverride?: { command: string; argsPrefix?: string[] };
550
555
  /** Test/debug override for startup timeout. */
551
556
  startupTimeoutMsOverride?: number;
557
+ /** Called once the child RPC process is ready to receive steering messages. */
558
+ onHandle?: RunningSubagentStartedCallback;
552
559
  }
553
560
 
554
561
  /**
@@ -640,7 +647,7 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
640
647
  }
641
648
 
642
649
  try {
643
- const piArgs = buildPiArgs(
650
+ const { args: piArgs, prompt } = buildPiArgs(
644
651
  agent,
645
652
  promptTmpPath,
646
653
  task,
@@ -664,7 +671,7 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
664
671
  const proc = spawn(spawnCmd, spawnArgs, {
665
672
  cwd,
666
673
  shell: false,
667
- stdio: ["ignore", "pipe", "pipe"],
674
+ stdio: ["pipe", "pipe", "pipe"],
668
675
  env: {
669
676
  ...process.env,
670
677
  [SUBAGENT_DEPTH_ENV]: String(nextDepth),
@@ -683,6 +690,18 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
683
690
  let startupTimer: ReturnType<typeof setTimeout> | undefined;
684
691
  let receivedFirstEvent = false;
685
692
 
693
+ const sendRpc = (command: Record<string, unknown>) => {
694
+ proc.stdin?.write(`${JSON.stringify(command)}\n`);
695
+ };
696
+
697
+ opts.onHandle?.({
698
+ steer(message: string) {
699
+ sendRpc({ type: "steer", message });
700
+ },
701
+ });
702
+
703
+ sendRpc({ type: "prompt", message: prompt });
704
+
686
705
  // Startup timeout: kill the process if it never produces its first
687
706
  // JSON event. Once the first event arrives, this timer is permanently
688
707
  // disabled — from that point, tool calls can run for as long as they
@@ -737,6 +756,14 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
737
756
  };
738
757
 
739
758
  const flushLine = (line: string) => {
759
+ let event: any;
760
+ try { event = JSON.parse(line); } catch { event = null; }
761
+ if (event?.type === "agent_end") {
762
+ if (result.exitCode === -1) result.exitCode = 0;
763
+ try { proc.kill("SIGTERM"); } catch { /* already dead */ }
764
+ doResolve(0);
765
+ return;
766
+ }
740
767
  const accepted = processJsonLine(line, result);
741
768
  if (accepted) {
742
769
  // Cancel the startup timer as soon as the subprocess proves it has
@@ -889,6 +916,8 @@ export async function executeParallelSubprocess(
889
916
  resumeExistingSessions = false,
890
917
  sessionRoot?: string,
891
918
  fallbackModel?: string,
919
+ onHandleForTask?: (index: number, task: { agent: string; task: string }, handle: RunningSubagentHandle) => void,
920
+ onTaskDone?: (index: number, task: { agent: string; task: string }) => void,
892
921
  ): Promise<{
893
922
  content: Array<{ type: "text"; text: string }>;
894
923
  details: SubagentDetails;
@@ -975,29 +1004,35 @@ export async function executeParallelSubprocess(
975
1004
  const sessionDir = shouldResumeThisSession && savedSessionDirExists
976
1005
  ? savedSessionDir
977
1006
  : getSessionDir?.(index, t);
978
- const result = await runAgentSubprocess({
979
- cwd: defaultCwd,
980
- agents,
981
- agentName: t.agent,
982
- task: t.task,
983
- parentDepth,
984
- parentAgentStack,
985
- maxDepth,
986
- preventCycles,
987
- signal,
988
- sessionDir,
989
- sessionRoot,
990
- resumeSession: shouldResumeThisSession && !!sessionDir,
991
- initialResult: previousResult,
992
- fallbackModel,
993
- onUpdate: (partial) => {
994
- if (partial.details?.results[0]) {
995
- allResults[index] = partial.details.results[0];
996
- emitProgress();
997
- }
998
- },
999
- makeDetails,
1000
- });
1007
+ let result: SingleResult;
1008
+ try {
1009
+ result = await runAgentSubprocess({
1010
+ cwd: defaultCwd,
1011
+ agents,
1012
+ agentName: t.agent,
1013
+ task: t.task,
1014
+ parentDepth,
1015
+ parentAgentStack,
1016
+ maxDepth,
1017
+ preventCycles,
1018
+ signal,
1019
+ sessionDir,
1020
+ sessionRoot,
1021
+ resumeSession: shouldResumeThisSession && !!sessionDir,
1022
+ initialResult: previousResult,
1023
+ fallbackModel,
1024
+ onHandle: (handle) => onHandleForTask?.(index, t, handle),
1025
+ onUpdate: (partial) => {
1026
+ if (partial.details?.results[0]) {
1027
+ allResults[index] = partial.details.results[0];
1028
+ emitProgress();
1029
+ }
1030
+ },
1031
+ makeDetails,
1032
+ });
1033
+ } finally {
1034
+ onTaskDone?.(index, t);
1035
+ }
1001
1036
  allResults[index] = result;
1002
1037
  emitProgress();
1003
1038
  return result;