@youngjurry/pi-agents 0.8.1 → 0.9.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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0 - 2026-09-10
4
+
5
+ - Render `/agent-usage` directly in the normal TUI transcript through Pi's TUI-only custom-entry API instead of opening an overlay.
6
+ - Add main-model and sub-agent-model usage breakdowns using the actual response model reported by each assistant call.
7
+ - Show per-model input, output, cache reads/writes, total tokens, cost, contributing Agent-session count, and usage-record count.
8
+ - Keep unattributable tool and summary usage in a separate `Tools/summaries` bucket.
9
+ - Preserve strict separation from LLM context and built-in `/session` cache accounting.
10
+
3
11
  ## 0.8.1 - 2026-09-10
4
12
 
5
13
  - Remove automatic legacy-file archival and its full main-session scan; the extension no longer archives or deletes user session data.
package/README.md CHANGED
@@ -64,7 +64,9 @@ Pi's built-in `/session` remains the authoritative view of the main Agent and it
64
64
  /agent-usage
65
65
  ```
66
66
 
67
- The read-only overlay reports main and child token totals, separate cache hit rates, combined tokens/cost, and any unreadable child sessions. It reads persisted child JSONL files without loading or waking their AgentSessions, does not add a main-session message, and does not alter provider-facing context. Pi currently has no extension hook that can add child tokens to built-in `/session` while excluding them from that command's cache statistics.
67
+ The command writes a detailed report directly into the normal TUI transcript, like `/session`; it does not open an overlay. The report includes main, sub-agent, and combined input/output/cache/token/cost totals, followed by separate main-model and sub-agent-model breakdowns. Each model row shows its tokens, prompt/output/cache details, number of contributing Agent sessions, and number of usage records. Non-model tool and summary usage is kept in an explicit `Tools/summaries` bucket rather than being misattributed to a model.
68
+
69
+ The report is stored as a TUI-only custom entry so it remains outside LLM context and does not affect `/session` message or token accounting. Reading the report does not load or wake child AgentSessions. Pi currently has no extension hook that can add child tokens to built-in `/session` while excluding them from that command's cache statistics.
68
70
 
69
71
  ## Tools
70
72
 
package/control.ts CHANGED
@@ -47,6 +47,7 @@ import {
47
47
  type AgentRole,
48
48
  type AgentRoleView,
49
49
  type AgentTranscriptView,
50
+ type AgentUsageBreakdownEntry,
50
51
  type AgentUsageReport,
51
52
  type AgentUsageTotals,
52
53
  type AgentView,
@@ -126,23 +127,77 @@ function addUsageTotals(target: AgentUsageTotals, source: AgentUsageTotals): voi
126
127
  target.cost += source.cost;
127
128
  }
128
129
 
129
- function sessionUsage(sessionManager: Pick<SessionManager, "getEntries">): AgentUsageTotals {
130
+ interface SessionUsageDetails {
131
+ totals: AgentUsageTotals;
132
+ breakdown: Map<string, AgentUsageBreakdownEntry>;
133
+ }
134
+
135
+ function addAttributedUsage(
136
+ breakdown: Map<string, AgentUsageBreakdownEntry>,
137
+ key: string,
138
+ usage: Usage,
139
+ ): void {
140
+ let item = breakdown.get(key);
141
+ if (!item) {
142
+ item = { key, usage: emptyUsageTotals(), sessionCount: 1, operations: 0 };
143
+ breakdown.set(key, item);
144
+ }
145
+ addUsage(item.usage, usage);
146
+ item.operations++;
147
+ }
148
+
149
+ function sessionUsageDetails(sessionManager: Pick<SessionManager, "getEntries">): SessionUsageDetails {
130
150
  const totals = emptyUsageTotals();
151
+ const breakdown = new Map<string, AgentUsageBreakdownEntry>();
131
152
  for (const entry of sessionManager.getEntries()) {
132
153
  if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
133
154
  addUsage(totals, entry.usage);
155
+ addAttributedUsage(breakdown, "Tools/summaries", entry.usage);
134
156
  }
135
157
  if (entry.type !== "message") continue;
136
158
  const message = entry.message;
137
- if (message.role === "assistant" || message.role === "toolResult") addUsage(totals, message.usage);
159
+ if (message.role === "assistant") {
160
+ addUsage(totals, message.usage);
161
+ const responseModel = (message as typeof message & { responseModel?: string }).responseModel;
162
+ addAttributedUsage(breakdown, `${message.provider}/${responseModel ?? message.model}`, message.usage);
163
+ } else if (message.role === "toolResult" && message.usage) {
164
+ addUsage(totals, message.usage);
165
+ addAttributedUsage(breakdown, "Tools/summaries", message.usage);
166
+ }
167
+ }
168
+ return { totals, breakdown };
169
+ }
170
+
171
+ function mergeUsageBreakdown(
172
+ target: Map<string, AgentUsageBreakdownEntry>,
173
+ source: Map<string, AgentUsageBreakdownEntry>,
174
+ ): void {
175
+ for (const item of source.values()) {
176
+ let aggregate = target.get(item.key);
177
+ if (!aggregate) {
178
+ aggregate = { key: item.key, usage: emptyUsageTotals(), sessionCount: 0, operations: 0 };
179
+ target.set(item.key, aggregate);
180
+ }
181
+ addUsageTotals(aggregate.usage, item.usage);
182
+ aggregate.sessionCount += item.sessionCount;
183
+ aggregate.operations += item.operations;
138
184
  }
139
- return totals;
140
185
  }
141
186
 
142
187
  function normalizeCost(value: number): number {
143
188
  return Math.round(value * 1_000_000_000) / 1_000_000_000;
144
189
  }
145
190
 
191
+ function finalizeBreakdown(breakdown: Map<string, AgentUsageBreakdownEntry>): AgentUsageBreakdownEntry[] {
192
+ return [...breakdown.values()]
193
+ .map((item) => ({
194
+ ...item,
195
+ usage: { ...item.usage, cost: normalizeCost(item.usage.cost) },
196
+ }))
197
+ .filter((item) => item.usage.total > 0 || item.usage.cost > 0)
198
+ .sort((left, right) => right.usage.total - left.usage.total || left.key.localeCompare(right.key));
199
+ }
200
+
146
201
  function normalizeAgentName(name: string): string {
147
202
  const normalized = name.trim();
148
203
  if (!normalized) throw new Error("task_name must not be empty");
@@ -1166,8 +1221,10 @@ export class AgentControl {
1166
1221
 
1167
1222
  getUsage(ctx: ExtensionContext): AgentUsageReport {
1168
1223
  this.callerPath(ctx);
1169
- const main = sessionUsage(ctx.sessionManager);
1224
+ const mainDetails = sessionUsageDetails(ctx.sessionManager);
1225
+ const main = mainDetails.totals;
1170
1226
  const subagents = emptyUsageTotals();
1227
+ const subagentBreakdown = new Map<string, AgentUsageBreakdownEntry>();
1171
1228
  let unreadableSubagents = 0;
1172
1229
  const countedSessionIds = new Set<string>();
1173
1230
  for (const record of this.agentsByPath.values()) {
@@ -1182,7 +1239,9 @@ export class AgentControl {
1182
1239
  const sessionId = manager.getSessionId();
1183
1240
  if (countedSessionIds.has(sessionId)) continue;
1184
1241
  countedSessionIds.add(sessionId);
1185
- addUsageTotals(subagents, sessionUsage(manager));
1242
+ const details = sessionUsageDetails(manager);
1243
+ addUsageTotals(subagents, details.totals);
1244
+ mergeUsageBreakdown(subagentBreakdown, details.breakdown);
1186
1245
  } catch {
1187
1246
  unreadableSubagents++;
1188
1247
  }
@@ -1197,6 +1256,8 @@ export class AgentControl {
1197
1256
  main,
1198
1257
  subagents,
1199
1258
  combined,
1259
+ mainBreakdown: finalizeBreakdown(mainDetails.breakdown),
1260
+ subagentBreakdown: finalizeBreakdown(subagentBreakdown),
1200
1261
  subagentCount: this.agentsByPath.size,
1201
1262
  unreadableSubagents,
1202
1263
  };
package/index.ts CHANGED
@@ -6,8 +6,15 @@ import { AgentControl } from "./control.ts";
6
6
  import { getAgentSettingsPath, loadAgentSettings, resolveAgentLimits } from "./settings.ts";
7
7
  import { createCollaborationTools } from "./tools.ts";
8
8
  import { migrateLegacyAgentStorage } from "./storage.ts";
9
- import { EXTENSION_ID, ROOT_PATH, type AgentLifecycleStatus, type AgentView } from "./types.ts";
10
- import { AgentPickerComponent, AgentTranscriptViewer, AgentUsageViewer, formatAgentUsage } from "./viewer.ts";
9
+ import {
10
+ EXTENSION_ID,
11
+ ROOT_PATH,
12
+ USAGE_ENTRY_TYPE,
13
+ type AgentLifecycleStatus,
14
+ type AgentUsageReport,
15
+ type AgentView,
16
+ } from "./types.ts";
17
+ import { AgentPickerComponent, AgentTranscriptViewer, formatAgentUsage, renderAgentUsage } from "./viewer.ts";
11
18
 
12
19
  const SELF_PATH = fileURLToPath(import.meta.url);
13
20
  const WIDGET_KEY = "codex-agents-tree";
@@ -209,32 +216,25 @@ export default function codexAgentsExtension(pi: ExtensionAPI): void {
209
216
  return new Text(`${header}\n${theme.fg("customMessageText", body)}`, 1, 0);
210
217
  });
211
218
 
219
+ pi.registerEntryRenderer<AgentUsageReport>(USAGE_ENTRY_TYPE, (entry, _options, theme) => {
220
+ return entry.data ? renderAgentUsage(entry.data, theme) : undefined;
221
+ });
222
+
212
223
  pi.registerCommand("agent-usage", {
213
- description: "Show main, sub-agent, and combined token usage",
224
+ description: "Show detailed main and sub-agent token usage by model",
214
225
  handler: async (_args, ctx) => {
215
226
  activeContext = ctx;
216
- let report;
227
+ let report: AgentUsageReport;
217
228
  try {
218
229
  report = control.getUsage(ctx);
219
230
  } catch (error) {
220
231
  ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
221
232
  return;
222
233
  }
223
- if (ctx.mode !== "tui") {
234
+ if (ctx.mode === "tui") {
235
+ pi.appendEntry(USAGE_ENTRY_TYPE, report);
236
+ } else {
224
237
  ctx.ui.notify(formatAgentUsage(report), "info");
225
- return;
226
- }
227
- const releaseUserOverlay = control.beginUserOverlay();
228
- try {
229
- await ctx.ui.custom<void>(
230
- (_tui, theme, keybindings, done) => new AgentUsageViewer(theme, keybindings, report, done),
231
- {
232
- overlay: true,
233
- overlayOptions: { anchor: "center", width: "62%", maxHeight: "70%", margin: 1 },
234
- },
235
- );
236
- } finally {
237
- releaseUserOverlay();
238
238
  }
239
239
  },
240
240
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youngjurry/pi-agents",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Persistent in-process Codex-style multi-agent collaboration for Pi",
5
5
  "author": "youngjurry",
6
6
  "type": "module",
package/types.ts CHANGED
@@ -6,6 +6,7 @@ export const EXTENSION_ID = "codex-agents";
6
6
  export const STATE_ENTRY_TYPE = "codex-agents-state";
7
7
  export const CHILD_META_ENTRY_TYPE = "codex-agents-child-meta";
8
8
  export const FORK_CONTEXT_ENTRY_TYPE = "codex-agents-fork-context";
9
+ export const USAGE_ENTRY_TYPE = "pi-agents-usage";
9
10
  export const ROOT_PATH = "/root";
10
11
  export const DIRECT_AGENT_TOOL_NAMES = [
11
12
  "spawn_agents",
@@ -113,10 +114,19 @@ export interface AgentUsageTotals {
113
114
  cost: number;
114
115
  }
115
116
 
117
+ export interface AgentUsageBreakdownEntry {
118
+ key: string;
119
+ usage: AgentUsageTotals;
120
+ sessionCount: number;
121
+ operations: number;
122
+ }
123
+
116
124
  export interface AgentUsageReport {
117
125
  main: AgentUsageTotals;
118
126
  subagents: AgentUsageTotals;
119
127
  combined: AgentUsageTotals;
128
+ mainBreakdown: AgentUsageBreakdownEntry[];
129
+ subagentBreakdown: AgentUsageBreakdownEntry[];
120
130
  subagentCount: number;
121
131
  unreadableSubagents: number;
122
132
  }
package/viewer.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  import {
21
21
  ROOT_PATH,
22
22
  type AgentTranscriptView,
23
+ type AgentUsageBreakdownEntry,
23
24
  type AgentUsageReport,
24
25
  type AgentUsageTotals,
25
26
  type AgentView,
@@ -88,66 +89,65 @@ function usageCacheRate(usage: AgentUsageTotals): string {
88
89
  return prompt > 0 ? `${((usage.cacheRead / prompt) * 100).toFixed(1)}%` : "n/a";
89
90
  }
90
91
 
92
+ function usageLine(label: string, usage: AgentUsageTotals): string {
93
+ const parts = [
94
+ `${label}: ${usage.total.toLocaleString()} tokens`,
95
+ `input ${usagePromptTokens(usage).toLocaleString()}`,
96
+ `output ${usage.output.toLocaleString()}`,
97
+ `cached ${usage.cacheRead.toLocaleString()} (${usageCacheRate(usage)})`,
98
+ ];
99
+ if (usage.cacheWrite > 0) parts.push(`cache write ${usage.cacheWrite.toLocaleString()}`);
100
+ if (usage.cost > 0) parts.push(`$${usage.cost.toFixed(3)}`);
101
+ return parts.join(" · ");
102
+ }
103
+
104
+ function breakdownLines(
105
+ title: string,
106
+ items: AgentUsageBreakdownEntry[],
107
+ sessionLabel: string,
108
+ ): string[] {
109
+ const lines = [title];
110
+ if (items.length === 0) {
111
+ lines.push(" (no billed usage)");
112
+ return lines;
113
+ }
114
+ for (const item of items) {
115
+ const sessions = `${item.sessionCount.toLocaleString()} ${sessionLabel}${item.sessionCount === 1 ? "" : "s"}`;
116
+ const operations = `${item.operations.toLocaleString()} usage record${item.operations === 1 ? "" : "s"}`;
117
+ lines.push(` ${item.key}`);
118
+ lines.push(` ${usageLine("Usage", item.usage)} · ${sessions} · ${operations}`);
119
+ }
120
+ return lines;
121
+ }
122
+
91
123
  export function formatAgentUsage(report: AgentUsageReport): string {
124
+ const counted = report.subagentCount - report.unreadableSubagents;
92
125
  const lines = [
93
126
  "Agent Usage",
94
127
  "",
95
- `Main agent: ${report.main.total.toLocaleString()} tokens · cache ${usageCacheRate(report.main)}`,
96
- `Sub-agents: ${report.subagents.total.toLocaleString()} tokens · cache ${usageCacheRate(report.subagents)}`,
97
- `Combined: ${report.combined.total.toLocaleString()} tokens`,
128
+ "Totals",
129
+ ` ${usageLine("Main agent", report.main)}`,
130
+ ` ${usageLine("Sub-agents", report.subagents)}`,
131
+ ` ${usageLine("Combined", report.combined)}`,
132
+ ` Sub-agent sessions: ${counted.toLocaleString()}/${report.subagentCount.toLocaleString()} readable`,
133
+ "",
134
+ ...breakdownLines("Main-agent models", report.mainBreakdown, "session"),
98
135
  "",
99
- `Sub-agents counted: ${report.subagentCount - report.unreadableSubagents}/${report.subagentCount}`,
136
+ ...breakdownLines("Sub-agent models", report.subagentBreakdown, "agent"),
100
137
  ];
101
- if (report.combined.cost > 0) lines.push(`Combined cost: $${report.combined.cost.toFixed(3)}`);
102
- if (report.unreadableSubagents > 0) lines.push(`Unreadable sessions: ${report.unreadableSubagents}`);
138
+ if (report.unreadableSubagents > 0) lines.push("", `Unreadable sub-agent sessions: ${report.unreadableSubagents.toLocaleString()}`);
103
139
  return lines.join("\n");
104
140
  }
105
141
 
106
- export class AgentUsageViewer {
107
- constructor(
108
- private readonly theme: Theme,
109
- private readonly keybindings: KeybindingsManager,
110
- private readonly report: AgentUsageReport,
111
- private readonly done: () => void,
112
- ) {}
113
-
114
- handleInput(data: string): void {
115
- if (this.keybindings.matches(data, "tui.select.cancel") || this.keybindings.matches(data, "tui.select.confirm")) {
116
- this.done();
117
- }
118
- }
119
-
120
- render(width: number): string[] {
121
- const innerWidth = Math.max(1, width - 2);
122
- const row = (label: string, usage: AgentUsageTotals, includeCache: boolean): string => {
123
- const cache = includeCache ? ` · cache ${usageCacheRate(usage)}` : "";
124
- return ` ${this.theme.fg("dim", `${label}:`)} ${usage.total.toLocaleString()} tokens${cache}`;
125
- };
126
- const lines = [
127
- framedRule(this.theme, innerWidth, "╭", "╮"),
128
- framedRow(this.theme, ` ${this.theme.fg("accent", this.theme.bold("Agent Usage"))}`, innerWidth),
129
- framedRule(this.theme, innerWidth, "├", "┤"),
130
- framedRow(this.theme, row("Main agent", this.report.main, true), innerWidth),
131
- framedRow(this.theme, row("Sub-agents", this.report.subagents, true), innerWidth),
132
- framedRow(this.theme, row("Combined", this.report.combined, false), innerWidth),
133
- framedRow(this.theme, "", innerWidth),
134
- framedRow(this.theme, ` ${this.theme.fg("dim", "Sub-agents counted:")} ${this.report.subagentCount - this.report.unreadableSubagents}/${this.report.subagentCount}`, innerWidth),
135
- ];
136
- if (this.report.combined.cost > 0) {
137
- lines.push(framedRow(this.theme, ` ${this.theme.fg("dim", "Combined cost:")} $${this.report.combined.cost.toFixed(3)}`, innerWidth));
138
- }
139
- if (this.report.unreadableSubagents > 0) {
140
- lines.push(framedRow(this.theme, ` ${this.theme.fg("warning", `Unreadable sessions: ${this.report.unreadableSubagents}`)}`, innerWidth));
141
- }
142
- lines.push(
143
- framedRule(this.theme, innerWidth, "├", "┤"),
144
- framedRow(this.theme, ` ${this.theme.fg("dim", "Enter / Esc close")}`, innerWidth),
145
- framedRule(this.theme, innerWidth, "╰", "╯"),
146
- );
147
- return lines;
148
- }
149
-
150
- invalidate(): void {}
142
+ export function renderAgentUsage(report: AgentUsageReport, theme: Theme): Text {
143
+ const headings = new Set(["Agent Usage", "Totals", "Main-agent models", "Sub-agent models"]);
144
+ const rendered = formatAgentUsage(report)
145
+ .split("\n")
146
+ .map((line) => headings.has(line)
147
+ ? (line === "Agent Usage" ? theme.bold(line) : theme.bold(theme.fg("accent", line)))
148
+ : line.startsWith("Unreadable ") ? theme.fg("warning", line) : line)
149
+ .join("\n");
150
+ return new Text(rendered, 1, 0);
151
151
  }
152
152
 
153
153
  export class AgentPickerComponent {