oira666_pi-subagent 0.3.14 → 0.3.16

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 CHANGED
@@ -152,15 +152,33 @@ pi --no-subagent-prevent-cycles # allow cycles (not recommended)
152
152
  | `PI_SUBAGENT_MAX_PARALLEL_TASKS` | `30` | Max tasks per single call |
153
153
  | `PI_SUBAGENT_MAX_CONCURRENCY` | `8` | Max subagents running simultaneously |
154
154
 
155
+ ## Child Process Environment
156
+
157
+ Children inherit all provider, authentication, proxy, home, temp, and Pi
158
+ environment variables. The runner also repairs the executable search path for
159
+ elevated Windows PowerShell and pnpm installations: it normalizes duplicate
160
+ `Path`/`PATH` keys and adds the Node directory, `PNPM_HOME`, npm's user bin,
161
+ `%LOCALAPPDATA%\\pnpm`, and `%SystemRoot%\\System32`. This also applies to every
162
+ nested child, so tools and sub-subagents use the same working environment.
163
+
164
+ Pi itself is relaunched with the current runtime and entrypoint
165
+ (`process.execPath process.argv[1]`). The extension does not inspect npm/pnpm
166
+ shims or assume package names, `node_modules` locations, or Pi `dist` layouts.
167
+ This also supports Bun and other Node-compatible runtimes with the same process
168
+ semantics. Embedded hosts without a script entrypoint can use
169
+ `PI_SUBAGENT_PI_COMMAND` and `PI_SUBAGENT_PI_ARGS_PREFIX` explicitly.
170
+
155
171
  ## Subagent Liveness Timeouts
156
172
 
157
173
  A delegated process cannot block its parents forever. The runner applies a
158
- startup timeout before the first model turn and a semantic-inactivity timeout
159
- after startup. Tool/turn events and changed nested-agent state reset the idle
160
- timer; repeated unchanged progress heartbeats do not. On timeout or
161
- cancellation, the runner terminates the child process tree and bounds cleanup;
162
- even if a wedged OS process never reports `close`, the tool returns an error
163
- result so every waiting parent can settle.
174
+ startup timeout before the first model turn and an agent-inactivity timeout
175
+ after startup. The inactivity watchdog is paused while any tool call is in
176
+ progress, so tool executions can run for unlimited time; a fresh full idle
177
+ window starts after the last concurrent tool finishes. Agent/turn events and
178
+ changed nested-agent state reset the idle timer; repeated unchanged progress
179
+ heartbeats do not. On timeout or cancellation, the runner terminates the child
180
+ process tree and bounds cleanup; even if a wedged OS process never reports
181
+ `close`, the tool returns an error result so every waiting parent can settle.
164
182
 
165
183
  RPC completion is based on Pi's `agent_settled` event—not `agent_end`.
166
184
  `agent_end` is only a low-level run boundary and may be followed by Pi's normal
@@ -172,15 +190,16 @@ reported immediately as failures.
172
190
  | --- | --- | --- |
173
191
  | `PI_SUBAGENT_STARTUP_TIMEOUT` | `120000` | Milliseconds allowed to reach the first model turn; `0` disables |
174
192
  | `PI_SUBAGENT_STARTUP_RETRIES` | `2` | Fresh retries after a startup timeout |
175
- | `PI_SUBAGENT_IDLE_TIMEOUT` | `1200000` | Milliseconds without new semantic RPC activity after startup; `0` disables |
193
+ | `PI_SUBAGENT_IDLE_TIMEOUT` | `1200000` | Milliseconds without agent activity after startup, excluding time spent in ongoing tool calls; `0` disables |
176
194
 
177
195
  ## Timestamps & Status Footer
178
196
 
179
- Subagent tool calls and live activity lines render a dim `hh:mm:ss` timestamp
180
- (call start, per-subagent run start, and each live log entry). The collapsed tool
181
- row keeps the normal compact progress text. Press `Ctrl+O` to expand it into the
182
- subagent tree immediately; every running node shows its latest six activity
183
- lines (thinking, tool starts/completions, and completed turns).
197
+ Subagent tool calls and live activity lines render a dim `hh:mm:ss` timestamp.
198
+ The collapsed view shows every direct child as `Name (agent-type)`, two prompt
199
+ lines, its current status, and `last action`. That timestamp is the newest
200
+ activity anywhere in the child's recursive subtree, so active grandchildren
201
+ keep their ancestor visibly alive. Press `Ctrl+O` to expand the full recursive
202
+ tree, including every agent's complete prompt and latest activity lines.
184
203
 
185
204
  In the interactive TUI the extension publishes the combined `total` usage line
186
205
  (parent + all subagents, recursively) via Pi's normal `ctx.ui.setStatus()`
@@ -188,7 +207,7 @@ status line. Pi renders all extension statuses on the same footer status line.
188
207
 
189
208
  ## Steering Running Subagents
190
209
 
