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.
- package/index.ts +357 -23
- package/package.json +1 -1
- package/render.ts +13 -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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
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
|
-
|
|
364
|
+
nodes.forEach((node, index) => {
|
|
358
365
|
const indent = " ".repeat(depth);
|
|
359
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
504
|
-
|
|
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: ["
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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;
|