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.
Files changed (39) hide show
  1. package/CHANGELOG.md +40 -4
  2. package/README.md +97 -7
  3. package/package.json +1 -1
  4. package/skills/pi-subagents/SKILL.md +6 -1
  5. package/src/agents/agent-management.ts +6 -1
  6. package/src/agents/agents.ts +55 -11
  7. package/src/extension/companion-suggestions.ts +359 -0
  8. package/src/extension/config.ts +27 -4
  9. package/src/extension/doctor.ts +2 -0
  10. package/src/extension/fanout-child.ts +1 -0
  11. package/src/extension/index.ts +58 -4
  12. package/src/extension/schemas.ts +2 -2
  13. package/src/runs/background/async-execution.ts +138 -33
  14. package/src/runs/background/async-job-tracker.ts +77 -1
  15. package/src/runs/background/async-status.ts +41 -9
  16. package/src/runs/background/chain-root-attachment.ts +34 -4
  17. package/src/runs/background/control-channel.ts +50 -0
  18. package/src/runs/background/run-status.ts +1 -0
  19. package/src/runs/background/stale-run-reconciler.ts +28 -1
  20. package/src/runs/background/subagent-runner.ts +454 -115
  21. package/src/runs/foreground/chain-execution.ts +29 -7
  22. package/src/runs/foreground/execution.ts +24 -6
  23. package/src/runs/foreground/subagent-executor.ts +209 -35
  24. package/src/runs/shared/acceptance.ts +45 -22
  25. package/src/runs/shared/dynamic-fanout.ts +1 -1
  26. package/src/runs/shared/model-fallback.ts +4 -0
  27. package/src/runs/shared/nested-events.ts +58 -0
  28. package/src/runs/shared/parallel-utils.ts +49 -1
  29. package/src/runs/shared/pi-args.ts +5 -3
  30. package/src/runs/shared/pi-spawn.ts +52 -20
  31. package/src/runs/shared/single-output.ts +2 -0
  32. package/src/runs/shared/subagent-prompt-runtime.ts +2 -2
  33. package/src/runs/shared/worktree.ts +28 -5
  34. package/src/shared/artifacts.ts +15 -1
  35. package/src/shared/fork-context.ts +133 -22
  36. package/src/shared/types.ts +81 -3
  37. package/src/shared/utils.ts +99 -14
  38. package/src/slash/slash-commands.ts +117 -0
  39. 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 summaryStr =
1559
+ const summaryParts = [
1551
1560
  totalSummary.toolCount || totalSummary.tokens
1552
- ? ` | ${totalSummary.toolCount} tools, ${formatTokens(totalSummary.tokens)} tok, ${formatDuration(totalSummary.durationMs)}`
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]") : "";