191
- While a `subagents` tool call is running, mid-stream steering input can be broadcast to one or more child agents. The extension uses Pi's `InputEvent.streamingBehavior` metadata when available, so idle prompts and queued follow-ups continue to the parent normally; only true `steer` inputs open the broadcast routing prompt.
210
+ While a `subagents` tool call is running, mid-stream steering input can be broadcast to one or more child agents. Targets are selected by human name; nested targets use paths such as `John > Maria > Elena`. The extension uses Pi's `InputEvent.streamingBehavior` metadata when available, so idle prompts and queued follow-ups continue to the parent normally; only true `steer` inputs open the broadcast routing prompt.
192
211
 
193
212
  ## Subagent Session Resume
194
213
 
@@ -215,16 +234,16 @@ Note: crash-resume covers `subagents` calls only. An interrupted `resume_subagen
215
234
 
216
235
  ## Resumable Subagents by Name (`resume_subagents`)
217
236
 
218
- Every subagent run is assigned a unique, durable name derived from its agent
219
- type plus a per-type counter — `code-writer-01`, `code-writer-02`,
220
- `code-reviewer-01`, ... The name is returned together with the subagent's
221
- results and shown in the TUI tree.
237
+ Every subagent run is assigned a random, durable human first name from a bundled
238
+ list of 500 popular American names — for example `John`, `Maria`, or `Elena`.
239
+ Names are never reused anywhere in the same delegation tree. The name is
240
+ returned together with the agent type and shown in every TUI view.
222
241
 
223
242
  The `resume_subagents` tool continues named subagents with a new task while
224
243
  preserving their full previous context:
225
244
 
226
245
  ```json
