@ferris1225/pi-subagents 4.2.7 → 4.2.8
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 +25 -2
- package/package.json +1 -1
- package/src/agents.ts +237 -237
- package/src/announcements.ts +81 -78
- package/src/dispatch.ts +73 -10
- package/src/format.ts +181 -165
- package/src/index.ts +102 -100
- package/src/prompt.ts +1 -1
- package/src/runtime.ts +33 -0
- package/src/status.ts +66 -0
- package/src/widget.ts +268 -266
- package/src/worktree.ts +974 -943
package/src/announcements.ts
CHANGED
|
@@ -1,78 +1,81 @@
|
|
|
1
|
-
/** Session-start recovery, stale-config migration, and
|
|
2
|
-
|
|
3
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import { loadConfig, saveConfig } from "./config.ts";
|
|
6
|
-
import { availableModelsInScope, filterUnavailableModelOverrides } from "./models.ts";
|
|
7
|
-
import { announceRecoveryRecords } from "./recovery.ts";
|
|
8
|
-
import type { SubagentRuntime } from "./runtime.ts";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
await
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
1
|
+
/** Session-start recovery, stale-config migration, and progress-surface installation. */
|
|
2
|
+
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { loadConfig, saveConfig } from "./config.ts";
|
|
6
|
+
import { availableModelsInScope, filterUnavailableModelOverrides } from "./models.ts";
|
|
7
|
+
import { announceRecoveryRecords } from "./recovery.ts";
|
|
8
|
+
import type { SubagentRuntime } from "./runtime.ts";
|
|
9
|
+
import { installActiveRunsStatus } from "./status.ts";
|
|
10
|
+
import { installActiveRunsWidget } from "./widget.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One-time-per-stale-override migration: keep agent model selections Pi still
|
|
14
|
+
* reports as available, drop the rest back to dynamic main-model routing, and
|
|
15
|
+
* tell the user what was removed. Saving the cleaned config is what makes it
|
|
16
|
+
* one-time — the dropped refs no longer exist to re-trigger the notice.
|
|
17
|
+
*/
|
|
18
|
+
async function migrateUnavailableAgentModels(
|
|
19
|
+
ctx: { ui: { notify: (message: string, kind: "info" | "warning" | "error") => void } } & Parameters<typeof availableModelsInScope>[0],
|
|
20
|
+
runtime: SubagentRuntime,
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
try {
|
|
23
|
+
const config = await loadConfig(runtime.configPath);
|
|
24
|
+
const overrides = Object.entries(config.agentModels);
|
|
25
|
+
if (overrides.length === 0) return;
|
|
26
|
+
const { kept, dropped } = filterUnavailableModelOverrides(config.agentModels, availableModelsInScope(ctx));
|
|
27
|
+
if (dropped.length === 0) return;
|
|
28
|
+
await saveConfig({ ...config, agentModels: kept }, runtime.configPath);
|
|
29
|
+
const list = dropped.map(({ agent, ref }) => `${agent}: ${ref}`).join(", ");
|
|
30
|
+
ctx.ui.notify(
|
|
31
|
+
`pi-subagents: removed stale agent model overrides that are no longer available (${list}). Those agents now follow the current main model; run /subagents-setup to re-pick.`,
|
|
32
|
+
"warning",
|
|
33
|
+
);
|
|
34
|
+
} catch {
|
|
35
|
+
/* migration failures are non-fatal */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function registerAnnouncements(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
40
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
41
|
+
if (!existsSync(runtime.configPath)) {
|
|
42
|
+
ctx.ui.notify(
|
|
43
|
+
"pi-subagents: no configuration yet — run /subagents-setup to pick agents, models, and thinking strengths. Defaults (all built-in agents on the main model) apply until then.",
|
|
44
|
+
"info",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
await announceRecoveryRecords(runtime.configPath, ctx);
|
|
48
|
+
await migrateUnavailableAgentModels(ctx, runtime);
|
|
49
|
+
// Restore starts at extension load and session_start fires right behind
|
|
50
|
+
// it, so without this the notice reports whatever the race left behind.
|
|
51
|
+
await runtime.durableRestore;
|
|
52
|
+
if (!runtime.restoredNotified && runtime.restoredRunIds.length > 0) {
|
|
53
|
+
runtime.restoredNotified = true;
|
|
54
|
+
const ids = runtime.restoredRunIds.map((id) => `#${id}`).join(", ");
|
|
55
|
+
ctx.ui.notify(
|
|
56
|
+
`pi-subagents: restored ${runtime.restoredRunIds.length} interrupted thread${runtime.restoredRunIds.length === 1 ? "" : "s"} (${ids}) with retained context. subagent_control resume continues one.`,
|
|
57
|
+
"info",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
// The footer status works in every UI host (TUI and RPC); the widget is TUI-only.
|
|
61
|
+
installActiveRunsStatus(ctx);
|
|
62
|
+
if (ctx.mode !== "tui") return;
|
|
63
|
+
installActiveRunsWidget(ctx);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Compaction failures are otherwise silent in long orchestration sessions
|
|
67
|
+
// where subagent results accumulate; aborted (user-cancelled) compactions
|
|
68
|
+
// are deliberate and not worth a notice.
|
|
69
|
+
pi.on("session_compact_failed", async (event, ctx) => {
|
|
70
|
+
if (event.aborted && !event.errorMessage) return;
|
|
71
|
+
const detail = event.errorMessage ? `: ${event.errorMessage}` : "";
|
|
72
|
+
if (event.willRetry) {
|
|
73
|
+
ctx.ui.notify(`pi-subagents: session compaction failed${detail} — retrying automatically.`, "warning");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
ctx.ui.notify(
|
|
77
|
+
`pi-subagents: session compaction failed${detail}. Long threads may hit context limits soon; run /compact to retry or trim old results.`,
|
|
78
|
+
"error",
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
}
|
package/src/dispatch.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* completion ownership live in thread-lifecycle.ts.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
9
|
+
import { StringEnum, type Usage } from "@earendil-works/pi-ai";
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { Text } from "@earendil-works/pi-tui";
|
|
12
12
|
import { join, resolve } from "node:path";
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
formatToolActivity,
|
|
20
20
|
monitor,
|
|
21
21
|
statusIcon,
|
|
22
|
+
statusLabel,
|
|
23
|
+
sumUsage,
|
|
22
24
|
type RunView,
|
|
23
25
|
type RunWaitReason,
|
|
24
26
|
} from "./monitor.ts";
|
|
@@ -32,6 +34,7 @@ import {
|
|
|
32
34
|
type SingleResult,
|
|
33
35
|
type SubagentDetails,
|
|
34
36
|
type SubagentLiveEvent,
|
|
37
|
+
type UsageStats,
|
|
35
38
|
} from "./spawn.ts";
|
|
36
39
|
import {
|
|
37
40
|
createBackgroundDispatcher,
|
|
@@ -116,6 +119,31 @@ export function defaultIsolationMode(
|
|
|
116
119
|
return mode === "parallel" && writeCapable ? "worktree" : "shared";
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
/** Map the child's own usage tally onto pi's tool-result `Usage`, so sub-agent
|
|
123
|
+
* token spend lands in the parent's footer, /session, and RPC session totals
|
|
124
|
+
* instead of being invisible. Only the total cost is known here: a child
|
|
125
|
+
* reports one cost number, not a per-bucket split. */
|
|
126
|
+
function toToolUsage(stats: UsageStats): Usage {
|
|
127
|
+
return {
|
|
128
|
+
input: stats.input,
|
|
129
|
+
output: stats.output,
|
|
130
|
+
cacheRead: stats.cacheRead,
|
|
131
|
+
cacheWrite: stats.cacheWrite,
|
|
132
|
+
totalTokens: stats.input + stats.output + stats.cacheRead + stats.cacheWrite,
|
|
133
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: stats.cost },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Usage of the runs awaited in-turn. Omitted entirely in the background path:
|
|
138
|
+
* those children have not finished when the tool returns, so any number there
|
|
139
|
+
* would be a fabrication. */
|
|
140
|
+
function toolUsage(runtime: SubagentRuntime, runIds: number[]): { usage?: Usage } {
|
|
141
|
+
const parts = runIds
|
|
142
|
+
.map((id) => runtime.settledRuns.get(id)?.usage)
|
|
143
|
+
.filter((usage): usage is UsageStats => usage !== undefined);
|
|
144
|
+
return parts.length > 0 ? { usage: toToolUsage(sumUsage(parts)) } : {};
|
|
145
|
+
}
|
|
146
|
+
|
|
119
147
|
/** In-turn wait behind dispatch `wait: true` — the escape hatch for one-shot
|
|
120
148
|
* `pi -p` parents that exit at end of turn: hold the call until every run it
|
|
121
149
|
* started settles, then hand back their result blocks. Interactive sessions
|
|
@@ -130,6 +158,7 @@ export async function awaitRunResults(
|
|
|
130
158
|
signal: AbortSignal | undefined,
|
|
131
159
|
maxResultLines: number,
|
|
132
160
|
fallbackCwd: string,
|
|
161
|
+
onProgress?: (text: string) => void,
|
|
133
162
|
): Promise<string> {
|
|
134
163
|
const waitForRun = (runId: number): Promise<{ result?: SingleResult; note?: string }> => {
|
|
135
164
|
const already = runtime.settledRuns.get(runId);
|
|
@@ -189,12 +218,38 @@ export async function awaitRunResults(
|
|
|
189
218
|
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
190
219
|
});
|
|
191
220
|
};
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
221
|
+
// One shared subscription drives the progress line: each waiter already
|
|
222
|
+
// subscribes for its own settlement, and the tool card wants a single
|
|
223
|
+
// rolled-up line rather than one per run.
|
|
224
|
+
let lastProgress: string | undefined;
|
|
225
|
+
const emitProgress = onProgress
|
|
226
|
+
? (): void => {
|
|
227
|
+
const parts = runIds.map((id) => {
|
|
228
|
+
const settled = runtime.settledRuns.get(id);
|
|
229
|
+
if (settled) return `#${id} ${isFailedResult(settled) ? "failed" : "done"}`;
|
|
230
|
+
const live = monitor.findRun(id);
|
|
231
|
+
return live ? `#${id} ${statusLabel(live.status)}` : `#${id} …`;
|
|
232
|
+
});
|
|
233
|
+
const text = `Waiting in-turn on ${runIds.length} run${runIds.length === 1 ? "" : "s"} · ${parts.join(", ")}`;
|
|
234
|
+
// The monitor notifies on every usage and activity change; this line
|
|
235
|
+
// names only statuses, so most notifications leave it identical.
|
|
236
|
+
if (text === lastProgress) return;
|
|
237
|
+
lastProgress = text;
|
|
238
|
+
onProgress(text);
|
|
239
|
+
}
|
|
240
|
+
: undefined;
|
|
241
|
+
const progressUnsub = emitProgress ? monitor.subscribe(emitProgress) : undefined;
|
|
242
|
+
emitProgress?.();
|
|
243
|
+
try {
|
|
244
|
+
const outcomes = await Promise.all(runIds.map(waitForRun));
|
|
245
|
+
return outcomes.map((outcome) =>
|
|
246
|
+
outcome.result
|
|
247
|
+
? formatCompletionBlock(outcome.result, maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, outcome.result.projectCwd ?? fallbackCwd) })
|
|
248
|
+
: (outcome.note ?? "(no outcome)"),
|
|
249
|
+
).join("\n\n");
|
|
250
|
+
} finally {
|
|
251
|
+
progressUnsub?.();
|
|
252
|
+
}
|
|
198
253
|
}
|
|
199
254
|
|
|
200
255
|
export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
@@ -332,7 +387,13 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
332
387
|
"Dispatch isolated background agents for recon, implementation, cleanup, docs sync, or result merging; never blocks your turn, and completions wake you automatically.",
|
|
333
388
|
parameters: SubagentParams,
|
|
334
389
|
|
|
335
|
-
async execute(_toolCallId, params, signal,
|
|
390
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
391
|
+
// `wait: true` holds this call for minutes and would otherwise show a
|
|
392
|
+
// blank card; the background path returns at once and has nothing to
|
|
393
|
+
// stream. Frames carry the final details shape because renderResult
|
|
394
|
+
// falls back to "(no output)" without it.
|
|
395
|
+
const makeProgress = (details: SubagentDetails): ((text: string) => void) | undefined =>
|
|
396
|
+
onUpdate ? (text: string): void => onUpdate({ content: [{ type: "text", text }], details }) : undefined;
|
|
336
397
|
// Run ids are allocated below; restore raises the allocator above every
|
|
337
398
|
// id a durable record still owns, so a dispatch racing it could hand a
|
|
338
399
|
// fresh run the id of a parked thread and overwrite its record.
|
|
@@ -438,7 +499,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
438
499
|
const startedIds = startedRuns
|
|
439
500
|
.map((result) => result.runId)
|
|
440
501
|
.filter((id): id is number => id !== undefined);
|
|
441
|
-
const blocks = await awaitRunResults(runtime, startedIds, signal, config.maxResultLines, ctx.cwd);
|
|
502
|
+
const blocks = await awaitRunResults(runtime, startedIds, signal, config.maxResultLines, ctx.cwd, makeProgress(makeDetails("parallel", true)(results)));
|
|
442
503
|
const text = [
|
|
443
504
|
`Started ${started} subagent${started === 1 ? "" : "s"} (${startedRefs.join(", ")}) and waited in-turn.`,
|
|
444
505
|
...(failureLines.length > 0
|
|
@@ -450,6 +511,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
450
511
|
return {
|
|
451
512
|
content: [{ type: "text", text }],
|
|
452
513
|
details: makeDetails("parallel", true)(results),
|
|
514
|
+
...toolUsage(runtime, startedIds),
|
|
453
515
|
};
|
|
454
516
|
}
|
|
455
517
|
const text = [
|
|
@@ -483,10 +545,11 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
483
545
|
}
|
|
484
546
|
const runRef = result.runId === undefined ? result.agent : `#${result.runId} ${result.agent}`;
|
|
485
547
|
if (params.wait && result.runId !== undefined) {
|
|
486
|
-
const blocks = await awaitRunResults(runtime, [result.runId], signal, config.maxResultLines, ctx.cwd);
|
|
548
|
+
const blocks = await awaitRunResults(runtime, [result.runId], signal, config.maxResultLines, ctx.cwd, makeProgress(makeDetails("single", true)([result])));
|
|
487
549
|
return {
|
|
488
550
|
content: [{ type: "text", text: `Started ${runRef} and waited in-turn.\n\n${blocks}` }],
|
|
489
551
|
details: makeDetails("single", true)([result]),
|
|
552
|
+
...toolUsage(runtime, [result.runId]),
|
|
490
553
|
};
|
|
491
554
|
}
|
|
492
555
|
return {
|