@henryqw/pi-subagent 15.0.0 → 15.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -64,7 +64,9 @@ An explicit `model` (`provider/modelId`) replaces only the route model and must
|
|
|
64
64
|
|
|
65
65
|
Parallel tasks start together, settle together, and report in input order. Chains are sequential and fail at the first failure. `{previous}` passes only the immediately preceding successful assistant output.
|
|
66
66
|
|
|
67
|
-
Foreground failures throw after keeping bounded sibling and recovery evidence. One call has one aggregate 50 KiB cap for Main-visible text.
|
|
67
|
+
Foreground failures throw after keeping bounded sibling and recovery evidence. One call has one aggregate 50 KiB cap for Main-visible text. Final results show summaries first and full evidence below.
|
|
68
|
+
|
|
69
|
+
The status widget shows each task group name above at most three indented child rows. Each row shows a one-letter Role badge, status, activity, usage, and duration. Flow stages for one unit share that unit's heading.
|
|
68
70
|
|
|
69
71
|
Background work belongs to its launching session. Shutdown or reload aborts it and may leave only recoverable-work evidence or no follow-up message.
|
|
70
72
|
|
|
@@ -65,6 +65,7 @@ type ChildSettlement =
|
|
|
65
65
|
type UnitState = {
|
|
66
66
|
request: FlowUnitRequest;
|
|
67
67
|
modelClass: FlowModelClass;
|
|
68
|
+
widgetTaskId: string;
|
|
68
69
|
worktree: WorktreeInfo;
|
|
69
70
|
base: string;
|
|
70
71
|
implementation?: ChildSettlement;
|
|
@@ -125,12 +126,14 @@ export interface DelegateFlowRuntime {
|
|
|
125
126
|
resolveLaunch: (role: Role, modelClass: FlowModelClass, ctx: ExtensionContext) => ResolvedRoleLaunch;
|
|
126
127
|
startWidget: (
|
|
127
128
|
id: string,
|
|
129
|
+
taskId: string,
|
|
128
130
|
role: string,
|
|
129
131
|
model: string,
|
|
130
132
|
thinkingLevel: string | undefined,
|
|
131
133
|
name: string,
|
|
132
134
|
ctx: ExtensionContext,
|
|
133
135
|
) => void;
|
|
136
|
+
setWidgetTaskRetained: (taskId: string, retained: boolean) => void;
|
|
134
137
|
updateWidgetTokens: (id: string, tokens: number) => void;
|
|
135
138
|
updateWidgetActivity: (id: string, event: EphemeralSubagentActivityEvent) => void;
|
|
136
139
|
finishWidget: (id: string, status: WidgetStatus) => void;
|
|
@@ -262,6 +265,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
262
265
|
signal ? AbortSignal.any([signal, flow.sessionController.signal]) : flow.sessionController.signal;
|
|
263
266
|
|
|
264
267
|
const invalidateActive = (): void => {
|
|
268
|
+
if (active?.blocked) runtime.setWidgetTaskRetained(active.blocked.unit.widgetTaskId, false);
|
|
265
269
|
active?.sessionController.abort(new Error("Flow session ended."));
|
|
266
270
|
active = undefined;
|
|
267
271
|
};
|
|
@@ -346,6 +350,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
346
350
|
modelClass: FlowModelClass,
|
|
347
351
|
task: string,
|
|
348
352
|
widgetName: string,
|
|
353
|
+
widgetTaskId: string,
|
|
349
354
|
cwd: string,
|
|
350
355
|
widgetId: string,
|
|
351
356
|
signal: AbortSignal | undefined,
|
|
@@ -365,7 +370,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
365
370
|
if (launch.missingSkills.length) {
|
|
366
371
|
ctx.ui.notify(`Subagent role ${role.name} skipped unavailable Pi skills: ${launch.missingSkills.join(", ")}.`, "warning");
|
|
367
372
|
}
|
|
368
|
-
runtime.startWidget(widgetId, role.name, launch.model.id, launch.thinkingLevel, widgetName, ctx);
|
|
373
|
+
runtime.startWidget(widgetId, widgetTaskId, role.name, launch.model.id, launch.thinkingLevel, widgetName, ctx);
|
|
369
374
|
started = true;
|
|
370
375
|
return { launch, task, cwd };
|
|
371
376
|
},
|
|
@@ -506,6 +511,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
506
511
|
diagnostic: string,
|
|
507
512
|
meter: UsageMeter,
|
|
508
513
|
) => {
|
|
514
|
+
if (flow.blocked) runtime.setWidgetTaskRetained(flow.blocked.unit.widgetTaskId, false);
|
|
509
515
|
if (active === flow) active = undefined;
|
|
510
516
|
return response(flow, "failed", meter, { classification, diagnostic: capOutput(diagnostic) });
|
|
511
517
|
};
|
|
@@ -521,6 +527,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
521
527
|
if (unit.repairUsed) return terminal(flow, classification, bounded, meter);
|
|
522
528
|
flow.phase = "blocked";
|
|
523
529
|
flow.blocked = { unit, classification, diagnostic: bounded };
|
|
530
|
+
runtime.setWidgetTaskRetained(unit.widgetTaskId, true);
|
|
524
531
|
return response(flow, "blocked", meter);
|
|
525
532
|
};
|
|
526
533
|
|
|
@@ -687,6 +694,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
687
694
|
unit.modelClass,
|
|
688
695
|
reviewerTask(unit.request, reviewCriterion, { base: evidence.base, tip: evidence.tip, patchPath: evidence.patchPath }),
|
|
689
696
|
unit.request.name,
|
|
697
|
+
unit.widgetTaskId,
|
|
690
698
|
unit.worktree.cwd,
|
|
691
699
|
`${toolCallId}:flow:${flow.index}:review`,
|
|
692
700
|
signal,
|
|
@@ -820,6 +828,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
820
828
|
flow.units.push({
|
|
821
829
|
request: unit,
|
|
822
830
|
modelClass: unit.modelClass,
|
|
831
|
+
widgetTaskId: `${toolCallId}:flow:${index}`,
|
|
823
832
|
worktree,
|
|
824
833
|
base: worktree.baseCommit,
|
|
825
834
|
repairUsed: false,
|
|
@@ -839,6 +848,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
839
848
|
unit.modelClass,
|
|
840
849
|
implementerTask(unit.request),
|
|
841
850
|
unit.request.name,
|
|
851
|
+
unit.widgetTaskId,
|
|
842
852
|
unit.worktree.cwd,
|
|
843
853
|
`${toolCallId}:flow:${index}:implement`,
|
|
844
854
|
operationSignal,
|
|
@@ -884,6 +894,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
884
894
|
const blocked = flow.blocked;
|
|
885
895
|
const unit = blocked.unit;
|
|
886
896
|
if (unit.repairUsed) throw new Error("delegate_flow_continue repair was already used for this Unit.");
|
|
897
|
+
runtime.setWidgetTaskRetained(unit.widgetTaskId, false);
|
|
887
898
|
flow.phase = "running";
|
|
888
899
|
flow.blocked = undefined;
|
|
889
900
|
unit.repairUsed = true;
|
|
@@ -901,6 +912,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
901
912
|
unit.modelClass,
|
|
902
913
|
repairTask(unit.request, blocked, guidance),
|
|
903
914
|
unit.request.name,
|
|
915
|
+
unit.widgetTaskId,
|
|
904
916
|
unit.worktree.cwd,
|
|
905
917
|
`${toolCallId}:flow:${flow.index}:repair`,
|
|
906
918
|
operationSignal,
|
package/extensions/subagent.ts
CHANGED
|
@@ -60,6 +60,7 @@ const WIDGET_KEY = "subagent-status";
|
|
|
60
60
|
const WIDGET_INTERVAL_MS = 80;
|
|
61
61
|
const MAX_WIDGET_ITEMS = 8;
|
|
62
62
|
const MAX_WIDGET_LINES = 6;
|
|
63
|
+
const MAX_WIDGET_GROUP_ROWS = 3;
|
|
63
64
|
export const MAX_WIDGET_ACTIVE_TOOLS = 8;
|
|
64
65
|
const DEFAULT_TIMEOUT_POLICY = {
|
|
65
66
|
idleMs: DEFAULT_TIMEOUT_CONFIG.idleMinutes * 60_000,
|
|
@@ -87,6 +88,7 @@ type WidgetItem = {
|
|
|
87
88
|
role: string;
|
|
88
89
|
model: string;
|
|
89
90
|
thinkingLevel: string;
|
|
91
|
+
taskId: string;
|
|
90
92
|
name: string;
|
|
91
93
|
tokens: number;
|
|
92
94
|
startedAt: number;
|
|
@@ -164,13 +166,35 @@ function renderWidgetRows(
|
|
|
164
166
|
theme: Theme,
|
|
165
167
|
): string[] {
|
|
166
168
|
const ordered = [...items.filter(({ status }) => status === "working"), ...items.filter(({ status }) => status !== "working")];
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
169
|
+
if (!ordered.length) return [];
|
|
170
|
+
const groups = new Map<string, { name: string; items: WidgetItem[] }>();
|
|
171
|
+
for (const item of ordered) {
|
|
172
|
+
const group = groups.get(item.taskId);
|
|
173
|
+
if (group) group.items.push(item);
|
|
174
|
+
else groups.set(item.taskId, { name: item.name, items: [item] });
|
|
175
|
+
}
|
|
176
|
+
const maxVisibleLines = ordered.length + groups.size > MAX_WIDGET_LINES ? MAX_WIDGET_LINES - 1 : MAX_WIDGET_LINES;
|
|
177
|
+
const workingGroups = [...groups.values()].filter(({ items }) => items.some(({ status }) => status === "working"));
|
|
178
|
+
const visibleWorkingGroups = new Set(workingGroups.slice(0, Math.floor(maxVisibleLines / 2)));
|
|
179
|
+
let remainingWorkingGroups = visibleWorkingGroups.size;
|
|
180
|
+
const visible = new Set<WidgetItem>();
|
|
181
|
+
const lines: string[] = [];
|
|
182
|
+
for (const group of groups.values()) {
|
|
183
|
+
const working = group.items.some(({ status }) => status === "working");
|
|
184
|
+
if (working && !visibleWorkingGroups.has(group)) continue;
|
|
185
|
+
const reservedLines = working ? --remainingWorkingGroups * 2 : 0;
|
|
186
|
+
const childCount = Math.min(MAX_WIDGET_GROUP_ROWS, group.items.length, maxVisibleLines - lines.length - reservedLines - 1);
|
|
187
|
+
if (childCount < 1) continue;
|
|
188
|
+
lines.push(truncateToWidth(theme.fg("text", group.name), width));
|
|
189
|
+
for (const item of group.items.slice(0, childCount)) {
|
|
190
|
+
visible.add(item);
|
|
191
|
+
lines.push(truncateToWidth(
|
|
192
|
+
` ${statusGlyph(item.status, spinnerIndex, theme)} ${theme.fg("accent", item.role)} ${theme.fg("text", activityLabel(item, now))} · ${theme.fg("muted", activityMetrics(item, now))}`,
|
|
193
|
+
width,
|
|
194
|
+
));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const hidden = ordered.filter((item) => !visible.has(item));
|
|
174
198
|
if (hidden.length) {
|
|
175
199
|
const counts: Record<WidgetStatus, number> = { working: 0, success: 0, failure: 0, aborted: 0 };
|
|
176
200
|
for (const { status } of hidden) counts[status] += 1;
|
|
@@ -250,6 +274,7 @@ export default function subagentExtension(
|
|
|
250
274
|
].join("\n"), outputPad, 0);
|
|
251
275
|
});
|
|
252
276
|
const widgetItems = new Map<string, WidgetItem>();
|
|
277
|
+
const retainedWidgetTaskIds = new Set<string>();
|
|
253
278
|
// Each child is a full Pi process issuing its own model calls; cap parallel
|
|
254
279
|
// spend. Precedence: PI_SUBAGENT_MAX_SUBAGENTS env > config/pi-subagent/config.json
|
|
255
280
|
// maxSubagents > default 5. Invalid present config falls back to the default
|
|
@@ -301,6 +326,11 @@ export default function subagentExtension(
|
|
|
301
326
|
|
|
302
327
|
const requestWidgetRender = () => activeTui?.requestRender();
|
|
303
328
|
|
|
329
|
+
const setWidgetTaskRetained = (taskId: string, retained: boolean) => {
|
|
330
|
+
if (retained) retainedWidgetTaskIds.add(taskId);
|
|
331
|
+
else retainedWidgetTaskIds.delete(taskId);
|
|
332
|
+
};
|
|
333
|
+
|
|
304
334
|
const startWidgetTimer = () => {
|
|
305
335
|
if (widgetTimer) return;
|
|
306
336
|
widgetTimer = setInterval(() => {
|
|
@@ -324,6 +354,7 @@ export default function subagentExtension(
|
|
|
324
354
|
|
|
325
355
|
const startWidgetItem = (
|
|
326
356
|
id: string,
|
|
357
|
+
taskId: string,
|
|
327
358
|
role: string,
|
|
328
359
|
model: string,
|
|
329
360
|
thinkingLevel: string | undefined,
|
|
@@ -334,7 +365,7 @@ export default function subagentExtension(
|
|
|
334
365
|
ensureWidget(ctx);
|
|
335
366
|
if (!widgetItems.has(id) && widgetItems.size >= MAX_WIDGET_ITEMS) {
|
|
336
367
|
for (const [oldestId, item] of widgetItems) {
|
|
337
|
-
if (item.status === "working") continue;
|
|
368
|
+
if (item.status === "working" || retainedWidgetTaskIds.has(item.taskId) || item.taskId === taskId) continue;
|
|
338
369
|
widgetItems.delete(oldestId);
|
|
339
370
|
if (widgetItems.size < MAX_WIDGET_ITEMS) break;
|
|
340
371
|
}
|
|
@@ -343,6 +374,7 @@ export default function subagentExtension(
|
|
|
343
374
|
role: roleBadge(role),
|
|
344
375
|
model,
|
|
345
376
|
thinkingLevel: thinkingLevel ?? "default",
|
|
377
|
+
taskId,
|
|
346
378
|
name,
|
|
347
379
|
tokens: 0,
|
|
348
380
|
startedAt: Date.now(),
|
|
@@ -434,6 +466,7 @@ export default function subagentExtension(
|
|
|
434
466
|
failedToolPatches.clear();
|
|
435
467
|
stopWidgetTimer();
|
|
436
468
|
widgetItems.clear();
|
|
469
|
+
retainedWidgetTaskIds.clear();
|
|
437
470
|
activeTui = undefined;
|
|
438
471
|
widgetInstalled = false;
|
|
439
472
|
if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
@@ -451,7 +484,7 @@ export default function subagentExtension(
|
|
|
451
484
|
pi.on("input", (event) => {
|
|
452
485
|
if (event.source === "extension") return;
|
|
453
486
|
for (const [id, item] of widgetItems) {
|
|
454
|
-
if (item.status !== "working") widgetItems.delete(id);
|
|
487
|
+
if (item.status !== "working" && !retainedWidgetTaskIds.has(item.taskId)) widgetItems.delete(id);
|
|
455
488
|
}
|
|
456
489
|
requestWidgetRender();
|
|
457
490
|
});
|
|
@@ -550,6 +583,7 @@ export default function subagentExtension(
|
|
|
550
583
|
...(modelClass === undefined ? {} : { modelClass }),
|
|
551
584
|
}),
|
|
552
585
|
startWidget: startWidgetItem,
|
|
586
|
+
setWidgetTaskRetained,
|
|
553
587
|
updateWidgetTokens,
|
|
554
588
|
updateWidgetActivity,
|
|
555
589
|
finishWidget: finishWidgetItem,
|
|
@@ -695,7 +729,7 @@ export default function subagentExtension(
|
|
|
695
729
|
if (role.isolation === "worktree") {
|
|
696
730
|
worktree = await createChildWorktree(ctx.cwd, entry.id, undefined, workflowSignal);
|
|
697
731
|
}
|
|
698
|
-
startWidgetItem(entry.id, role.name, launch.model.id, launch.thinkingLevel, entry.delegation.name, ctx);
|
|
732
|
+
startWidgetItem(entry.id, entry.id, role.name, launch.model.id, launch.thinkingLevel, entry.delegation.name, ctx);
|
|
699
733
|
setState("running", "");
|
|
700
734
|
emitUpdate(emitToolUpdates);
|
|
701
735
|
return {
|
package/package.json
CHANGED
|
@@ -13,7 +13,9 @@ Before slicing, identify applicable repository prohibitions. If the request or p
|
|
|
13
13
|
|
|
14
14
|
Use the fewest cohesive units. `delegate_flow` is for independent units expected to commute: split independent outcomes into units, combine or sequence work that overlaps files, APIs, schemas, generated output, package metadata, lockfiles, or invariants, and never divide one invariant across multiple units. Dependent work remains outside Flow; sequence it in one task or ordinary caller-controlled sequencing.
|
|
15
15
|
|
|
16
|
-
Give every unit a bounded objective, owned scope and exclusions, and its direct validation command/argument array
|
|
16
|
+
Give every unit a bounded objective, owned scope and exclusions, and its direct validation command/argument array. Each task packet must name the neighboring behavior that must stay unchanged. Include the exact test name or error when known CI evidence exists. Never claim a validation command matches unknown CI.
|
|
17
|
+
|
|
18
|
+
Each delegation must own one concrete outcome with one focused validation story. Order declared validation from the cheapest focused check to broader required checks. If the affected flow or scope is not yet known, perform bounded read-only discovery first. Do not pass the parent request unchanged. Choose `modelClass` according to the delegation tool's guidance. Add non-empty `review` only for an explicit judgment that automated validation cannot establish. Call `delegate_flow` with 1–8 units; the runtime always supplies the effective Implementer and supplies the Reviewer only when a unit needs review.
|
|
17
19
|
|
|
18
20
|
## Runtime Flow
|
|
19
21
|
|
|
@@ -27,7 +29,9 @@ A successful Flow owns integration and cleanup. A blocked outcome is repairable
|
|
|
27
29
|
delegate_flow_continue({ guidance: "Address the reported block and complete the bounded unit.", modelClass: "balanced" })
|
|
28
30
|
```
|
|
29
31
|
|
|
30
|
-
Make the guidance specific to the reported implementation, validation, or review failure. Omit `modelClass` to retain an explicit blocked-unit class or otherwise use each frozen Role's default; supply it only to replace both defaults for that one repair. Do not call continuation unless Flow reports a repairable block. If continuation or Flow returns a terminal failure, inspect every retained path reported by the runtime, then reslice or manually recover from Main; do not retry the Flow or guess a rebase resolution.
|
|
32
|
+
Make the guidance specific to the reported implementation, validation, or review failure. Omit `modelClass` to retain an explicit blocked-unit class or otherwise use each frozen Role's default; supply it only to replace both defaults for that one repair. Do not call continuation unless Flow reports a repairable block. If continuation or Flow returns a terminal failure, inspect every retained path reported by the runtime, then reslice or manually recover from Main; do not retry the Flow or guess a rebase resolution.
|
|
33
|
+
|
|
34
|
+
A cleanup warning does not undo successful integration. Report a cleanup warning from a successful Flow as-is. Do not investigate it unless the user asks or cleanup is part of acceptance.
|
|
31
35
|
|
|
32
36
|
## Ordinary delegation
|
|
33
37
|
|