pi-subagents 0.31.1 → 0.32.0
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/CHANGELOG.md +40 -4
- package/README.md +97 -7
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +6 -1
- package/src/agents/agent-management.ts +6 -1
- package/src/agents/agents.ts +55 -11
- package/src/extension/companion-suggestions.ts +359 -0
- package/src/extension/config.ts +27 -4
- package/src/extension/doctor.ts +2 -0
- package/src/extension/fanout-child.ts +1 -0
- package/src/extension/index.ts +58 -4
- package/src/extension/schemas.ts +2 -2
- package/src/runs/background/async-execution.ts +138 -33
- package/src/runs/background/async-job-tracker.ts +77 -1
- package/src/runs/background/async-status.ts +41 -9
- package/src/runs/background/chain-root-attachment.ts +34 -4
- package/src/runs/background/control-channel.ts +50 -0
- package/src/runs/background/run-status.ts +1 -0
- package/src/runs/background/stale-run-reconciler.ts +28 -1
- package/src/runs/background/subagent-runner.ts +454 -115
- package/src/runs/foreground/chain-execution.ts +29 -7
- package/src/runs/foreground/execution.ts +24 -6
- package/src/runs/foreground/subagent-executor.ts +209 -35
- package/src/runs/shared/acceptance.ts +45 -22
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +4 -0
- package/src/runs/shared/nested-events.ts +58 -0
- package/src/runs/shared/parallel-utils.ts +49 -1
- package/src/runs/shared/pi-args.ts +5 -3
- package/src/runs/shared/pi-spawn.ts +52 -20
- package/src/runs/shared/single-output.ts +2 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +2 -2
- package/src/runs/shared/worktree.ts +28 -5
- package/src/shared/artifacts.ts +15 -1
- package/src/shared/fork-context.ts +133 -22
- package/src/shared/types.ts +81 -3
- package/src/shared/utils.ts +99 -14
- package/src/slash/slash-commands.ts +117 -0
- package/src/tui/render.ts +16 -4
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
17
17
|
import { isDynamicParallelStep, isParallelStep, type ChainStep } from "../shared/settings.ts";
|
|
18
18
|
import { findModelInfo, toModelInfo } from "../shared/model-info.ts";
|
|
19
|
+
import { formatTokens } from "../shared/formatters.ts";
|
|
19
20
|
import { assertJsonSchemaObject } from "../runs/shared/structured-output.ts";
|
|
20
21
|
import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
|
|
21
22
|
import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
buildSlashInitialResult,
|
|
25
26
|
failSlashResult,
|
|
26
27
|
finalizeSlashResult,
|
|
28
|
+
resolveSlashMessageDetails,
|
|
27
29
|
} from "./slash-live-state.ts";
|
|
28
30
|
import {
|
|
29
31
|
SLASH_RESULT_TYPE,
|
|
@@ -33,9 +35,11 @@ import {
|
|
|
33
35
|
SLASH_SUBAGENT_RESPONSE_EVENT,
|
|
34
36
|
SLASH_SUBAGENT_STARTED_EVENT,
|
|
35
37
|
SLASH_SUBAGENT_UPDATE_EVENT,
|
|
38
|
+
type Details,
|
|
36
39
|
type JsonSchemaObject,
|
|
37
40
|
type SingleResult,
|
|
38
41
|
type SubagentState,
|
|
42
|
+
type Usage,
|
|
39
43
|
} from "../shared/types.ts";
|
|
40
44
|
|
|
41
45
|
interface InlineConfig {
|
|
@@ -216,6 +220,113 @@ async function withSlashStatus<T>(
|
|
|
216
220
|
}
|
|
217
221
|
}
|
|
218
222
|
|
|
223
|
+
function emptyUsage(): Usage {
|
|
224
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function addUsage(target: Usage, source: Usage): void {
|
|
228
|
+
target.input += source.input;
|
|
229
|
+
target.output += source.output;
|
|
230
|
+
target.cacheRead += source.cacheRead;
|
|
231
|
+
target.cacheWrite += source.cacheWrite;
|
|
232
|
+
target.cost += source.cost;
|
|
233
|
+
target.turns += source.turns;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function usageHasValue(usage: Usage): boolean {
|
|
237
|
+
return usage.input !== 0 || usage.output !== 0 || usage.cacheRead !== 0 || usage.cacheWrite !== 0 || usage.cost !== 0 || usage.turns !== 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function assistantUsageFromMessage(message: unknown): Usage | undefined {
|
|
241
|
+
if (!message || typeof message !== "object") return undefined;
|
|
242
|
+
const msg = message as { role?: unknown; usage?: unknown };
|
|
243
|
+
if (msg.role !== "assistant" || !msg.usage || typeof msg.usage !== "object") return undefined;
|
|
244
|
+
const usage = msg.usage as {
|
|
245
|
+
input?: unknown;
|
|
246
|
+
output?: unknown;
|
|
247
|
+
cacheRead?: unknown;
|
|
248
|
+
cacheWrite?: unknown;
|
|
249
|
+
cost?: { total?: unknown };
|
|
250
|
+
};
|
|
251
|
+
return {
|
|
252
|
+
input: typeof usage.input === "number" ? usage.input : 0,
|
|
253
|
+
output: typeof usage.output === "number" ? usage.output : 0,
|
|
254
|
+
cacheRead: typeof usage.cacheRead === "number" ? usage.cacheRead : 0,
|
|
255
|
+
cacheWrite: typeof usage.cacheWrite === "number" ? usage.cacheWrite : 0,
|
|
256
|
+
cost: typeof usage.cost?.total === "number" ? usage.cost.total : 0,
|
|
257
|
+
turns: 1,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isSubagentDetails(value: unknown): value is Details {
|
|
262
|
+
if (!value || typeof value !== "object") return false;
|
|
263
|
+
const details = value as { mode?: unknown; results?: unknown };
|
|
264
|
+
return typeof details.mode === "string" && Array.isArray(details.results);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function detailsFromSessionEntry(entry: unknown): Details | undefined {
|
|
268
|
+
if (!entry || typeof entry !== "object") return undefined;
|
|
269
|
+
const record = entry as { type?: unknown; customType?: unknown; details?: unknown; message?: unknown };
|
|
270
|
+
if (record.type === "custom_message" && record.customType === SLASH_RESULT_TYPE) {
|
|
271
|
+
const details = resolveSlashMessageDetails(record.details)?.result.details;
|
|
272
|
+
return isSubagentDetails(details) ? details : undefined;
|
|
273
|
+
}
|
|
274
|
+
if (record.type !== "message" || !record.message || typeof record.message !== "object") return undefined;
|
|
275
|
+
const message = record.message as { role?: unknown; toolName?: unknown; details?: unknown };
|
|
276
|
+
if (message.role !== "toolResult" || message.toolName !== "subagent") return undefined;
|
|
277
|
+
return isSubagentDetails(message.details) ? message.details : undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function formatCostUsage(label: string, usage: Usage): string {
|
|
281
|
+
const extras = [
|
|
282
|
+
usage.cacheRead ? `cache read ${formatTokens(usage.cacheRead)}` : "",
|
|
283
|
+
usage.cacheWrite ? `cache write ${formatTokens(usage.cacheWrite)}` : "",
|
|
284
|
+
usage.turns ? `${usage.turns} turn${usage.turns === 1 ? "" : "s"}` : "",
|
|
285
|
+
].filter(Boolean);
|
|
286
|
+
return `${label}: ↑${formatTokens(usage.input)} ↓${formatTokens(usage.output)} $${usage.cost.toFixed(4)}${extras.length ? ` (${extras.join(", ")})` : ""}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function buildSubagentCostReport(ctx: ExtensionContext): string {
|
|
290
|
+
const parent = emptyUsage();
|
|
291
|
+
const childTotal = emptyUsage();
|
|
292
|
+
const total = emptyUsage();
|
|
293
|
+
const children: Array<{ label: string; usage: Usage; sessionFile?: string }> = [];
|
|
294
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
295
|
+
const message = entry.type === "message" ? (entry as { message?: unknown }).message : undefined;
|
|
296
|
+
const parentUsage = assistantUsageFromMessage(message);
|
|
297
|
+
if (parentUsage) addUsage(parent, parentUsage);
|
|
298
|
+
const details = detailsFromSessionEntry(entry);
|
|
299
|
+
if (!details) continue;
|
|
300
|
+
for (const result of details.results) {
|
|
301
|
+
if (!usageHasValue(result.usage)) continue;
|
|
302
|
+
const usage = { ...result.usage };
|
|
303
|
+
children.push({
|
|
304
|
+
label: `Child ${children.length + 1} (${result.agent})`,
|
|
305
|
+
usage,
|
|
306
|
+
...(result.sessionFile ? { sessionFile: result.sessionFile } : {}),
|
|
307
|
+
});
|
|
308
|
+
addUsage(childTotal, usage);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
addUsage(total, parent);
|
|
312
|
+
addUsage(total, childTotal);
|
|
313
|
+
const lines = [
|
|
314
|
+
"Subagent cost",
|
|
315
|
+
"",
|
|
316
|
+
formatCostUsage("Parent", parent),
|
|
317
|
+
];
|
|
318
|
+
if (children.length === 0) {
|
|
319
|
+
lines.push("No subagent child usage found in this session.");
|
|
320
|
+
} else {
|
|
321
|
+
for (const child of children) {
|
|
322
|
+
lines.push(formatCostUsage(child.label, child.usage));
|
|
323
|
+
if (child.sessionFile) lines.push(` Session: ${child.sessionFile}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
lines.push("────────────────────────────", formatCostUsage("Children", childTotal), formatCostUsage("Total", total));
|
|
327
|
+
return lines.join("\n");
|
|
328
|
+
}
|
|
329
|
+
|
|
219
330
|
function parseSingleRequiredArg(args: string, usage: string): { ok: true; value: string } | { ok: false; message: string } {
|
|
220
331
|
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
221
332
|
if (parts.length !== 1) return { ok: false, message: usage };
|
|
@@ -962,6 +1073,12 @@ export function registerSlashCommands(
|
|
|
962
1073
|
},
|
|
963
1074
|
});
|
|
964
1075
|
|
|
1076
|
+
pi.registerCommand("subagent-cost", {
|
|
1077
|
+
description: "Show parent and subagent child usage cost for this session",
|
|
1078
|
+
handler: async (_args, ctx) => {
|
|
1079
|
+
sendSlashText(pi, buildSubagentCostReport(ctx));
|
|
1080
|
+
},
|
|
1081
|
+
});
|
|
965
1082
|
|
|
966
1083
|
pi.registerCommand("subagents-doctor", {
|
|
967
1084
|
description: "Show subagent diagnostics",
|
package/src/tui/render.ts
CHANGED
|
@@ -236,6 +236,15 @@ function formatToolUseStat(count: number): string {
|
|
|
236
236
|
return `${count} tool use${count === 1 ? "" : "s"}`;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
function formatTotalCostStat(totalCost: Details["totalCost"] | undefined): string {
|
|
240
|
+
if (!totalCost || (totalCost.inputTokens === 0 && totalCost.outputTokens === 0 && totalCost.costUsd === 0)) return "";
|
|
241
|
+
const parts: string[] = [];
|
|
242
|
+
if (totalCost.inputTokens) parts.push(`in:${formatTokens(totalCost.inputTokens)}`);
|
|
243
|
+
if (totalCost.outputTokens) parts.push(`out:${formatTokens(totalCost.outputTokens)}`);
|
|
244
|
+
if (totalCost.costUsd) parts.push(`$${totalCost.costUsd.toFixed(4)}`);
|
|
245
|
+
return parts.join(" ");
|
|
246
|
+
}
|
|
247
|
+
|
|
239
248
|
function formatProgressStats(theme: Theme, progress: Pick<AgentProgress, "toolCount" | "tokens" | "durationMs"> | undefined, includeDuration = true): string {
|
|
240
249
|
if (!progress) return "";
|
|
241
250
|
const parts: string[] = [];
|
|
@@ -1315,7 +1324,7 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
|
|
|
1315
1324
|
}
|
|
1316
1325
|
const multiLabel = buildMultiProgressLabel(d, hasRunning);
|
|
1317
1326
|
const itemTitle = multiLabel.itemTitle;
|
|
1318
|
-
const stats = statJoin(theme, [multiLabel.headerLabel, formatProgressStats(theme, totalSummary)]);
|
|
1327
|
+
const stats = statJoin(theme, [multiLabel.headerLabel, formatProgressStats(theme, totalSummary), formatTotalCostStat(d.totalCost)]);
|
|
1319
1328
|
const glyph = hasRunning
|
|
1320
1329
|
? theme.fg("accent", runningGlyph(frame !== undefined ? (runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex) ?? 0) + frame : runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex)))
|
|
1321
1330
|
: failed
|
|
@@ -1547,10 +1556,13 @@ export function renderSubagentResult(
|
|
|
1547
1556
|
{ toolCount: 0, tokens: 0, durationMs: 0 },
|
|
1548
1557
|
);
|
|
1549
1558
|
|
|
1550
|
-
const
|
|
1559
|
+
const summaryParts = [
|
|
1551
1560
|
totalSummary.toolCount || totalSummary.tokens
|
|
1552
|
-
?
|
|
1553
|
-
: ""
|
|
1561
|
+
? `${totalSummary.toolCount} tools, ${formatTokens(totalSummary.tokens)} tok, ${formatDuration(totalSummary.durationMs)}`
|
|
1562
|
+
: "",
|
|
1563
|
+
formatTotalCostStat(d.totalCost),
|
|
1564
|
+
].filter(Boolean);
|
|
1565
|
+
const summaryStr = summaryParts.length ? ` | ${summaryParts.join(", ")}` : "";
|
|
1554
1566
|
|
|
1555
1567
|
const modeLabel = d.mode;
|
|
1556
1568
|
const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
|