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 +37 -18
- package/american-names.ts +56 -0
- package/index.ts +80 -160
- package/names.ts +19 -14
- package/package.json +2 -1
- package/render.ts +77 -64
- package/runner.ts +128 -82
- package/tree.ts +20 -3
- package/types.ts +4 -1
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
|
|
159
|
-
after startup.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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": "
|
|
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
|
|
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;
|
|
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:
|
|
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
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
-
|
|
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
|
|
1122
|
-
if (!
|
|
1123
|
-
|
|
1124
|
-
if (
|
|
1125
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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((
|
|
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
|
-
|
|
1139
|
+
namePath: string[],
|
|
1140
|
+
topLevelId: number,
|
|
1207
1141
|
targets: { all: BroadcastTarget[]; youngest: BroadcastTarget[] },
|
|
1208
1142
|
): boolean {
|
|
1209
|
-
|
|
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.
|
|
1214
|
-
|
|
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
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
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 (
|
|
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
|
|
1248
|
-
const
|
|
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(
|
|
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
|
|
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.
|
|
1267
|
-
.sort((a, b) => a - b)
|
|
1268
|
-
.map((id) => ({ display:
|
|
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.
|
|
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("
|
|
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", "
|
|
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 !== "
|
|
1266
|
+
if (choice !== "Names (e.g. John, Maria)") return "continue";
|
|
1339
1267
|
|
|
1340
1268
|
const answer = await ctx.ui.input(
|
|
1341
|
-
"Subagent
|
|
1269
|
+
"Subagent names to broadcast to (comma-separated)",
|
|
1342
1270
|
"",
|
|
1343
1271
|
);
|
|
1344
1272
|
if (!answer) return "continue";
|
|
1345
|
-
const current =
|
|
1346
|
-
|
|
1347
|
-
|
|
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
|
|
1351
|
-
const
|
|
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
|
-
|
|
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. \`
|
|
1794
|
-
\`
|
|
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": "
|
|
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
|
|
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
|
-
|
|
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.
|
|
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: "
|
|
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
|
|
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,
|
|
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,
|
|
2321
|
+
activeSubagents.set(id, { agent: task.agent, task: task.task, handle, name: extras?.names?.[index] });
|
|
2402
2322
|
updateLatestBroadcastTargets(undefined);
|
|
2403
2323
|
},
|
|
2404
2324
|
(index) => {
|