@henryqw/pi-subagent 15.0.0 → 15.0.1
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 +3 -1
- package/extensions/delegate-flow.ts +13 -1
- package/extensions/subagent.ts +51 -10
- package/package.json +1 -1
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,15 +365,23 @@ 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
|
}
|
|
372
|
+
if (widgetItems.size >= MAX_WIDGET_ITEMS) {
|
|
373
|
+
for (const [oldestId, item] of widgetItems) {
|
|
374
|
+
if (item.status === "working" || retainedWidgetTaskIds.has(item.taskId)) continue;
|
|
375
|
+
widgetItems.delete(oldestId);
|
|
376
|
+
if (widgetItems.size < MAX_WIDGET_ITEMS) break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
341
379
|
}
|
|
342
380
|
widgetItems.set(id, {
|
|
343
381
|
role: roleBadge(role),
|
|
344
382
|
model,
|
|
345
383
|
thinkingLevel: thinkingLevel ?? "default",
|
|
384
|
+
taskId,
|
|
346
385
|
name,
|
|
347
386
|
tokens: 0,
|
|
348
387
|
startedAt: Date.now(),
|
|
@@ -434,6 +473,7 @@ export default function subagentExtension(
|
|
|
434
473
|
failedToolPatches.clear();
|
|
435
474
|
stopWidgetTimer();
|
|
436
475
|
widgetItems.clear();
|
|
476
|
+
retainedWidgetTaskIds.clear();
|
|
437
477
|
activeTui = undefined;
|
|
438
478
|
widgetInstalled = false;
|
|
439
479
|
if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
@@ -451,7 +491,7 @@ export default function subagentExtension(
|
|
|
451
491
|
pi.on("input", (event) => {
|
|
452
492
|
if (event.source === "extension") return;
|
|
453
493
|
for (const [id, item] of widgetItems) {
|
|
454
|
-
if (item.status !== "working") widgetItems.delete(id);
|
|
494
|
+
if (item.status !== "working" && !retainedWidgetTaskIds.has(item.taskId)) widgetItems.delete(id);
|
|
455
495
|
}
|
|
456
496
|
requestWidgetRender();
|
|
457
497
|
});
|
|
@@ -550,6 +590,7 @@ export default function subagentExtension(
|
|
|
550
590
|
...(modelClass === undefined ? {} : { modelClass }),
|
|
551
591
|
}),
|
|
552
592
|
startWidget: startWidgetItem,
|
|
593
|
+
setWidgetTaskRetained,
|
|
553
594
|
updateWidgetTokens,
|
|
554
595
|
updateWidgetActivity,
|
|
555
596
|
finishWidget: finishWidgetItem,
|
|
@@ -695,7 +736,7 @@ export default function subagentExtension(
|
|
|
695
736
|
if (role.isolation === "worktree") {
|
|
696
737
|
worktree = await createChildWorktree(ctx.cwd, entry.id, undefined, workflowSignal);
|
|
697
738
|
}
|
|
698
|
-
startWidgetItem(entry.id, role.name, launch.model.id, launch.thinkingLevel, entry.delegation.name, ctx);
|
|
739
|
+
startWidgetItem(entry.id, entry.id, role.name, launch.model.id, launch.thinkingLevel, entry.delegation.name, ctx);
|
|
699
740
|
setState("running", "");
|
|
700
741
|
emitUpdate(emitToolUpdates);
|
|
701
742
|
return {
|