227
- { "resumes": [{ "subagent": "code-writer-01", "task": "Now also update the tests." }] }
246
+ { "resumes": [{ "subagent": "John", "task": "Now also update the tests." }] }
228
247
  ```
229
248
 
230
249
  Naming is deliberately unambiguous: `agent` (in `subagents`) selects an agent
@@ -0,0 +1,56 @@
1
+ /** 500 common American first names used for human-friendly subagent identities. */
2
+ export const AMERICAN_NAMES = [
3
+ // Common masculine names
4
+ "James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph", "Thomas", "Christopher",
5
+ "Charles", "Daniel", "Matthew", "Anthony", "Mark", "Donald", "Steven", "Andrew", "Paul", "Joshua",
6
+ "Kenneth", "Kevin", "Brian", "George", "Timothy", "Ronald", "Edward", "Jason", "Jeffrey", "Ryan",
7
+ "Jacob", "Gary", "Nicholas", "Eric", "Jonathan", "Stephen", "Larry", "Justin", "Scott", "Brandon",
8
+ "Benjamin", "Samuel", "Gregory", "Alexander", "Patrick", "Frank", "Raymond", "Jack", "Dennis", "Jerry",
9
+ "Tyler", "Aaron", "Jose", "Adam", "Nathan", "Henry", "Douglas", "Zachary", "Peter", "Kyle",
10
+ "Walter", "Ethan", "Jeremy", "Harold", "Keith", "Christian", "Roger", "Noah", "Gerald", "Carl",
11
+ "Terry", "Sean", "Austin", "Arthur", "Lawrence", "Jesse", "Dylan", "Bryan", "Joe", "Jordan",
12
+ "Billy", "Bruce", "Albert", "Willie", "Gabriel", "Logan", "Alan", "Juan", "Wayne", "Roy",
13
+ "Ralph", "Randy", "Eugene", "Vincent", "Russell", "Elijah", "Louis", "Bobby", "Philip", "Johnny",
14
+ "Howard", "Isaac", "Harry", "Victor", "Martin", "Ernest", "Phillip", "Todd", "Craig", "Shawn",
15
+ "Clarence", "Carlos", "Caleb", "Blake", "Mason", "Luke", "Liam", "Owen", "Nathaniel", "Cameron",
16
+ "Hunter", "Connor", "Adrian", "Julian", "Jeremiah", "Angel", "Isaiah", "Evan", "Colton", "Gavin",
17
+ "Dominic", "Cooper", "Ian", "Carson", "Jaxon", "Theodore", "Hudson", "Lincoln", "Asher", "Wyatt",
18
+ "Leo", "Ezra", "Miles", "Micah", "Silas", "Roman", "Wesley", "Brooks", "Bennett", "Parker",
19
+ "Beau", "Weston", "Damian", "Sawyer", "Emmett", "Harrison", "Cole", "Maxwell", "Holden", "Antonio",
20
+ "Miguel", "Oscar", "Diego", "Alejandro", "Marco", "Luis", "Javier", "Sergio", "Ricardo", "Manuel",
21
+ "Roberto", "Francisco", "Eduardo", "Fernando", "Ruben", "Raul", "Andres", "Hector", "Edgar", "Mario",
22
+ "Derek", "Trevor", "Travis", "Cody", "Dustin", "Corey", "Brett", "Chad", "Bradley", "Spencer",
23
+ "Grant", "Joel", "Dean", "Dale", "Glenn", "Stanley", "Leonard", "Francis", "Herbert", "Frederick",
24
+ "Norman", "Melvin", "Marvin", "Gordon", "Allen", "Bernard", "Clifford", "Warren", "Curtis", "Darren",
25
+ "Troy", "Marcus", "Derrick", "Andre", "Terrence", "Maurice", "Darius", "Malcolm", "Xavier", "Preston",
26
+ "Grayson", "Easton", "Nolan", "Everett", "Axel", "Emmanuel", "Kingston", "Elliot", "Tim", "Simon",
27
+ "Riley", "Finn", "Jasper", "Milo", "Rowan", "Declan", "Graham", "Max", "Nash", "Reid",
28
+ "Lane", "Tucker", "Carter", "Chase", "Coleman", "Bryce", "Hayden", "Aidan", "Jonah", "Jonas",
29
+
30
+ // Common feminine names
31
+ "Mary", "Patricia", "Jennifer", "Linda", "Elizabeth", "Barbara", "Susan", "Jessica", "Sarah", "Karen",
32
+ "Lisa", "Nancy", "Betty", "Margaret", "Sandra", "Ashley", "Kimberly", "Emily", "Donna", "Michelle",
33
+ "Carol", "Amanda", "Melissa", "Deborah", "Stephanie", "Dorothy", "Rebecca", "Sharon", "Laura", "Cynthia",
34
+ "Amy", "Kathleen", "Angela", "Shirley", "Brenda", "Emma", "Anna", "Pamela", "Nicole", "Samantha",
35
+ "Katherine", "Christine", "Debra", "Rachel", "Carolyn", "Janet", "Maria", "Heather", "Diane", "Julie",
36
+ "Joyce", "Victoria", "Kelly", "Christina", "Lauren", "Joan", "Evelyn", "Olivia", "Judith", "Megan",
37
+ "Cheryl", "Martha", "Andrea", "Frances", "Hannah", "Jacqueline", "Ann", "Gloria", "Jean", "Kathryn",
38
+ "Alice", "Teresa", "Sara", "Janice", "Doris", "Madison", "Julia", "Grace", "Judy", "Abigail",
39
+ "Marie", "Denise", "Beverly", "Amber", "Theresa", "Marilyn", "Danielle", "Diana", "Brittany", "Natalie",
40
+ "Sophia", "Rose", "Isabella", "Alexis", "Kayla", "Charlotte", "Lori", "Alexandra", "Savannah", "Brooklyn",
41
+ "Bella", "Claire", "Skylar", "Lucy", "Paisley", "Everly", "Caroline", "Nova", "Genesis", "Emilia",
42
+ "Kennedy", "Maya", "Willow", "Kinsley", "Naomi", "Aaliyah", "Elena", "Mckenna", "Ariana", "Allison",
43
+ "Gabriella", "Margot", "Madelyn", "Cora", "Ruby", "Eva", "Serenity", "Autumn", "Adeline", "Hailey",
44
+ "Gianna", "Valentina", "Isla", "Eliana", "Quinn", "Nevaeh", "Ivy", "Sadie", "Piper", "Lydia",
45
+ "Alexa", "Josephine", "Emery", "Phoebe", "Delilah", "Arianna", "Vivian", "Kaylee", "Sophie", "Brielle",
46
+ "Madeline", "Peyton", "Rylee", "Clara", "Hadley", "Melanie", "Mackenzie", "Reagan", "Adalynn", "Liliana",
47
+ "Aubree", "Jade", "Miranda", "Isabelle", "Natalia", "Raelynn", "Dakota", "Athena", "Ximena", "Arya",
48
+ "Leilani", "Taylor", "Faith", "Lena", "Kylie", "Miriam", "Kate", "Summer", "Lyla", "Brooke",
49
+ "Amaya", "Eliza", "Brianna", "Bailey", "Nina", "Khloe", "Jasmine", "Melody", "Isabel", "Norah",
50
+ "Annabelle", "Valeria", "Emerson", "Cecilia", "Valerie", "Molly", "Reese", "Aliyah", "Lilly", "Blair",
51
+ "Finley", "Morgan", "Sydney", "Jordyn", "Eloise", "Trinity", "Daisy", "Catherine", "Lola", "Genevieve",
52
+ "Marley", "Arabella", "Harmony", "Elise", "Remi", "Teagan", "Evie", "London", "Sloane", "Laila",
53
+ "Lucia", "Willa", "Juliana", "Gracie", "June", "Tessa", "Ada", "Camille", "Hope", "Samara",
54
+ "Rosalie", "Ruth", "Fiona", "Georgia", "Noelle", "Vanessa", "Daniela", "Paige", "Violet", "Presley",
55
+ "Adriana", "Joanna", "Giselle", "Harper", "Avery", "Scarlett", "Aria", "Ellie", "Chloe", "Layla",
56
+ ] as const;
package/index.ts CHANGED
@@ -45,7 +45,6 @@ import {
45
45
  renderCall,
46
46
  renderResult,
47
47
  renderResumeCall,
48
- setBroadcastNumberingActive,
49
48
  } from "./render.js";
50
49
  import { runAgentSubprocess, executeParallelSubprocess, type RunningSubagentHandle } from "./runner.js";
51
50
  import {
@@ -166,7 +165,7 @@ const SubagentParams = Type.Object({
166
165
 
167
166
  const ResumeItem = Type.Object({
168
167
  subagent: Type.String({
169
- description: "Unique subagent name returned by a previous subagents run (e.g. code-writer-01)",
168
+ description: "Unique human name returned by a previous subagents run (e.g. John)",
170
169
  }),
171
170
  task: Type.String({
172
171
  description: "New task for the resumed subagent. It keeps its previous context.",
@@ -1001,7 +1000,7 @@ export default function (pi: ExtensionAPI) {
1001
1000
  let pendingResumePlans: ResumableSubagentCall[] = [];
1002
1001
  let modelToRestoreAfterResume: any | undefined;
1003
1002
  const approvedProjectAgentDirsForSession = new Set<string>();
1004
- const activeSubagents = new Map<number, { agent: string; task: string; taskIndex: number; handle: RunningSubagentHandle; name?: string }>();
1003
+ const activeSubagents = new Map<number, { agent: string; task: string; handle: RunningSubagentHandle; name?: string }>();
1005
1004
  /** Names with a resume in flight in this process (same-process race guard). */
1006
1005
  const activeResumeNames = new Set<string>();
1007
1006
  /** Tool calls whose execute result requested an actual Pi error result. */
@@ -1083,9 +1082,10 @@ export default function (pi: ExtensionAPI) {
1083
1082
  const BROADCAST_STEER_PREFIX = "__PI_SUBAGENT_BROADCAST_STEER__";
1084
1083
 
1085
1084
  interface BroadcastTarget {
1085
+ /** Human-name path, e.g. "John > Maria > Elena". */
1086
1086
  display: string;
1087
1087
  topLevelId: number;
1088
- restPath: number[];
1088
+ restPath: string[];
1089
1089
  }
1090
1090
 
1091
1091
  interface ParsedBroadcastSelection {
@@ -1093,83 +1093,40 @@ export default function (pi: ExtensionAPI) {
1093
1093
  errors: string[];
1094
1094
  }
1095
1095
 
1096
- function parseBroadcastPath(raw: string): number[] | null {
1097
- const parts = raw.trim().split(".");
1098
- if (parts.length === 0) return null;
1099
- const path: number[] = [];
1100
- for (const part of parts) {
1101
- if (!/^\d+$/.test(part)) return null;
1102
- const value = Number(part);
1103
- if (!Number.isSafeInteger(value) || value < 1) return null;
1104
- path.push(value);
1096
+ function parseBroadcastSelection(input: string, available: BroadcastTarget[]): ParsedBroadcastSelection {
1097
+ if (input.trim().toUpperCase() === "ALL") return { targets: [...available], errors: [] };
1098
+ const byName = new Map<string, BroadcastTarget>();
1099
+ for (const target of available) {
1100
+ byName.set(target.display.toLowerCase(), target);
1101
+ // Names are globally unique across the delegation tree, so a nested leaf
1102
+ // can be addressed as "Maria" without spelling "John > Maria".
1103
+ const leafName = target.display.split(" > ").at(-1);
1104
+ if (leafName) byName.set(leafName.toLowerCase(), target);
1105
1105
  }
1106
- return path;
1107
- }
1108
-
1109
- function parseBroadcastSelection(input: string, available: number[]): ParsedBroadcastSelection {
1110
- const normalized = input.trim().toUpperCase();
1111
- if (normalized === "ALL") {
1112
- return {
1113
- targets: available.map((id) => ({ display: String(id), topLevelId: id, restPath: [] })),
1114
- errors: [],
1115
- };
1116
- }
1117
-
1106
+ const targets: BroadcastTarget[] = [];
1118
1107
  const errors: string[] = [];
1119
- const targetMap = new Map<string, BroadcastTarget>();
1120
1108
  for (const rawPart of input.split(",")) {
1121
- const part = rawPart.trim();
1122
- if (!part) continue;
1123
-
1124
- if (part.includes("-")) {
1125
- const [startRaw, endRaw, extra] = part.split("-").map((p) => p.trim());
1126
- const startPath = parseBroadcastPath(startRaw);
1127
- const endPath = parseBroadcastPath(endRaw);
1128
- if (extra !== undefined || !startPath || !endPath || startPath.length !== 1 || endPath.length !== 1) {
1129
- errors.push(`Invalid range "${part}". Use top-level ranges like 1-3.`);
1130
- continue;
1131
- }
1132
- const start = startPath[0];
1133
- const end = endPath[0];
1134
- for (let id = Math.min(start, end); id <= Math.max(start, end); id++) {
1135
- if (!available.includes(id)) {
1136
- errors.push(`Subagent ${id} is not running.`);
1137
- continue;
1138
- }
1139
- targetMap.set(String(id), { display: String(id), topLevelId: id, restPath: [] });
1140
- }
1141
- continue;
1142
- }
1143
-
1144
- const path = parseBroadcastPath(part);
1145
- if (!path) {
1146
- errors.push(`Invalid target "${part}". Use ALL, numbers, paths, or top-level ranges (e.g. 1, 2, 4.1, 1-3).`);
1147
- continue;
1148
- }
1149
- const [topLevelId, ...restPath] = path;
1150
- if (!available.includes(topLevelId)) {
1151
- errors.push(`Subagent ${topLevelId} is not running.`);
1152
- continue;
1153
- }
1154
- const display = path.join(".");
1155
- targetMap.set(display, { display, topLevelId, restPath });
1109
+ const requested = rawPart.trim();
1110
+ if (!requested) continue;
1111
+ const target = byName.get(requested.toLowerCase());
1112
+ if (target) targets.push(target);
1113
+ else errors.push(`Subagent "${requested}" is not running. Use its human name.`);
1156
1114
  }
1157
-
1158
1115
  return {
1159
- targets: Array.from(targetMap.values()).sort((a, b) => a.display.localeCompare(b.display, undefined, { numeric: true })),
1116
+ targets: Array.from(new Map(targets.map((target) => [target.display, target])).values()),
1160
1117
  errors,
1161
1118
  };
1162
1119
  }
1163
1120
 
1164
- function encodeNestedBroadcast(message: string, path: number[]): string {
1121
+ function encodeNestedBroadcast(message: string, path: string[]): string {
1165
1122
  return `${BROADCAST_STEER_PREFIX}${JSON.stringify({ path, message })}`;
1166
1123
  }
1167
1124
 
1168
- function decodeNestedBroadcast(text: string): { path: number[]; message: string } | null {
1125
+ function decodeNestedBroadcast(text: string): { path: string[]; message: string } | null {
1169
1126
  if (!text.startsWith(BROADCAST_STEER_PREFIX)) return null;
1170
1127
  try {
1171
1128
  const parsed = JSON.parse(text.slice(BROADCAST_STEER_PREFIX.length));
1172
- if (!Array.isArray(parsed?.path) || !parsed.path.every((n: unknown) => Number.isSafeInteger(n) && (n as number) >= 1)) return null;
1129
+ if (!Array.isArray(parsed?.path) || !parsed.path.every((name: unknown) => typeof name === "string" && name.length > 0)) return null;
1173
1130
  if (typeof parsed?.message !== "string") return null;
1174
1131
  return { path: parsed.path, message: parsed.message };
1175
1132
  } catch {
@@ -1177,66 +1134,37 @@ export default function (pi: ExtensionAPI) {
1177
1134
  }
1178
1135
  }
1179
1136
 
1180
- function extractPendingSubagentTaskCounts(result: SingleResult): number[] {
1181
- const messages = Array.isArray((result as any).messages) ? (result as any).messages : [];
1182
- const completedToolCallIds = new Set(getNestedSubagentResults(messages).map((nested) => nested.toolCallId));
1183
- const counts: number[] = [];
1184
- for (const message of messages) {
1185
- if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
1186
- for (const part of message.content) {
1187
- if (part?.type !== "toolCall" || !isSubagentToolName(part?.name)) continue;
1188
- const toolCallId = typeof part.toolCallId === "string" ? part.toolCallId : typeof part.id === "string" ? part.id : undefined;
1189
- if (toolCallId && completedToolCallIds.has(toolCallId)) continue;
1190
- const rawResumes = part.arguments?.resumes;
1191
- const tasks = Array.isArray(part.arguments?.tasks)
1192
- ? part.arguments.tasks
1193
- : Array.isArray(rawResumes)
1194
- ? rawResumes
1195
- : rawResumes && typeof rawResumes === "object"
1196
- ? [rawResumes]
1197
- : [];
1198
- if (tasks.length > 0) counts.push(tasks.length);
1199
- }
1200
- }
1201
- return counts;
1202
- }
1203
-
1204
1137
  function collectRunningBroadcastTargetsFromResult(
1205
1138
  result: SingleResult,
1206
- path: number[],
1139
+ namePath: string[],
1140
+ topLevelId: number,
1207
1141
  targets: { all: BroadcastTarget[]; youngest: BroadcastTarget[] },
1208
1142
  ): boolean {
1209
- const nestedRunningPaths: number[][] = [];
1210
-
1143
+ let hasRunningDescendant = false;
1144
+ const completedIds = new Set<string>();
1145
+ const nestedDetails: SubagentDetails[] = [];
1211
1146
  for (const nested of getNestedSubagentResults(result.messages)) {
1212
1147
  if (!isSubagentDetails(nested.details)) continue;
1213
- nested.details.results.forEach((child, index) => {
1214
- const childPath = [...path, index + 1];
1215
- if (collectRunningBroadcastTargetsFromResult(child, childPath, targets)) {
1216
- nestedRunningPaths.push(childPath);
1217
- }
1218
- });
1148
+ if (nested.toolCallId) completedIds.add(nested.toolCallId);
1149
+ nestedDetails.push(nested.details);
1219
1150
  }
1220
-
1221
- if (result.exitCode === -1) {
1222
- for (const taskCount of extractPendingSubagentTaskCounts(result)) {
1223
- for (let i = 1; i <= taskCount; i++) {
1224
- const childPath = [...path, i];
1225
- const [topLevelId, ...restPath] = childPath;
1226
- targets.all.push({ display: childPath.join("."), topLevelId, restPath });
1227
- targets.youngest.push({ display: childPath.join("."), topLevelId, restPath });
1228
- nestedRunningPaths.push(childPath);
1151
+ for (const [toolCallId, live] of Object.entries(result.liveNestedSubagents ?? {})) {
1152
+ if (!completedIds.has(toolCallId) && isSubagentDetails(live)) nestedDetails.push(live);
1153
+ }
1154
+ for (const details of nestedDetails) {
1155
+ for (const child of details.results) {
1156
+ const childName = child.name || child.agent;
1157
+ if (collectRunningBroadcastTargetsFromResult(child, [...namePath, childName], topLevelId, targets)) {
1158
+ hasRunningDescendant = true;
1229
1159
  }
1230
1160
  }
1231
1161
  }
1232
1162
 
1233
- const isRunning = result.exitCode === -1;
1234
- if (!isRunning) return nestedRunningPaths.length > 0;
1235
-
1236
- const [topLevelId, ...restPath] = path;
1237
- const self = { display: path.join("."), topLevelId, restPath };
1163
+ if (result.exitCode !== -1) return hasRunningDescendant;
1164
+ const restPath = namePath.slice(1);
1165
+ const self = { display: namePath.join(" > "), topLevelId, restPath };
1238
1166
  targets.all.push(self);
1239
- if (nestedRunningPaths.length === 0) targets.youngest.push(self);
1167
+ if (!hasRunningDescendant) targets.youngest.push(self);
1240
1168
  return true;
1241
1169
  }
1242
1170
 
@@ -1244,28 +1172,34 @@ export default function (pi: ExtensionAPI) {
1244
1172
  latestBroadcastTargets.all = [];
1245
1173
  latestBroadcastTargets.youngest = [];
1246
1174
  if (!details) {
1247
- for (const id of activeSubagents.keys()) {
1248
- const target = { display: String(id), topLevelId: id, restPath: [] };
1175
+ for (const [id, active] of activeSubagents) {
1176
+ const display = active.name || active.agent;
1177
+ const target = { display, topLevelId: id, restPath: [] };
1249
1178
  latestBroadcastTargets.all.push(target);
1250
1179
  latestBroadcastTargets.youngest.push(target);
1251
1180
  }
1252
1181
  return;
1253
1182
  }
1254
1183
  details.results.forEach((result, index) => {
1255
- collectRunningBroadcastTargetsFromResult(result, [topLevelBaseId + index], latestBroadcastTargets);
1184
+ collectRunningBroadcastTargetsFromResult(
1185
+ result,
1186
+ [result.name || result.agent],
1187
+ topLevelBaseId + index,
1188
+ latestBroadcastTargets,
1189
+ );
1256
1190
  });
1257
1191
  const dedupe = (targets: BroadcastTarget[]) =>
1258
1192
  Array.from(new Map(targets.map((target) => [target.display, target])).values())
1259
1193
  .filter((target) => activeSubagents.has(target.topLevelId))
1260
- .sort((a, b) => a.display.localeCompare(b.display, undefined, { numeric: true }));
1194
+ .sort((a, b) => a.display.localeCompare(b.display));
1261
1195
  latestBroadcastTargets.all = dedupe(latestBroadcastTargets.all);
1262
1196
  latestBroadcastTargets.youngest = dedupe(latestBroadcastTargets.youngest);
1263
1197
  }
1264
1198
 
1265
1199
  function getFallbackTopLevelTargets(): BroadcastTarget[] {
1266
- return Array.from(activeSubagents.keys())
1267
- .sort((a, b) => a - b)
1268
- .map((id) => ({ display: String(id), topLevelId: id, restPath: [] }));
1200
+ return Array.from(activeSubagents.entries())
1201
+ .sort(([a], [b]) => a - b)
1202
+ .map(([id, active]) => ({ display: active.name || active.agent, topLevelId: id, restPath: [] }));
1269
1203
  }
1270
1204
 
1271
1205
  function sendBroadcastToTargets(message: string, targets: BroadcastTarget[], ctx: any): void {
@@ -1297,30 +1231,24 @@ export default function (pi: ExtensionAPI) {
1297
1231
  if (nested) {
1298
1232
  if (activeSubagents.size === 0) return "handled";
1299
1233
  const [rawTarget, ...restPath] = nested.path;
1300
- // Path components below the top level are 1-based task indexes within the
1301
- // subagent tool call running in THIS process. Internal active ids keep
1302
- // growing across sequential tool calls (and process restarts), so resolve
1303
- // by task index first; fall back to a direct id match for compatibility.
1304
1234
  let resolvedId: number | undefined;
1305
1235
  for (const [id, item] of activeSubagents) {
1306
- if (item.taskIndex === rawTarget - 1 && (resolvedId === undefined || id > resolvedId)) {
1236
+ if ((item.name || item.agent).toLowerCase() === rawTarget.toLowerCase()) {
1307
1237
  resolvedId = id;
1238
+ break;
1308
1239
  }
1309
1240
  }
1310
- if (resolvedId === undefined && activeSubagents.has(rawTarget)) resolvedId = rawTarget;
1311
1241
  if (resolvedId === undefined) return "handled";
1312
- sendBroadcastToTargets(nested.message, [{ display: nested.path.join("."), topLevelId: resolvedId, restPath }], ctx);
1242
+ sendBroadcastToTargets(nested.message, [{ display: nested.path.join(" > "), topLevelId: resolvedId, restPath }], ctx);
1313
1243
  return "handled";
1314
1244
  }
1315
1245
 
1316
1246
  if (!ctx.hasUI || activeSubagents.size === 0) return "continue";
1317
1247
 
1318
- setBroadcastNumberingActive(true);
1319
1248
  try {
1320
- const available = Array.from(activeSubagents.keys()).sort((a, b) => a - b);
1321
1249
  const choice = await ctx.ui.select(
1322
1250
  "Broadcast this steering message to subagents?",
1323
- ["No", "All (+nested)", "Youngest", "Numbers (e.g. 1, 2, 4.1)"],
1251
+ ["No", "All (+nested)", "Youngest", "Names (e.g. John, Maria)"],
1324
1252
  );
1325
1253
  if (choice === "All (+nested)" || choice === "Youngest") {
1326
1254
  const fallback = getFallbackTopLevelTargets();
@@ -1335,29 +1263,19 @@ export default function (pi: ExtensionAPI) {
1335
1263
  sendBroadcastToTargets(message, current, ctx);
1336
1264
  return "handled";
1337
1265
  }
1338
- if (choice !== "Numbers (e.g. 1, 2, 4.1)") return "continue";
1266
+ if (choice !== "Names (e.g. John, Maria)") return "continue";
1339
1267
 
1340
1268
  const answer = await ctx.ui.input(
1341
- "Subagent numbers/ranges to broadcast to (e.g. 1, 2, 4.1)",
1269
+ "Subagent names to broadcast to (comma-separated)",
1342
1270
  "",
1343
1271
  );
1344
1272
  if (!answer) return "continue";
1345
- const current = Array.from(new Set([
1346
- ...available,
1347
- ...latestBroadcastTargets.all.map((target) => target.topLevelId),
1348
- ])).sort((a, b) => a - b);
1273
+ const current = latestBroadcastTargets.all.length > 0
1274
+ ? latestBroadcastTargets.all
1275
+ : getFallbackTopLevelTargets();
1349
1276
  const parsed = parseBroadcastSelection(answer, current);
1350
- const knownDisplays = new Set(latestBroadcastTargets.all.map((target) => target.display));
1351
- const validatedTargets = knownDisplays.size > 0
1352
- ? parsed.targets.filter((target) => knownDisplays.has(target.display))
1353
- : parsed.targets;
1354
- const unknownNested = knownDisplays.size > 0
1355
- ? parsed.targets.filter((target) => !knownDisplays.has(target.display)).map((target) => target.display)
1356
- : [];
1357
- const errors = [
1358
- ...parsed.errors,
1359
- ...unknownNested.map((display) => `Subagent ${display} is not running.`),
1360
- ];
1277
+ const validatedTargets = parsed.targets;
1278
+ const errors = parsed.errors;
1361
1279
  if (errors.length > 0) {
1362
1280
  ctx.ui.notify(errors.slice(0, 4).join("\n"), "warning");
1363
1281
  }
@@ -1368,7 +1286,7 @@ export default function (pi: ExtensionAPI) {
1368
1286
  sendBroadcastToTargets(message, validatedTargets, ctx);
1369
1287
  return "handled";
1370
1288
  } finally {
1371
- setBroadcastNumberingActive(false);
1289
+ // No persistent steering UI state.
1372
1290
  }
1373
1291
  }
1374
1292
 
@@ -1790,13 +1708,13 @@ ${preventCycles
1790
1708
  ? ""
1791
1709
  : configuredToolPrompts[RESUME_SUBAGENTS_TOOL_NAME] ?? `### Resumable subagents
