oira666_pi-subagent 0.3.14 → 0.3.15
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 +28 -11
- 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 +107 -78
- package/tree.ts +20 -3
- package/types.ts +4 -1
package/README.md
CHANGED
|
@@ -152,6 +152,22 @@ 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
|
|
@@ -176,11 +192,12 @@ reported immediately as failures.
|
|
|
176
192
|
|
|
177
193
|
## Timestamps & Status Footer
|
|
178
194
|
|
|
179
|
-
Subagent tool calls and live activity lines render a dim `hh:mm:ss` timestamp
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
195
|
+
Subagent tool calls and live activity lines render a dim `hh:mm:ss` timestamp.
|
|
196
|
+
The collapsed view shows every direct child as `Name (agent-type)`, two prompt
|
|
197
|
+
lines, its current status, and `last action`. That timestamp is the newest
|
|
198
|
+
activity anywhere in the child's recursive subtree, so active grandchildren
|
|
199
|
+
keep their ancestor visibly alive. Press `Ctrl+O` to expand the full recursive
|
|
200
|
+
tree, including every agent's complete prompt and latest activity lines.
|
|
184
201
|
|
|
185
202
|
In the interactive TUI the extension publishes the combined `total` usage line
|
|
186
203
|
(parent + all subagents, recursively) via Pi's normal `ctx.ui.setStatus()`
|
|
@@ -188,7 +205,7 @@ status line. Pi renders all extension statuses on the same footer status line.
|
|
|
188
205
|
|
|
189
206
|
## Steering Running Subagents
|
|
190
207
|
|
|
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.
|
|
208
|
+
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
209
|
|
|
193
210
|
## Subagent Session Resume
|
|
194
211
|
|
|
@@ -215,16 +232,16 @@ Note: crash-resume covers `subagents` calls only. An interrupted `resume_subagen
|
|
|
215
232
|
|
|
216
233
|
## Resumable Subagents by Name (`resume_subagents`)
|
|
217
234
|
|
|
218
|
-
Every subagent run is assigned a
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
235
|
+
Every subagent run is assigned a random, durable human first name from a bundled
|
|
236
|
+
list of 500 popular American names — for example `John`, `Maria`, or `Elena`.
|
|
237
|
+
Names are never reused anywhere in the same delegation tree. The name is
|
|
238
|
+
returned together with the agent type and shown in every TUI view.
|
|
222
239
|
|
|
223
240
|
The `resume_subagents` tool continues named subagents with a new task while
|
|
224
241
|
preserving their full previous context:
|
|
225
242
|
|
|
226
243
|
```json
|
|
227
|
-
{ "resumes": [{ "subagent": "
|
|
244
|
+
{ "resumes": [{ "subagent": "John", "task": "Now also update the tests." }] }
|
|
228
245
|
```
|
|
229
246
|
|
|
230
247
|
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) => {
|
package/names.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Durable subagent name registry.
|
|
3
3
|
*
|
|
4
|
-
* Every spawned subagent gets a
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Every spawned subagent gets a random human first name from the bundled
|
|
5
|
+
* 500-name pool (e.g. `John`, `Maria`, `Elena`). Names are unique within one
|
|
6
|
+
* delegation tree (the tree
|
|
7
7
|
* rooted at the top-level pi session) and are persisted in a JSON registry
|
|
8
8
|
* file under the subagent session root, so they survive process restarts.
|
|
9
9
|
*
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
import * as fs from "node:fs";
|
|
22
22
|
import * as path from "node:path";
|
|
23
|
+
import { randomInt } from "node:crypto";
|
|
24
|
+
import { AMERICAN_NAMES } from "./american-names.js";
|
|
23
25
|
|
|
24
26
|
export const SUBAGENT_NAMES_FILE_ENV = "PI_SUBAGENT_NAMES_FILE";
|
|
25
27
|
|
|
@@ -168,7 +170,7 @@ export interface SubagentNameRecord {
|
|
|
168
170
|
|
|
169
171
|
export interface NamesRegistry {
|
|
170
172
|
version: 1;
|
|
171
|
-
/**
|
|
173
|
+
/** Retained in the file shape for simple migration from older registries. */
|
|
172
174
|
counters: Record<string, number>;
|
|
173
175
|
/** All allocated names in this delegation tree. */
|
|
174
176
|
agents: Record<string, SubagentNameRecord>;
|
|
@@ -306,8 +308,8 @@ export function readNamesRegistry(file: string): NamesRegistry {
|
|
|
306
308
|
/* fall through to corruption handling */
|
|
307
309
|
}
|
|
308
310
|
// Corrupt registry: keep a backup for forensics and warn instead of
|
|
309
|
-
// silently resetting (a reset
|
|
310
|
-
//
|
|
311
|
+
// silently resetting (a reset could reuse names the model remembers from
|
|
312
|
+
// the conversation transcript).
|
|
311
313
|
try {
|
|
312
314
|
const backup = `${file}.corrupt-${Date.now().toString(36)}`;
|
|
313
315
|
fs.copyFileSync(file, backup);
|
|
@@ -347,8 +349,9 @@ export async function updateNamesRegistry<T>(
|
|
|
347
349
|
// Name allocation
|
|
348
350
|
// ---------------------------------------------------------------------------
|
|
349
351
|
|
|
350
|
-
export function
|
|
351
|
-
|
|
352
|
+
export function getAvailableSubagentNames(registry: NamesRegistry): string[] {
|
|
353
|
+
const used = new Set(Object.keys(registry.agents).map((name) => name.toLowerCase()));
|
|
354
|
+
return AMERICAN_NAMES.filter((name) => !used.has(name.toLowerCase()));
|
|
352
355
|
}
|
|
353
356
|
|
|
354
357
|
export interface AllocateNameRequest {
|
|
@@ -371,13 +374,15 @@ export async function allocateSubagentNames(
|
|
|
371
374
|
if (requests.length === 0) return [];
|
|
372
375
|
return updateNamesRegistry(file, (registry) => {
|
|
373
376
|
const names: string[] = [];
|
|
377
|
+
const available = getAvailableSubagentNames(registry);
|
|
378
|
+
if (available.length < requests.length) {
|
|
379
|
+
throw new Error(
|
|
380
|
+
`The 500-name subagent pool is exhausted (${available.length} available, ${requests.length} requested).`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
374
383
|
for (const request of requests) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const next = (registry.counters[request.agent] ?? 0) + 1;
|
|
378
|
-
registry.counters[request.agent] = next;
|
|
379
|
-
name = formatSubagentName(request.agent, next);
|
|
380
|
-
} while (registry.agents[name] !== undefined);
|
|
384
|
+
const selectedIndex = randomInt(available.length);
|
|
385
|
+
const [name] = available.splice(selectedIndex, 1);
|
|
381
386
|
registry.agents[name] = {
|
|
382
387
|
name,
|
|
383
388
|
agent: request.agent,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oira666_pi-subagent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
4
4
|
"description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"runner.ts",
|
|
12
12
|
"resume.ts",
|
|
13
13
|
"names.ts",
|
|
14
|
+
"american-names.ts",
|
|
14
15
|
"shared.ts",
|
|
15
16
|
"render.ts",
|
|
16
17
|
"tree.ts",
|
package/render.ts
CHANGED
|
@@ -7,11 +7,36 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { Container, Spacer, Text } from "@mariozechner/pi-tui";
|
|
10
|
+
|
|
11
|
+
interface Component {
|
|
12
|
+
render(width: number): string[];
|
|
13
|
+
invalidate(): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fitLine(text: string, width: number): string {
|
|
17
|
+
if (width <= 0) return "";
|
|
18
|
+
let visible = 0;
|
|
19
|
+
let output = "";
|
|
20
|
+
for (let i = 0; i < text.length && visible < width;) {
|
|
21
|
+
if (text[i] === "\u001b") {
|
|
22
|
+
const match = text.slice(i).match(/^\u001b(?:\[[0-?]*[ -\/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\))/);
|
|
23
|
+
if (match) {
|
|
24
|
+
output += match[0];
|
|
25
|
+
i += match[0].length;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const codePoint = text.codePointAt(i)!;
|
|
30
|
+
const char = String.fromCodePoint(codePoint);
|
|
31
|
+
output += char;
|
|
32
|
+
i += char.length;
|
|
33
|
+
visible += 1;
|
|
34
|
+
}
|
|
35
|
+
return output;
|
|
36
|
+
}
|
|
10
37
|
import {
|
|
11
38
|
MAX_LIVE_LOG_ENTRIES,
|
|
12
|
-
isResultError,
|
|
13
39
|
isSubagentDetails,
|
|
14
|
-
type SingleResult,
|
|
15
40
|
type SubagentDetails,
|
|
16
41
|
} from "./types.js";
|
|
17
42
|
import {
|
|
@@ -21,6 +46,7 @@ import {
|
|
|
21
46
|
formatClockTime,
|
|
22
47
|
formatLiveLogEntry,
|
|
23
48
|
hasNestedChildren,
|
|
49
|
+
statusEmoji,
|
|
24
50
|
renderTreeLines,
|
|
25
51
|
setBroadcastNumberingActive,
|
|
26
52
|
topLevelSummary,
|
|
@@ -73,19 +99,8 @@ export function renderCall(
|
|
|
73
99
|
): Text {
|
|
74
100
|
const tasks = Array.isArray(args.tasks) ? args.tasks : [];
|
|
75
101
|
const count = tasks.length;
|
|
76
|
-
const icon = context?.isPartial === false
|
|
77
|
-
? context.isError
|
|
78
|
-
? theme.fg("error", "❌")
|
|
79
|
-
: theme.fg("success", "✅")
|
|
80
|
-
: theme.fg("warning", "⏳");
|
|
81
102
|
const stamp = getCallStartStamp(context, theme);
|
|
82
|
-
|
|
83
|
-
for (const task of tasks.slice(0, 6)) {
|
|
84
|
-
const agent = typeof task?.agent === "string" ? task.agent : "...";
|
|
85
|
-
const preview = typeof task?.task === "string" ? ` ${truncate(task.task, 56)}` : "";
|
|
86
|
-
text += `\n ${icon} ${theme.fg("accent", agent)}${theme.fg("dim", preview)}`;
|
|
87
|
-
}
|
|
88
|
-
if (tasks.length > 6) text += `\n ${theme.fg("muted", `... +${tasks.length - 6} more`)}`;
|
|
103
|
+
const text = `${stamp}${theme.fg("toolTitle", theme.bold("subagents "))}${theme.fg("accent", `${count} task${count === 1 ? "" : "s"}`)}`;
|
|
89
104
|
return new Text(text, 0, 0);
|
|
90
105
|
}
|
|
91
106
|
|
|
@@ -104,24 +119,8 @@ export function renderResumeCall(
|
|
|
104
119
|
? [args.resumes]
|
|
105
120
|
: [];
|
|
106
121
|
const count = resumes.length;
|
|
107
|
-
const icon = context?.isPartial === false
|
|
108
|
-
? context.isError
|
|
109
|
-
? theme.fg("error", "❌")
|
|
110
|
-
: theme.fg("success", "✅")
|
|
111
|
-
: theme.fg("warning", "⏳");
|
|
112
122
|
const stamp = getCallStartStamp(context, theme);
|
|
113
|
-
|
|
114
|
-
for (const resume of resumes.slice(0, 6)) {
|
|
115
|
-
const name = typeof resume?.subagent === "string"
|
|
116
|
-
? resume.subagent
|
|
117
|
-
: typeof resume?.name === "string"
|
|
118
|
-
? resume.name
|
|
119
|
-
: "...";
|
|
120
|
-
const task = typeof resume?.task === "string" ? resume.task : typeof resume?.prompt === "string" ? resume.prompt : undefined;
|
|
121
|
-
const preview = task !== undefined ? ` ${truncate(task, 56)}` : "";
|
|
122
|
-
text += `\n ${icon} ${theme.fg("accent", name)}${theme.fg("dim", preview)}`;
|
|
123
|
-
}
|
|
124
|
-
if (resumes.length > 6) text += `\n ${theme.fg("muted", `... +${resumes.length - 6} more`)}`;
|
|
123
|
+
const text = `${stamp}${theme.fg("toolTitle", theme.bold("resume subagents "))}${theme.fg("accent", `${count} subagent${count === 1 ? "" : "s"}`)}`;
|
|
125
124
|
return new Text(text, 0, 0);
|
|
126
125
|
}
|
|
127
126
|
|
|
@@ -143,52 +142,66 @@ function compactText(text: string, maxLength = 240): string {
|
|
|
143
142
|
return truncate(firstLine, maxLength);
|
|
144
143
|
}
|
|
145
144
|
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
145
|
+
function takePromptLine(text: string, width: number): { line: string; rest: string } {
|
|
146
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
147
|
+
if (!normalized || width <= 0) return { line: "", rest: normalized };
|
|
148
|
+
if (normalized.length <= width) return { line: normalized, rest: "" };
|
|
149
|
+
let split = normalized.lastIndexOf(" ", width);
|
|
150
|
+
if (split < Math.max(1, Math.floor(width / 2))) split = width;
|
|
151
|
+
return { line: normalized.slice(0, split), rest: normalized.slice(split).trimStart() };
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const finished = results.length - running;
|
|
160
|
-
const succeeded = results.filter((item) => item.exitCode === 0 && !isResultError(item)).length;
|
|
161
|
-
const failed = finished - succeeded;
|
|
154
|
+
class CollapsedSubagentComponent implements Component {
|
|
155
|
+
constructor(
|
|
156
|
+
private readonly details: SubagentDetails,
|
|
157
|
+
private readonly theme: { fg: ThemeFg; bold: (s: string) => string },
|
|
158
|
+
) {}
|
|
162
159
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
160
|
+
render(width: number): string[] {
|
|
161
|
+
if (width <= 0) return [];
|
|
162
|
+
const nodes = buildTopLevelNodes(this.details);
|
|
163
|
+
const lines: string[] = [];
|
|
164
|
+
for (const node of nodes) {
|
|
165
|
+
const rawPrefix = ` ${node.status === "running" ? "⏳" : node.status === "error" ? "❌" : "✅"} ${node.label} `;
|
|
166
|
+
const firstWidth = Math.max(8, width - rawPrefix.length);
|
|
167
|
+
const first = takePromptLine(node.task ?? "", firstWidth);
|
|
168
|
+
const continuationIndent = " ".repeat(Math.min(rawPrefix.length, Math.max(2, width - 8)));
|
|
169
|
+
const second = takePromptLine(first.rest, Math.max(8, width - continuationIndent.length));
|
|
170
|
+
const firstLine = ` ${statusEmoji(node.status, this.theme)} ${this.theme.fg("accent", node.label)}${first.line ? ` ${this.theme.fg("dim", first.line)}` : ""}`;
|
|
171
|
+
lines.push(fitLine(firstLine, width));
|
|
172
|
+
if (first.rest) {
|
|
173
|
+
const hasMore = second.rest.length > 0;
|
|
174
|
+
const secondText = `${second.line}${hasMore ? "..." : ""}`;
|
|
175
|
+
lines.push(fitLine(`${continuationIndent}${this.theme.fg("dim", secondText)}`, width));
|
|
176
|
+
}
|
|
177
|
+
const actionAt = node.lastActionAt ?? node.startedAt;
|
|
178
|
+
if (actionAt !== undefined) {
|
|
179
|
+
lines.push(fitLine(` ${this.theme.fg("muted", `last action: ${formatClockTime(actionAt)}`)}`, width));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const counts = countNodes(nodes);
|
|
183
|
+
if (lines.length > 0) lines.push("");
|
|
184
|
+
lines.push(fitLine(this.theme.fg("dim", topLevelSummary(this.details, counts)), width));
|
|
185
|
+
return lines;
|
|
167
186
|
}
|
|
168
187
|
|
|
169
|
-
|
|
170
|
-
if (item.exitCode === -1) return `Agent ${item.agent}: running...`;
|
|
171
|
-
if (isResultError(item)) {
|
|
172
|
-
const reason = [item.errorMessage, item.stderr, item.stopReason]
|
|
173
|
-
.find((value): value is string => typeof value === "string" && value.length > 0);
|
|
174
|
-
return `Agent ${item.agent}: failed${reason ? ` — ${compactText(reason, 180)}` : ""}`;
|
|
175
|
-
}
|
|
176
|
-
return `Agent ${item.agent}: completed`;
|
|
188
|
+
invalidate(): void {}
|
|
177
189
|
}
|
|
178
190
|
|
|
179
191
|
export function renderResult(
|
|
180
192
|
result: { content: Array<{ type: string; text?: string }>; details?: unknown },
|
|
181
193
|
expanded: boolean,
|
|
182
194
|
theme: { fg: ThemeFg; bold: (s: string) => string },
|
|
183
|
-
): Container | Text {
|
|
195
|
+
): Component | Container | Text {
|
|
184
196
|
const fallbackText = getResultText(result);
|
|
185
197
|
|
|
186
|
-
//
|
|
198
|
+
// The collapsed view remains live and shows each direct child separately.
|
|
199
|
+
// Its status icon comes from that child's result, not the outer tool state,
|
|
200
|
+
// so completed siblings update while other tasks are still running.
|
|
187
201
|
if (!expanded) {
|
|
188
|
-
|
|
189
|
-
?
|
|
190
|
-
: compactText(fallbackText);
|
|
191
|
-
return new Text(text, 0, 0);
|
|
202
|
+
return isSubagentDetails(result.details) && result.details.results.length > 0
|
|
203
|
+
? new CollapsedSubagentComponent(result.details, theme)
|
|
204
|
+
: new Text(compactText(fallbackText), 0, 0);
|
|
192
205
|
}
|
|
193
206
|
|
|
194
207
|
if (!isSubagentDetails(result.details) || result.details.results.length === 0) {
|
|
@@ -216,7 +229,7 @@ export function renderResult(
|
|
|
216
229
|
);
|
|
217
230
|
|
|
218
231
|
container.addChild(new Spacer(1));
|
|
219
|
-
container.addChild(new Text(renderTreeLines(nodes, theme, showOutputPreview).join("\n"), 0, 0));
|
|
232
|
+
container.addChild(new Text(renderTreeLines(nodes, theme, showOutputPreview, 0, "", true).join("\n"), 0, 0));
|
|
220
233
|
return container;
|
|
221
234
|
} catch {
|
|
222
235
|
// Pi falls back to raw result.content when a custom renderer throws, which
|
package/runner.ts
CHANGED
|
@@ -150,77 +150,32 @@ function cleanupTempDir(dir: string | null): void {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// When this extension is loaded by pi, process.argv[1] is the pi entrypoint.
|
|
158
|
-
// Reusing it with process.execPath avoids relying on PATH while still running
|
|
159
|
-
// the exact same pi installation as the parent process.
|
|
160
|
-
//
|
|
161
|
-
// On Linux/macOS the launched entrypoint is usually the npm `bin` symlink
|
|
162
|
-
// (e.g. <prefix>/bin/pi) that points at .../pi-coding-agent/dist/cli.js, so
|
|
163
|
-
// argv[1] does NOT end in /dist/cli.js. Resolve symlinks before matching —
|
|
164
|
-
// otherwise this check fails and we fall back to scanning PATH, which can
|
|
165
|
-
// pick a different (e.g. much slower, cross-filesystem) pi install than the
|
|
166
|
-
// one actually running.
|
|
167
|
-
const candidates = [script];
|
|
168
|
-
try {
|
|
169
|
-
const real = fs.realpathSync(script);
|
|
170
|
-
if (real && real !== script) candidates.push(real);
|
|
171
|
-
} catch {
|
|
172
|
-
// argv[1] not statable; fall through with the raw value.
|
|
173
|
-
}
|
|
174
|
-
for (const candidate of candidates) {
|
|
175
|
-
const normalized = candidate.replace(/\\/g, "/");
|
|
176
|
-
if (normalized.includes("/pi-coding-agent/") && normalized.endsWith("/dist/cli.js")) {
|
|
177
|
-
return candidate;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return null;
|
|
153
|
+
export interface RuntimeLaunchCommand {
|
|
154
|
+
command: string;
|
|
155
|
+
argsPrefix: string[];
|
|
181
156
|
}
|
|
182
157
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
} catch {
|
|
203
|
-
// not a resolvable symlink; fall through to the wrapper-text scan.
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
let text = "";
|
|
207
|
-
try {
|
|
208
|
-
text = fs.readFileSync(shimPath, "utf8");
|
|
209
|
-
} catch {
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
const match = text.match(/node_modules[\\/]([^\s"']*pi-coding-agent)[\\/]dist[\\/]cli\.js/);
|
|
213
|
-
if (!match) continue;
|
|
214
|
-
const cliPath = path.join(dir, "node_modules", match[1], "dist", "cli.js");
|
|
215
|
-
if (fs.existsSync(cliPath)) return cliPath;
|
|
216
|
-
}
|
|
158
|
+
/**
|
|
159
|
+
* Re-launch the runtime and entrypoint that are executing this Pi process.
|
|
160
|
+
*
|
|
161
|
+
* This deliberately knows nothing about npm, pnpm, Pi package names, or dist
|
|
162
|
+
* layouts. With Node it becomes `node <entrypoint>`; with Bun it becomes
|
|
163
|
+
* `bun <entrypoint>`. Package-manager shims have already done their job before
|
|
164
|
+
* the current process starts, so children do not need to execute or parse them.
|
|
165
|
+
*/
|
|
166
|
+
export function getCurrentRuntimeLaunch(
|
|
167
|
+
argv: readonly string[] = process.argv,
|
|
168
|
+
execPath = process.execPath,
|
|
169
|
+
): RuntimeLaunchCommand | null {
|
|
170
|
+
const rawEntrypoint = argv[1];
|
|
171
|
+
if (!execPath || !rawEntrypoint || rawEntrypoint.startsWith("-")) return null;
|
|
172
|
+
const entrypoint = path.resolve(rawEntrypoint);
|
|
173
|
+
try {
|
|
174
|
+
if (!fs.statSync(entrypoint).isFile()) return null;
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
217
177
|
}
|
|
218
|
-
return
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** Resolve the dist/cli.js of the currently running pi installation, if findable. */
|
|
222
|
-
export function getPiCliScriptPath(): string | null {
|
|
223
|
-
return getCurrentPiCliScript() ?? findPiCliScriptOnPath();
|
|
178
|
+
return { command: execPath, argsPrefix: [entrypoint] };
|
|
224
179
|
}
|
|
225
180
|
|
|
226
181
|
function getPiSpawnCommand(override?: { command: string; argsPrefix?: string[] }): { command: string; argsPrefix: string[] } {
|
|
@@ -243,11 +198,63 @@ function getPiSpawnCommand(override?: { command: string; argsPrefix?: string[] }
|
|
|
243
198
|
return { command: overrideCommand, argsPrefix };
|
|
244
199
|
}
|
|
245
200
|
|
|
246
|
-
const
|
|
247
|
-
if (
|
|
201
|
+
const currentRuntime = getCurrentRuntimeLaunch();
|
|
202
|
+
if (currentRuntime) return currentRuntime;
|
|
203
|
+
|
|
204
|
+
// SDK/embedded hosts may have no script entrypoint. They can set the
|
|
205
|
+
// explicit command variables above; keep `pi` as a final conventional
|
|
206
|
+
// fallback for environments where it is a real executable on PATH.
|
|
248
207
|
return { command: "pi", argsPrefix: [] };
|
|
249
208
|
}
|
|
250
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Build a child environment with a dependable executable search path.
|
|
212
|
+
*
|
|
213
|
+
* Elevated PowerShell sessions and pnpm shims can start Pi with a PATH that
|
|
214
|
+
* omits PNPM_HOME, the user npm bin directory, or even the Node directory.
|
|
215
|
+
* Child Pi processes then start (when we resolved cli.js directly) but cannot
|
|
216
|
+
* launch tools or nested Pi processes. Windows also treats Path/PATH keys
|
|
217
|
+
* case-insensitively, while Node's env object does not, so emit exactly one.
|
|
218
|
+
*/
|
|
219
|
+
export function buildChildProcessEnv(extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
|
|
220
|
+
const inherited = { ...process.env, ...extra };
|
|
221
|
+
const pathEntries: string[] = [];
|
|
222
|
+
const add = (value: string | undefined) => {
|
|
223
|
+
if (!value) return;
|
|
224
|
+
for (const entry of value.split(path.delimiter)) {
|
|
225
|
+
const clean = entry.trim();
|
|
226
|
+
if (!clean) continue;
|
|
227
|
+
const key = process.platform === "win32" ? clean.toLowerCase() : clean;
|
|
228
|
+
if (!pathEntries.some((existing) => (process.platform === "win32" ? existing.toLowerCase() : existing) === key)) {
|
|
229
|
+
pathEntries.push(clean);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
for (const [key, value] of Object.entries(inherited)) {
|
|
235
|
+
if (key.toLowerCase() === "path") add(value);
|
|
236
|
+
}
|
|
237
|
+
add(path.dirname(process.execPath));
|
|
238
|
+
add(process.argv[1] ? path.dirname(path.resolve(process.argv[1])) : undefined);
|
|
239
|
+
add(inherited.PNPM_HOME);
|
|
240
|
+
add(inherited.npm_config_prefix);
|
|
241
|
+
if (inherited.npm_config_prefix) add(path.join(inherited.npm_config_prefix, "bin"));
|
|
242
|
+
if (process.platform === "win32") {
|
|
243
|
+
add(inherited.APPDATA ? path.join(inherited.APPDATA, "npm") : undefined);
|
|
244
|
+
add(inherited.LOCALAPPDATA ? path.join(inherited.LOCALAPPDATA, "pnpm") : undefined);
|
|
245
|
+
add(inherited.USERPROFILE ? path.join(inherited.USERPROFILE, "AppData", "Local", "pnpm") : undefined);
|
|
246
|
+
const systemRoot = inherited.SystemRoot ?? inherited.SYSTEMROOT;
|
|
247
|
+
add(systemRoot ? path.join(systemRoot, "System32") : undefined);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const env: NodeJS.ProcessEnv = {};
|
|
251
|
+
for (const [key, value] of Object.entries(inherited)) {
|
|
252
|
+
if (key.toLowerCase() !== "path") env[key] = value;
|
|
253
|
+
}
|
|
254
|
+
env[process.platform === "win32" ? "Path" : "PATH"] = pathEntries.join(path.delimiter);
|
|
255
|
+
return env;
|
|
256
|
+
}
|
|
257
|
+
|
|
251
258
|
function resolveExtensionArg(value: string): string {
|
|
252
259
|
if (!value) return value;
|
|
253
260
|
if (value.startsWith("npm:") || value.startsWith("git:")) return value;
|
|
@@ -472,6 +479,14 @@ export function processJsonLine(line: string, result: SingleResult): boolean {
|
|
|
472
479
|
// Guard: JSON.parse can return null, a number, boolean, or array — none of which have .type
|
|
473
480
|
if (!event || typeof event !== "object" || Array.isArray(event)) return false;
|
|
474
481
|
|
|
482
|
+
const semanticEventTypes = new Set([
|
|
483
|
+
"message_end", "tool_result_end", "subagent_progress", "turn_start",
|
|
484
|
+
"turn_end", "tool_execution_start", "tool_execution_end",
|
|
485
|
+
]);
|
|
486
|
+
if (semanticEventTypes.has(event.type)) {
|
|
487
|
+
result.lastActionAt = typeof event.timestamp === "number" ? event.timestamp : Date.now();
|
|
488
|
+
}
|
|
489
|
+
|
|
475
490
|
if (event.type === "message_end" && event.message) {
|
|
476
491
|
const msg = event.message as Message;
|
|
477
492
|
if (hasMessage(result, msg)) return true;
|
|
@@ -638,7 +653,7 @@ export interface RunAgentOptions {
|
|
|
638
653
|
agentName: string;
|
|
639
654
|
/** Task description. */
|
|
640
655
|
task: string;
|
|
641
|
-
/** Unique resumable name assigned to this subagent (e.g. "
|
|
656
|
+
/** Unique resumable human name assigned to this subagent (e.g. "John"). */
|
|
642
657
|
subagentName?: string;
|
|
643
658
|
/** When true, send the task text to the child verbatim (no "Task:" / resume preamble). */
|
|
644
659
|
rawPrompt?: boolean;
|
|
@@ -745,6 +760,7 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
745
760
|
task,
|
|
746
761
|
name: opts.subagentName ?? initialResult?.name,
|
|
747
762
|
startedAt: initialResult?.startedAt ?? Date.now(),
|
|
763
|
+
lastActionAt: initialResult?.lastActionAt ?? Date.now(),
|
|
748
764
|
exitCode: -1,
|
|
749
765
|
messages: initialResult?.messages ? [...initialResult.messages] : [],
|
|
750
766
|
stderr: initialResult?.stderr ?? "",
|
|
@@ -835,8 +851,7 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
835
851
|
// agents and tools, rather than only their immediate Pi parent.
|
|
836
852
|
detached: process.platform !== "win32",
|
|
837
853
|
stdio: ["pipe", "pipe", "pipe"],
|
|
838
|
-
env: {
|
|
839
|
-
...process.env,
|
|
854
|
+
env: buildChildProcessEnv({
|
|
840
855
|
[SUBAGENT_DEPTH_ENV]: String(nextDepth),
|
|
841
856
|
[SUBAGENT_MAX_DEPTH_ENV]: String(propagatedMaxDepth),
|
|
842
857
|
[SUBAGENT_STACK_ENV]: JSON.stringify(propagatedStack),
|
|
@@ -844,8 +859,9 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
844
859
|
...(sessionRoot ? { [SUBAGENT_SESSION_ROOT_ENV]: sessionRoot } : {}),
|
|
845
860
|
...(opts.namesFile ? { [SUBAGENT_NAMES_FILE_ENV]: opts.namesFile } : {}),
|
|
846
861
|
...(fallbackModel ? { [SUBAGENT_FALLBACK_MODEL_ENV]: fallbackModel } : {}),
|
|
862
|
+
// All other provider/auth/proxy/temp/home variables are inherited.
|
|
847
863
|
// PI_OFFLINE is NOT forced here — see explanation near PI_OFFLINE_ENV.
|
|
848
|
-
},
|
|
864
|
+
}),
|
|
849
865
|
});
|
|
850
866
|
|
|
851
867
|
let buffer = "";
|
|
@@ -922,7 +938,10 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
922
938
|
const systemRoot = process.env.SystemRoot ?? process.env.SYSTEMROOT ?? "C:\\Windows";
|
|
923
939
|
const killer = spawn(
|
|
924
940
|
path.join(systemRoot, "System32", "taskkill.exe"),
|
|
925
|
-
|
|
941
|
+
// taskkill has no reliable graceful console-process mode when
|
|
942
|
+
// launched from elevated PowerShell; without /F it often leaves
|
|
943
|
+
// the Node child alive until our five-second escalation timer.
|
|
944
|
+
["/PID", String(proc.pid), "/T", "/F"],
|
|
926
945
|
{ shell: false, stdio: "ignore", windowsHide: true },
|
|
927
946
|
);
|
|
928
947
|
killer.unref();
|
|
@@ -993,7 +1012,11 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
993
1012
|
result.stopReason = "error";
|
|
994
1013
|
result.errorMessage = message;
|
|
995
1014
|
appendBoundedStderr(result, `[pi-subagent] ${message}\n`);
|
|
996
|
-
|
|
1015
|
+
// No agent turn ever started, so there is no buffered semantic
|
|
1016
|
+
// output to drain. Kill the rejected RPC process tree and settle
|
|
1017
|
+
// immediately instead of waiting on slow Windows taskkill/stdio.
|
|
1018
|
+
stopChild(true);
|
|
1019
|
+
doResolve(1);
|
|
997
1020
|
}
|
|
998
1021
|
return;
|
|
999
1022
|
}
|
|
@@ -1002,6 +1025,7 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
1002
1025
|
// compact-and-retry, or process a queued continuation. Killing here was
|
|
1003
1026
|
// the direct cause of the WebSocket failures in the inspected session.
|
|
1004
1027
|
if (event?.type === "agent_end") {
|
|
1028
|
+
result.lastActionAt = Date.now();
|
|
1005
1029
|
noteSemanticActivity(
|
|
1006
1030
|
event.willRetry === true ? RETRY_WAIT_GRACE_MS : 0,
|
|
1007
1031
|
);
|
|
@@ -1010,6 +1034,7 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
1010
1034
|
|
|
1011
1035
|
if (event?.type === "agent_settled") {
|
|
1012
1036
|
agentSettled = true;
|
|
1037
|
+
result.lastActionAt = Date.now();
|
|
1013
1038
|
// No semantic watchdog may fire while we are only waiting for the
|
|
1014
1039
|
// deliberately terminated RPC process tree to close. On slower
|
|
1015
1040
|
// launchers (notably Windows taskkill), that cleanup can outlast a
|
|
@@ -1052,8 +1077,10 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
1052
1077
|
if (event?.type === "message_update" || event?.type === "tool_execution_update") {
|
|
1053
1078
|
// Streaming deltas are intentionally not retained in result.messages,
|
|
1054
1079
|
// but they prove the model/tool is still making real progress.
|
|
1080
|
+
result.lastActionAt = Date.now();
|
|
1055
1081
|
noteSemanticActivity();
|
|
1056
1082
|
} else if (event?.type === "auto_retry_start") {
|
|
1083
|
+
result.lastActionAt = Date.now();
|
|
1057
1084
|
const delayMs = Number.isFinite(event.delayMs) ? Math.max(0, Number(event.delayMs)) : 0;
|
|
1058
1085
|
noteSemanticActivity(delayMs + RETRY_WAIT_GRACE_MS);
|
|
1059
1086
|
} else if (
|
|
@@ -1064,6 +1091,7 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
1064
1091
|
event?.type === "summarization_retry_attempt_start" ||
|
|
1065
1092
|
event?.type === "summarization_retry_finished"
|
|
1066
1093
|
) {
|
|
1094
|
+
result.lastActionAt = Date.now();
|
|
1067
1095
|
noteSemanticActivity();
|
|
1068
1096
|
}
|
|
1069
1097
|
}
|
|
@@ -1340,6 +1368,7 @@ export async function executeParallelSubprocess(
|
|
|
1340
1368
|
task: t.task,
|
|
1341
1369
|
name: extras?.names?.[index],
|
|
1342
1370
|
startedAt: Date.now(),
|
|
1371
|
+
lastActionAt: Date.now(),
|
|
1343
1372
|
exitCode: -1,
|
|
1344
1373
|
messages: [],
|
|
1345
1374
|
stderr: "",
|
|
@@ -1434,9 +1463,9 @@ export async function executeParallelSubprocess(
|
|
|
1434
1463
|
const output = succeeded
|
|
1435
1464
|
? getFinalOutput(r.messages, r.finalOutput)
|
|
1436
1465
|
: r.errorMessage || r.stderr || getFinalOutput(r.messages, r.finalOutput);
|
|
1437
|
-
const
|
|
1466
|
+
const identity = r.name ? `${r.name} (${r.agent})` : r.agent;
|
|
1438
1467
|
const status = succeeded ? "completed" : r.exitCode === -1 ? "unfinished" : "failed";
|
|
1439
|
-
return `[${
|
|
1468
|
+
return `[${identity}] ${status}: ${output || "(no output)"}`;
|
|
1440
1469
|
});
|
|
1441
1470
|
|
|
1442
1471
|
return {
|
package/tree.ts
CHANGED
|
@@ -44,6 +44,8 @@ export interface TreeNode {
|
|
|
44
44
|
task?: string;
|
|
45
45
|
/** Epoch ms when this subagent run started (rendered as a dim hh:mm:ss prefix). */
|
|
46
46
|
startedAt?: number;
|
|
47
|
+
/** Latest activity across this node and every descendant. */
|
|
48
|
+
lastActionAt?: number;
|
|
47
49
|
outputPreview?: string[];
|
|
48
50
|
liveActivity?: LiveLogEntry[];
|
|
49
51
|
children: TreeNode[];
|
|
@@ -571,7 +573,6 @@ function buildResultNode(rawResult: SingleResult): TreeNode {
|
|
|
571
573
|
const status = statusFromResult(result);
|
|
572
574
|
const usage = formatUsage(result.usage, result.model);
|
|
573
575
|
const metaParts: string[] = [];
|
|
574
|
-
if (typeof result.name === "string" && result.name) metaParts.push(result.name);
|
|
575
576
|
if (typeof result.agentSource === "string" && result.agentSource) metaParts.push(result.agentSource);
|
|
576
577
|
if (usage) metaParts.push(usage);
|
|
577
578
|
if (status === "error") {
|
|
@@ -584,14 +585,21 @@ function buildResultNode(rawResult: SingleResult): TreeNode {
|
|
|
584
585
|
const children = buildNestedChildren(result);
|
|
585
586
|
const isRunning = status === "running";
|
|
586
587
|
const liveLog = Array.isArray(result.liveLog) ? result.liveLog : [];
|
|
588
|
+
const ownLastAction = typeof result.lastActionAt === "number" && Number.isFinite(result.lastActionAt)
|
|
589
|
+
? result.lastActionAt
|
|
590
|
+
: liveLog.reduce((latest, entry) => Math.max(latest, typeof entry.at === "number" ? entry.at : 0), result.startedAt ?? 0);
|
|
591
|
+
const descendantLastAction = children.reduce((latest, child) => Math.max(latest, child.lastActionAt ?? 0), 0);
|
|
592
|
+
const agentType = stringValue(result.agent, "unknown agent");
|
|
593
|
+
const humanName = typeof result.name === "string" && result.name ? result.name : undefined;
|
|
587
594
|
return {
|
|
588
|
-
label:
|
|
595
|
+
label: humanName ? `${humanName} (${agentType})` : agentType,
|
|
589
596
|
status,
|
|
590
597
|
meta: metaParts.join(" • "),
|
|
591
598
|
task: typeof result.task === "string" ? result.task : undefined,
|
|
592
599
|
startedAt: typeof result.startedAt === "number" && Number.isFinite(result.startedAt)
|
|
593
600
|
? result.startedAt
|
|
594
601
|
: undefined,
|
|
602
|
+
lastActionAt: Math.max(ownLastAction, descendantLastAction) || undefined,
|
|
595
603
|
liveActivity: isRunning && liveLog.length > 0 ? liveLog.slice(-MAX_LIVE_LOG_ENTRIES) : undefined,
|
|
596
604
|
outputPreview: !isRunning && children.length === 0 ? buildLeafPreview(result) : undefined,
|
|
597
605
|
children,
|
|
@@ -608,6 +616,7 @@ export function renderTreeLines(
|
|
|
608
616
|
showOutputPreview: boolean,
|
|
609
617
|
depth = 0,
|
|
610
618
|
prefix = "",
|
|
619
|
+
showFullPrompts = false,
|
|
611
620
|
): string[] {
|
|
612
621
|
const lines: string[] = [];
|
|
613
622
|
|
|
@@ -622,6 +631,14 @@ export function renderTreeLines(
|
|
|
622
631
|
if (node.meta) line += ` ${theme.fg("dim", node.meta)}`;
|
|
623
632
|
lines.push(line);
|
|
624
633
|
|
|
634
|
+
if (showFullPrompts && node.task) {
|
|
635
|
+
const promptLines = node.task.replace(/\r\n?/g, "\n").split("\n");
|
|
636
|
+
promptLines.forEach((promptLine, promptIndex) => {
|
|
637
|
+
const promptLabel = promptIndex === 0 ? "prompt: " : " ";
|
|
638
|
+
lines.push(`${indent} ${theme.fg("muted", promptLabel)}${theme.fg("toolOutput", promptLine)}`);
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
625
642
|
if (showOutputPreview && node.outputPreview && node.outputPreview.length > 0) {
|
|
626
643
|
for (const outputLine of node.outputPreview) {
|
|
627
644
|
lines.push(`${indent} ${theme.fg("toolOutput", outputLine)}`);
|
|
@@ -641,7 +658,7 @@ export function renderTreeLines(
|
|
|
641
658
|
}
|
|
642
659
|
|
|
643
660
|
if (node.children.length > 0) {
|
|
644
|
-
lines.push(...renderTreeLines(node.children, theme, showOutputPreview, depth + 1, number));
|
|
661
|
+
lines.push(...renderTreeLines(node.children, theme, showOutputPreview, depth + 1, number, showFullPrompts));
|
|
645
662
|
}
|
|
646
663
|
});
|
|
647
664
|
|
package/types.ts
CHANGED
|
@@ -64,10 +64,12 @@ export interface SingleResult {
|
|
|
64
64
|
agent: string;
|
|
65
65
|
agentSource: "user" | "project" | "builtin" | "unknown";
|
|
66
66
|
task: string;
|
|
67
|
-
/** Unique resumable name
|
|
67
|
+
/** Unique resumable human name within the delegation tree (e.g. "John"). */
|
|
68
68
|
name?: string;
|
|
69
69
|
/** Epoch ms when this subagent run started (used for TUI timestamps). */
|
|
70
70
|
startedAt?: number;
|
|
71
|
+
/** Most recent semantic activity in this agent's entire live run. */
|
|
72
|
+
lastActionAt?: number;
|
|
71
73
|
exitCode: number;
|
|
72
74
|
messages: Message[];
|
|
73
75
|
stderr: string;
|
|
@@ -264,6 +266,7 @@ export function compactSingleResultForDurableDetails(result: SingleResult): Sing
|
|
|
264
266
|
subtreeUsageSummary: result.subtreeUsageSummary ?? buildUsageSummary([result]),
|
|
265
267
|
...(result.name !== undefined ? { name: result.name } : {}),
|
|
266
268
|
...(result.startedAt !== undefined ? { startedAt: result.startedAt } : {}),
|
|
269
|
+
...(result.lastActionAt !== undefined ? { lastActionAt: result.lastActionAt } : {}),
|
|
267
270
|
...(result.stopReason !== undefined ? { stopReason: result.stopReason } : {}),
|
|
268
271
|
...(result.errorMessage !== undefined ? { errorMessage: result.errorMessage } : {}),
|
|
269
272
|
...(result.model !== undefined ? { model: result.model } : {}),
|