@henryqw/pi-subagent 6.1.0 → 6.1.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 +14 -2
- package/extensions/delegate-flow.ts +39 -11
- package/extensions/subagent.ts +47 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ pi install npm:@henryqw/pi-subagent
|
|
|
30
30
|
| `delegate_flow` | tool | Package-owned parallel implementation and declared-order Git integration for 1–8 independent units. |
|
|
31
31
|
| `delegate_flow_continue` | tool | Repair the blocked Flow unit once in its existing worktree. |
|
|
32
32
|
|
|
33
|
-
All three delegation tool blocks use
|
|
33
|
+
All three delegation tool blocks use Pi's default boxed shell and background. Their compact custom content is an immutable call label, foreground aggregate partial-result status, and bounded final summaries—no expanded view.
|
|
34
34
|
|
|
35
35
|
### `delegate_task`
|
|
36
36
|
|
|
@@ -53,7 +53,7 @@ Parallel mode starts entries concurrently, waits for every entry, and reports th
|
|
|
53
53
|
|
|
54
54
|
Background workflows are session-scoped. Session shutdown or reload aborts them and may deliver only recoverable-work evidence or no follow-up message.
|
|
55
55
|
|
|
56
|
-
The transient
|
|
56
|
+
The transient status widget renders one line per child with: status glyph, role, status label, task summary, activity (thinking… or active tool with elapsed time and path basename), and metrics (completed turns, started tools, model, thinking level, tokens, total duration). Rows are ordered active-first (working items first, stable insertion order for the rest). A hard six-physical-line maximum applies: when total items are six or fewer, all child rows render; above six, five child rows plus one status-aware overflow line render (`… N more · X working · Y complete · Z failed · W stopped`). Terminal rows clear on the next real user input; active rows persist until the child settles. The final `delegate_task` block is deliberately minimal: bounded final summaries with role attribution for parallel/chain, and only retained-worktree recovery paths. It has no expanded view.
|
|
57
57
|
|
|
58
58
|
Each delegation resolves its own Role, resources, route, and optional worktree request. When available, `isolation: worktree` gives each entry a deterministic separate worktree; non-Git or unborn-`HEAD` contexts may use Main's cwd. Siblings and chain steps never implicitly share one created worktree.
|
|
59
59
|
|
|
@@ -76,6 +76,18 @@ A rebase that drops all unit commits is a no-op: Flow validates it, skips Review
|
|
|
76
76
|
|
|
77
77
|
`delegate_task` remains generic with its ordinary isolation behavior. Flow uses the package-shipped Implementer by default and the package-shipped Reviewer only when a unit requests review; same-named user Roles remain supported overrides.
|
|
78
78
|
|
|
79
|
+
### Delegate UI summary
|
|
80
|
+
|
|
81
|
+
| Aspect | Behavior |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| Call label | `delegate_task · single/parallel/chain · N task(s)`; `delegate_flow · parallel→serial · N unit(s)`; `delegate_flow_continue · repair continuation` |
|
|
84
|
+
| Partial progress | `delegate_task`: aggregate counts (running/pending/complete/failed/skipped); `delegate_flow`: phase transitions (setup → implement → verify/integrate → review → repair) |
|
|
85
|
+
| Widget rows | One line per child: glyph, role, status, task, activity, metrics |
|
|
86
|
+
| Ordering | Active-first stable (working first, then insertion order) |
|
|
87
|
+
| Line cap | 6 physical lines max (≤6 items: all child rows; >6 items: 5 rows + 1 status-aware overflow) |
|
|
88
|
+
| Terminal retention | Active rows persist; terminal rows clear on next user input |
|
|
89
|
+
| Final result | Bounded summaries with recovery paths; no expanded view |
|
|
90
|
+
|
|
79
91
|
## Config
|
|
80
92
|
|
|
81
93
|
pi-subagent owns the extension-named config directory `~/.pi/agent/config/pi-subagent/`, which holds two kinds of user-owned configuration: one Markdown file per Role (see [Roles](#roles)) and its own optional JSON file below. Model routing is *not* configured here; children resolve routes through the shared `@henryqw/pi-task-models` config at `~/.pi/agent/config/pi-task-models.json`.
|
|
@@ -108,6 +108,7 @@ type FlowState = {
|
|
|
108
108
|
};
|
|
109
109
|
|
|
110
110
|
type UsageMeter = { usage?: Usage };
|
|
111
|
+
type FlowProgress = { line: string };
|
|
111
112
|
|
|
112
113
|
type CommandResult = {
|
|
113
114
|
stdout: string;
|
|
@@ -176,9 +177,18 @@ export function parseDelegateFlowContinue(value: unknown): Static<typeof Delegat
|
|
|
176
177
|
};
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
function unitCount(count: number): string {
|
|
181
|
+
return `${count} unit${count === 1 ? "" : "s"}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
179
184
|
function flowCallLabel(args: { units?: unknown }): string {
|
|
180
185
|
const count = Array.isArray(args.units) ? args.units.length : 0;
|
|
181
|
-
return `delegate_flow ·
|
|
186
|
+
return `delegate_flow · parallel→serial · ${unitCount(count)}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isFlowProgress(value: unknown): value is FlowProgress {
|
|
190
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
191
|
+
&& typeof (value as { line?: unknown }).line === "string";
|
|
182
192
|
}
|
|
183
193
|
|
|
184
194
|
function flowResultLines(text: string): string[] {
|
|
@@ -609,6 +619,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
609
619
|
signal: AbortSignal | undefined,
|
|
610
620
|
ctx: ExtensionContext,
|
|
611
621
|
meter: UsageMeter,
|
|
622
|
+
emitProgress: (line: string) => void,
|
|
612
623
|
) => {
|
|
613
624
|
assertCurrent(flow);
|
|
614
625
|
const main = flow.main!;
|
|
@@ -624,6 +635,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
624
635
|
|
|
625
636
|
const implementationFailure = settlementFailure(unit.implementation!);
|
|
626
637
|
if (implementationFailure) return block(flow, unit, "implementer", implementationFailure, meter);
|
|
638
|
+
emitProgress(`verify/integrate · unit ${flow.index + 1}/${flow.units.length}`);
|
|
627
639
|
|
|
628
640
|
let inspected = await inspectUnit(unit, false, signal);
|
|
629
641
|
assertCurrent(flow);
|
|
@@ -690,6 +702,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
690
702
|
if (reviewCriterion !== undefined) {
|
|
691
703
|
const reviewer = flow.reviewer;
|
|
692
704
|
if (!reviewer) return terminal(flow, "infrastructure", "Flow Reviewer was not resolved for a unit that requires review.", meter);
|
|
705
|
+
emitProgress(`review · unit ${flow.index + 1}/${flow.units.length}`);
|
|
693
706
|
let evidence;
|
|
694
707
|
try {
|
|
695
708
|
evidence = await prepareExactReviewEvidence({ base: main.expectedHead, tip, worktree: unit.worktree.path }, signal);
|
|
@@ -787,16 +800,16 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
787
800
|
"If a Flow blocks, inspect its classification and call delegate_flow_continue once with explicit repair guidance; modelClass may replace that one repair's current class.",
|
|
788
801
|
],
|
|
789
802
|
parameters: DelegateFlowSchema,
|
|
790
|
-
renderShell: "self",
|
|
791
803
|
renderCall(args, theme, _context) {
|
|
792
804
|
return renderToolLines([theme.fg("toolTitle", flowCallLabel(args))], theme);
|
|
793
805
|
},
|
|
794
|
-
renderResult(result,
|
|
806
|
+
renderResult(result, { isPartial }, theme, _context) {
|
|
807
|
+
if (isPartial) return renderToolLines(isFlowProgress(result.details) ? [theme.fg("muted", result.details.line)] : [], theme);
|
|
795
808
|
const text = result.content.find((part) => part.type === "text")?.text ?? "(no output)";
|
|
796
809
|
return renderToolLines(flowResultLines(text), theme);
|
|
797
810
|
},
|
|
798
811
|
prepareArguments: parseDelegateFlow,
|
|
799
|
-
async execute(toolCallId, params, signal,
|
|
812
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
800
813
|
const request = parseDelegateFlow(params);
|
|
801
814
|
if (active) throw new Error("delegate_flow rejected because another Flow is active.");
|
|
802
815
|
const roles = runtime.loadRoles();
|
|
@@ -805,6 +818,10 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
805
818
|
const reviewer = needsReviewer ? roles.find(({ name }) => name === "reviewer") : undefined;
|
|
806
819
|
if (!implementer) throw new Error("delegate_flow requires an implementer Role.");
|
|
807
820
|
if (needsReviewer && !reviewer) throw new Error("delegate_flow requires a reviewer Role when a unit declares review.");
|
|
821
|
+
const emitProgress = (line: string) => {
|
|
822
|
+
const progress: FlowProgress = { line };
|
|
823
|
+
onUpdate?.({ content: [{ type: "text", text: progress.line }], details: progress });
|
|
824
|
+
};
|
|
808
825
|
const flow: FlowState = {
|
|
809
826
|
phase: "running",
|
|
810
827
|
generation: runtime.getSessionGeneration(),
|
|
@@ -818,6 +835,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
818
835
|
warnings: [],
|
|
819
836
|
};
|
|
820
837
|
active = flow;
|
|
838
|
+
emitProgress(`setup · ${unitCount(request.units.length)}`);
|
|
821
839
|
const operationSignal = bindSignal(flow, signal);
|
|
822
840
|
const meter: UsageMeter = {};
|
|
823
841
|
let setupComplete = false;
|
|
@@ -855,6 +873,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
855
873
|
await checkMain(flow.main, operationSignal);
|
|
856
874
|
assertCurrent(flow);
|
|
857
875
|
setupComplete = true;
|
|
876
|
+
let completedImplementers = 0;
|
|
858
877
|
const settlements = await Promise.all(flow.units.map((unit, index) => runChild(
|
|
859
878
|
flow,
|
|
860
879
|
flow.implementer,
|
|
@@ -866,11 +885,15 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
866
885
|
operationSignal,
|
|
867
886
|
ctx,
|
|
868
887
|
meter,
|
|
869
|
-
))
|
|
888
|
+
).then((settlement) => {
|
|
889
|
+
completedImplementers += 1;
|
|
890
|
+
emitProgress(`implement · ${completedImplementers}/${flow.units.length} complete`);
|
|
891
|
+
return settlement;
|
|
892
|
+
})));
|
|
870
893
|
for (const [index, settlement] of settlements.entries()) flow.units[index]!.implementation = settlement;
|
|
871
894
|
assertCurrent(flow);
|
|
872
895
|
if (operationSignal.aborted) operationSignal.throwIfAborted();
|
|
873
|
-
return await processFlow(flow, toolCallId, operationSignal, ctx, meter);
|
|
896
|
+
return await processFlow(flow, toolCallId, operationSignal, ctx, meter, emitProgress);
|
|
874
897
|
} catch (error) {
|
|
875
898
|
if (!setupComplete) {
|
|
876
899
|
for (const unit of [...flow.units].reverse()) {
|
|
@@ -892,16 +915,16 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
892
915
|
promptSnippet: "Repair and continue the blocked deterministic Flow",
|
|
893
916
|
promptGuidelines: ["Call delegate_flow_continue only after delegate_flow reports a repairable block, with explicit guidance addressing that block."],
|
|
894
917
|
parameters: DelegateFlowContinueSchema,
|
|
895
|
-
renderShell: "self",
|
|
896
918
|
renderCall(_args, theme, _context) {
|
|
897
|
-
return renderToolLines([theme.fg("toolTitle", "delegate_flow_continue ·
|
|
919
|
+
return renderToolLines([theme.fg("toolTitle", "delegate_flow_continue · repair continuation")], theme);
|
|
898
920
|
},
|
|
899
|
-
renderResult(result,
|
|
921
|
+
renderResult(result, { isPartial }, theme, _context) {
|
|
922
|
+
if (isPartial) return renderToolLines(isFlowProgress(result.details) ? [theme.fg("muted", result.details.line)] : [], theme);
|
|
900
923
|
const text = result.content.find((part) => part.type === "text")?.text ?? "(no output)";
|
|
901
924
|
return renderToolLines(flowResultLines(text), theme);
|
|
902
925
|
},
|
|
903
926
|
prepareArguments: parseDelegateFlowContinue,
|
|
904
|
-
async execute(toolCallId, params, signal,
|
|
927
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
905
928
|
const { guidance, modelClass } = parseDelegateFlowContinue(params);
|
|
906
929
|
const flow = active;
|
|
907
930
|
if (!flow) throw new Error("delegate_flow_continue requires an active blocked Flow.");
|
|
@@ -916,7 +939,12 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
916
939
|
if (modelClass !== undefined) unit.modelClass = modelClass;
|
|
917
940
|
const operationSignal = bindSignal(flow, signal);
|
|
918
941
|
const meter: UsageMeter = {};
|
|
942
|
+
const emitProgress = (line: string) => {
|
|
943
|
+
const progress: FlowProgress = { line };
|
|
944
|
+
onUpdate?.({ content: [{ type: "text", text: progress.line }], details: progress });
|
|
945
|
+
};
|
|
919
946
|
try {
|
|
947
|
+
emitProgress(`repair · unit ${flow.index + 1}/${flow.units.length}`);
|
|
920
948
|
unit.implementation = await runChild(
|
|
921
949
|
flow,
|
|
922
950
|
flow.implementer,
|
|
@@ -931,7 +959,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
931
959
|
);
|
|
932
960
|
assertCurrent(flow);
|
|
933
961
|
if (operationSignal.aborted) operationSignal.throwIfAborted();
|
|
934
|
-
return await processFlow(flow, toolCallId, operationSignal, ctx, meter);
|
|
962
|
+
return await processFlow(flow, toolCallId, operationSignal, ctx, meter, emitProgress);
|
|
935
963
|
} catch (error) {
|
|
936
964
|
return terminal(flow, "infrastructure", errorText(error), meter);
|
|
937
965
|
}
|
package/extensions/subagent.ts
CHANGED
|
@@ -57,7 +57,8 @@ import {
|
|
|
57
57
|
const SUBAGENT_TASK = "pi-subagent/delegateTask";
|
|
58
58
|
const WIDGET_KEY = "subagent-status";
|
|
59
59
|
const WIDGET_INTERVAL_MS = 80;
|
|
60
|
-
const
|
|
60
|
+
const MAX_WIDGET_ITEMS = 8;
|
|
61
|
+
const MAX_WIDGET_LINES = 6;
|
|
61
62
|
export const MAX_WIDGET_ACTIVE_TOOLS = 8;
|
|
62
63
|
const DEFAULT_TIMEOUT_POLICY = {
|
|
63
64
|
idleMs: DEFAULT_TIMEOUT_CONFIG.idleMinutes * 60_000,
|
|
@@ -178,9 +179,30 @@ function isWorkflowTransportDetails(value: unknown): value is WorkflowTransportD
|
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
function workflowCallLabel(args: { tasks?: unknown; chain?: unknown }): string {
|
|
181
|
-
if (Array.isArray(args.chain)) return `delegate_task ·
|
|
182
|
-
if (Array.isArray(args.tasks)) return `delegate_task ·
|
|
183
|
-
return "delegate_task ·
|
|
182
|
+
if (Array.isArray(args.chain)) return `delegate_task · chain · ${args.chain.length} task${args.chain.length === 1 ? "" : "s"}`;
|
|
183
|
+
if (Array.isArray(args.tasks)) return `delegate_task · parallel · ${args.tasks.length} task${args.tasks.length === 1 ? "" : "s"}`;
|
|
184
|
+
return "delegate_task · single · 1 task";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function workflowProgressLine(details: WorkflowTransportDetails): string {
|
|
188
|
+
let running = 0;
|
|
189
|
+
let pending = 0;
|
|
190
|
+
let complete = 0;
|
|
191
|
+
let failed = 0;
|
|
192
|
+
let skipped = 0;
|
|
193
|
+
for (const { status } of details.entries) {
|
|
194
|
+
switch (status) {
|
|
195
|
+
case "running": running += 1; break;
|
|
196
|
+
case "pending": pending += 1; break;
|
|
197
|
+
case "succeeded": complete += 1; break;
|
|
198
|
+
case "failed":
|
|
199
|
+
case "rejected": failed += 1; break;
|
|
200
|
+
case "skipped": skipped += 1; break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return [[running, "running"], [pending, "pending"], [complete, "complete"], [failed, "failed"], [skipped, "skipped"]]
|
|
204
|
+
.flatMap(([count, label]) => count ? [`${count} ${label}`] : [])
|
|
205
|
+
.join(" · ");
|
|
184
206
|
}
|
|
185
207
|
|
|
186
208
|
function workflowResultLines(details: WorkflowTransportDetails, theme: Theme): string[] {
|
|
@@ -215,18 +237,23 @@ function renderWidgetRows(
|
|
|
215
237
|
spinnerIndex: number,
|
|
216
238
|
theme: Theme,
|
|
217
239
|
): string[] {
|
|
218
|
-
const
|
|
240
|
+
const ordered = [...items.filter(({ status }) => status === "working"), ...items.filter(({ status }) => status !== "working")];
|
|
241
|
+
const visible = ordered.slice(0, ordered.length > MAX_WIDGET_LINES ? MAX_WIDGET_LINES - 1 : MAX_WIDGET_LINES);
|
|
219
242
|
if (!visible.length) return [];
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
243
|
+
const hidden = ordered.slice(visible.length);
|
|
244
|
+
const lines = visible.map((item) => truncateToWidth(
|
|
245
|
+
`${statusGlyph(item.status, spinnerIndex, theme)} ${theme.fg("accent", item.role)} · ${statusLabel(item.status)} · ${theme.fg("text", item.task)} · ${theme.fg("text", activityLabel(item, now))} · ${theme.fg("muted", activityMetrics(item, now))}`,
|
|
246
|
+
width,
|
|
247
|
+
));
|
|
248
|
+
if (hidden.length) {
|
|
249
|
+
const counts: Record<WidgetStatus, number> = { working: 0, success: 0, failure: 0, aborted: 0 };
|
|
250
|
+
for (const { status } of hidden) counts[status] += 1;
|
|
251
|
+
lines.push(truncateToWidth(theme.fg("muted", [
|
|
252
|
+
`… ${hidden.length} more`,
|
|
253
|
+
...(["working", "success", "failure", "aborted"] as const).flatMap((status) =>
|
|
254
|
+
counts[status] ? [`${counts[status]} ${statusLabel(status)}`] : []),
|
|
255
|
+
].join(" · ")), width));
|
|
256
|
+
}
|
|
230
257
|
return lines;
|
|
231
258
|
}
|
|
232
259
|
|
|
@@ -357,11 +384,11 @@ export default function subagentExtension(
|
|
|
357
384
|
) => {
|
|
358
385
|
if (!ctx.hasUI) return;
|
|
359
386
|
ensureWidget(ctx);
|
|
360
|
-
if (!widgetItems.has(id) && widgetItems.size >=
|
|
387
|
+
if (!widgetItems.has(id) && widgetItems.size >= MAX_WIDGET_ITEMS) {
|
|
361
388
|
for (const [oldestId, item] of widgetItems) {
|
|
362
389
|
if (item.status === "working") continue;
|
|
363
390
|
widgetItems.delete(oldestId);
|
|
364
|
-
if (widgetItems.size <
|
|
391
|
+
if (widgetItems.size < MAX_WIDGET_ITEMS) break;
|
|
365
392
|
}
|
|
366
393
|
}
|
|
367
394
|
widgetItems.set(id, {
|
|
@@ -580,13 +607,14 @@ export default function subagentExtension(
|
|
|
580
607
|
"delegate_task background applies to the whole selected workflow and returns before results exist; use it only when the user explicitly asks for non-blocking work.",
|
|
581
608
|
],
|
|
582
609
|
parameters: WorkflowSchema,
|
|
583
|
-
renderShell: "self",
|
|
584
610
|
renderCall(args, theme, _context) {
|
|
585
611
|
return renderToolLines([theme.fg("toolTitle", workflowCallLabel(args))], theme);
|
|
586
612
|
},
|
|
587
613
|
renderResult(result, { isPartial }, theme, _context) {
|
|
588
|
-
if (isPartial) return renderToolLines([], theme);
|
|
589
614
|
const details = result.details;
|
|
615
|
+
if (isPartial) return renderToolLines(isWorkflowTransportDetails(details)
|
|
616
|
+
? [theme.fg("muted", workflowProgressLine(details))]
|
|
617
|
+
: [], theme);
|
|
590
618
|
if (isWorkflowTransportDetails(details)) return renderToolLines(workflowResultLines(details, theme), theme);
|
|
591
619
|
if (typeof details === "object" && details !== null && (details as { background?: unknown }).background === true) {
|
|
592
620
|
return renderToolLines([theme.fg("muted", "Background workflow accepted.")], theme);
|