1792
1710
 
1793
- Every subagent run is assigned a unique, durable name (e.g. \`code-writer-01\`,
1794
- \`code-reviewer-02\`) which is returned together with its results. Use the
1711
+ Every subagent run is assigned a unique, durable human name (e.g. \`John\`,
1712
+ \`Maria\`) which is returned together with its results. Use the
1795
1713
  \`resume_subagents\` tool to continue named subagents with a new task while
1796
1714
  keeping their full previous context:
1797
1715
 
1798
1716
  \`\`\`json
1799
- { "resumes": [{ "subagent": "code-writer-01", "task": "Now also update the tests." }] }
1717
+ { "resumes": [{ "subagent": "John", "task": "Now also update the tests." }] }
1800
1718
  \`\`\`
1801
1719
 
1802
1720
  - \`agent\` (in \`subagents\`) is an agent TYPE; \`subagent\` (in \`resume_subagents\`)
@@ -1937,7 +1855,7 @@ ${agentList}\n\n${subagentsGuidance}\n\n${delegationGuardGuidance}${resumeGuidan
1937
1855
  pendingResumePlans.splice(resumePlanIndex, 1);
1938
1856
  }
1939
1857
 
1940
- // Assign durable, tree-unique names (code-writer-01, ...). Resumed
1858
+ // Assign random durable human names unique across the whole tree. Resumed
1941
1859
  // runs keep the names already recorded in the previous results.
1942
1860
  const names: Array<string | undefined> = tasks.map(
1943
1861
  (_task, index) => resumePlan?.details?.results[index]?.name,
@@ -1969,7 +1887,7 @@ ${agentList}\n\n${subagentsGuidance}\n\n${delegationGuardGuidance}${resumeGuidan
1969
1887
  names[index] = allocated[allocIndex];
1970
1888
  });
1971
1889
  } catch (err) {
1972
- console.warn("[pi-subagent] Failed to allocate subagent names (continuing without):", err);
1890
+ throw new Error(`Failed to allocate unique subagent names: ${err instanceof Error ? err.message : String(err)}`);
1973
1891
  }
1974
1892
  }
1975
1893
  }
@@ -2037,10 +1955,10 @@ ${agentList}\n\n${subagentsGuidance}\n\n${delegationGuardGuidance}${resumeGuidan
2037
1955
  description: configuredToolPrompts[RESUME_SUBAGENTS_TOOL_NAME] ?? [
2038
1956
  "Resume previously run subagents by name with a new task, keeping their full context.",
2039
1957
  "",
2040
- "Every subagent run returns a unique name (e.g. code-writer-01). Pass those names",
1958
+ "Every subagent run returns a unique human name (e.g. John). Pass those names",
2041
1959
  "as `subagent` to continue them. All resumes in one call run IN PARALLEL.",
2042
1960
  "",
2043
- 'Example: { resumes: [{ subagent: "code-writer-01", task: "Also update the tests." }] }',
1961
+ 'Example: { resumes: [{ subagent: "John", task: "Also update the tests." }] }',
2044
1962
  ].join("\n"),
2045
1963
  parameters: ResumeSubagentsParams,
2046
1964
  prepareArguments: prepareResumeArguments,
@@ -2274,7 +2192,9 @@ ${agentList}\n\n${subagentsGuidance}\n\n${delegationGuardGuidance}${resumeGuidan
2274
2192
  // -----------------------------------------------------------------------
2275
2193
 
2276
2194
  function formatNameSuffix(result: SingleResult | undefined): string {
2277
- return result?.name ? `\n\n(subagent name: ${result.name} — resumable via resume_subagents)` : "";
2195
+ return result?.name
2196
+ ? `\n\n(subagent: ${result.name} (${result.agent}) — resume with the name ${result.name})`
2197
+ : "";
2278
2198
  }
2279
2199
 
2280
2200
  async function executeSingle(
@@ -2326,7 +2246,7 @@ ${agentList}\n\n${subagentsGuidance}\n\n${delegationGuardGuidance}${resumeGuidan
2326
2246
  fallbackModel,
2327
2247
  onHandle: (handle) => {
2328
2248
  activeId = topLevelBaseId;
2329
- activeSubagents.set(activeId, { agent: agentName, task, taskIndex: 0, handle, name: subagentName });
2249
+ activeSubagents.set(activeId, { agent: agentName, task, handle, name: subagentName });
2330
2250
  updateLatestBroadcastTargets(undefined);
2331
2251
  },
2332
2252
  });
@@ -2345,7 +2265,7 @@ ${agentList}\n\n${subagentsGuidance}\n\n${delegationGuardGuidance}${resumeGuidan
2345
2265
  content: [
2346
2266
  {
2347
2267
  type: "text" as const,
2348
- text: `Agent ${result.stopReason || "failed"}: ${errorMsg}`,
2268
+ text: `${result.name ? `${result.name} (${result.agent})` : `Agent ${result.agent}`} ${result.stopReason || "failed"}: ${errorMsg}`,
2349
2269
  },
2350
2270
  ],
2351
2271
  details: makeDetails("single")([result]),
@@ -2398,7 +2318,7 @@ ${agentList}\n\n${subagentsGuidance}\n\n${delegationGuardGuidance}${resumeGuidan
2398
2318
  (index, task, handle) => {
2399
2319
  const id = topLevelBaseId + index;
2400
2320
  taskIds.set(index, id);
2401
- activeSubagents.set(id, { agent: task.agent, task: task.task, taskIndex: index, handle, name: extras?.names?.[index] });
2321
+ activeSubagents.set(id, { agent: task.agent, task: task.task, handle, name: extras?.names?.[index] });
2402
2322
  updateLatestBroadcastTargets(undefined);
2403
2323
  },
2404
2324
  (index) => {