oira666_pi-subagent 0.2.28 → 0.2.30

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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: team-lead
3
- description: Focused on tasks managment, delegates work to its subagents. For more compex tasks.
3
+ description: "A team of agents with different specializations that can take any complex task, split it into parts, and implement architecture, generation, review, or any other kind of task."
4
4
  ---
5
5
 
6
6
  You are an experienced team lead, focused on tasks management. You don't do any work yourself. You delegate.
package/index.ts CHANGED
@@ -42,6 +42,7 @@ import {
42
42
  type DelegationMode,
43
43
  type SingleResult,
44
44
  type SubagentDetails,
45
+ type SubagentUsageSummary,
45
46
  DEFAULT_DELEGATION_MODE,
46
47
  buildLiveSubagentDetails,
47
48
  buildSubagentDetails,
@@ -54,6 +55,7 @@ import {
54
55
  emptyUsageSummary,
55
56
  addUsageSummary,
56
57
  usageSummaryToUsageStats,
58
+ usageSummaryFromUsage,
57
59
  } from "./types.js";
58
60
  import { formatCombinedUsageStatusLine } from "./tree.js";
59
61
 
@@ -314,11 +316,60 @@ function formatAgentNames(agents: AgentConfig[]): string {
314
316
  return agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
315
317
  }
316
318
 
317
- function collectCombinedUsageStatusLine(ctx: any): string | undefined {
319
+ function liveDetailsSignature(details: SubagentDetails): string {
320
+ return JSON.stringify(details.results.map((result) => ({
321
+ agent: result.agent,
322
+ task: result.task ?? "",
323
+ })));
324
+ }
325
+
326
+ function collectLiveUsageSummary(details: SubagentDetails): SubagentUsageSummary {
327
+ const summary = emptyUsageSummary();
328
+ for (const result of details.results) {
329
+ addUsageSummary(summary, usageSummaryFromUsage(result.usage));
330
+
331
+ const completedNested = getNestedSubagentResults(result.messages ?? []);
332
+ const completedNestedIds = new Set<string>();
333
+ const completedNestedSignatureCounts = new Map<string, number>();
334
+ for (const nested of completedNested) {
335
+ if (nested.toolCallId) completedNestedIds.add(nested.toolCallId);
336
+ const signature = liveDetailsSignature(nested.details);
337
+ completedNestedSignatureCounts.set(
338
+ signature,
339
+ (completedNestedSignatureCounts.get(signature) ?? 0) + 1,
340
+ );
341
+ addUsageSummary(
342
+ summary,
343
+ nested.details.usageSummary ?? collectLiveUsageSummary(nested.details),
344
+ );
345
+ }
346
+
347
+ for (const [liveToolCallId, liveNested] of Object.entries(result.liveNestedSubagents ?? {})) {
348
+ if (!isSubagentDetails(liveNested)) continue;
349
+ // Prefer durable completed toolResult messages over matching live progress.
350
+ // Some Pi versions key live progress differently than final tool results,
351
+ // so also de-dupe by requested child agent/task signature as a multiset.
352
+ if (completedNestedIds.has(liveToolCallId)) continue;
353
+ const signature = liveDetailsSignature(liveNested);
354
+ const completedSignatureCount = completedNestedSignatureCounts.get(signature) ?? 0;
355
+ if (completedSignatureCount > 0) {
356
+ completedNestedSignatureCounts.set(signature, completedSignatureCount - 1);
357
+ continue;
358
+ }
359
+ addUsageSummary(summary, collectLiveUsageSummary(liveNested));
360
+ }
361
+ }
362
+ return summary;
363
+ }
364
+
365
+ function collectCombinedUsageStatusLine(
366
+ ctx: any,
367
+ liveSummaries: SubagentUsageSummary[] = [],
368
+ ): string | undefined {
318
369
  const entries = typeof ctx?.sessionManager?.getEntries === "function" || typeof ctx?.sessionManager?.getBranch === "function"
319
370
  ? branchEntries(ctx)
320
371
  : [];
321
- if (entries.length === 0) return undefined;
372
+ if (entries.length === 0 && liveSummaries.length === 0) return undefined;
322
373
 
323
374
  const parentUsage = emptyUsage();
324
375
  const subagents = emptyUsageSummary();
@@ -333,14 +384,20 @@ function collectCombinedUsageStatusLine(ctx: any): string | undefined {
333
384
  parentUsage.output += usage.output || 0;
334
385
  parentUsage.cacheRead += usage.cacheRead || 0;
335
386
  parentUsage.cacheWrite += usage.cacheWrite || 0;
336
- parentUsage.cost += usage.cost?.total || 0;
387
+ parentUsage.cost += typeof usage.cost === "number" ? usage.cost : usage.cost?.total || 0;
337
388
  parentUsage.turns += 1;
338
389
  }
339
390
  }
340
391
  if (msg.role === "toolResult" && msg.toolName === "subagent" && isSubagentDetails(msg.details)) {
341
- addUsageSummary(subagents, msg.details.usageSummary ?? emptyUsageSummary());
392
+ addUsageSummary(
393
+ subagents,
394
+ msg.details.usageSummary ?? usageSummaryFromUsage(msg.details.aggregatedUsage),
395
+ );
342
396
  }
343
397
  }
398
+ for (const liveSummary of liveSummaries) {
399
+ addUsageSummary(subagents, liveSummary);
400
+ }
344
401
  const subagentUsage = usageSummaryToUsageStats(subagents) ?? emptyUsage();
345
402
  addUsage(parentUsage, subagentUsage);
346
403
  return formatCombinedUsageStatusLine(parentUsage, subagents.subagentCount);
@@ -687,6 +744,7 @@ export default function (pi: ExtensionAPI) {
687
744
  let modelToRestoreAfterResume: any | undefined;
688
745
  const approvedProjectAgentDirsForSession = new Set<string>();
689
746
  const activeSubagents = new Map<number, { agent: string; task: string; handle: RunningSubagentHandle }>();
747
+ const activeSubagentUsageSummaries = new Map<string, SubagentUsageSummary>();
690
748
  const latestBroadcastTargets = {
691
749
  all: [] as BroadcastTarget[],
692
750
  youngest: [] as BroadcastTarget[],
@@ -858,9 +916,10 @@ export default function (pi: ExtensionAPI) {
858
916
  }
859
917
 
860
918
  function extractPendingSubagentTaskCounts(result: SingleResult): number[] {
861
- const completedToolCallIds = new Set(getNestedSubagentResults(result.messages).map((nested) => nested.toolCallId));
919
+ const messages = Array.isArray((result as any).messages) ? (result as any).messages : [];
920
+ const completedToolCallIds = new Set(getNestedSubagentResults(messages).map((nested) => nested.toolCallId));
862
921
  const counts: number[] = [];
863
- for (const message of result.messages as any[]) {
922
+ for (const message of messages) {
864
923
  if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
865
924
  for (const part of message.content) {
866
925
  if (part?.type !== "toolCall" || part?.name !== "subagent") continue;
@@ -1049,23 +1108,36 @@ export default function (pi: ExtensionAPI) {
1049
1108
  }
1050
1109
  }
1051
1110
 
1052
- // If the host exposes a status-line extension point, add a second line under
1053
- // the normal session stats with parent + active-branch subagent totals. Older
1054
- // Pi versions simply won't have this API, so keep this best-effort.
1055
- try {
1056
- const statusProvider = (ctx: any) => collectCombinedUsageStatusLine(ctx ?? latestSessionCtx);
1057
- if (typeof pi.registerStatusLine === "function") {
1058
- pi.registerStatusLine({ id: "subagent-combined-usage", placement: "after-session-stats", render: statusProvider });
1059
- } else if (typeof pi.addStatusLine === "function") {
1060
- pi.addStatusLine(statusProvider, { id: "subagent-combined-usage", placement: "after-session-stats" });
1111
+ // Pi exposes footer/status-line extension text through ctx.ui.setStatus().
1112
+ // Keep a dedicated line updated with parent + active-branch subagent totals.
1113
+ // This is shown by the normal footer underneath Pi's built-in session stats.
1114
+ function formatFooterStatusText(ctx: any, text: string): string {
1115
+ return typeof ctx?.ui?.theme?.fg === "function"
1116
+ ? ctx.ui.theme.fg("dim", text)
1117
+ : text;
1118
+ }
1119
+
1120
+ function updateCombinedUsageStatus(ctx?: any): void {
1121
+ const targetCtx = ctx ?? latestSessionCtx;
1122
+ if (!targetCtx?.ui || typeof targetCtx.ui.setStatus !== "function") return;
1123
+ try {
1124
+ const line = collectCombinedUsageStatusLine(
1125
+ targetCtx,
1126
+ Array.from(activeSubagentUsageSummaries.values()),
1127
+ );
1128
+ targetCtx.ui.setStatus(
1129
+ "subagent-usage",
1130
+ line ? formatFooterStatusText(targetCtx, `total ${line}`) : undefined,
1131
+ );
1132
+ } catch (err) {
1133
+ console.error("[pi-subagent] Failed to update combined subagent status line:", err);
1061
1134
  }
1062
- } catch (err) {
1063
- console.error("[pi-subagent] Failed to register combined subagent status line:", err);
1064
1135
  }
1065
1136
 
1066
1137
  // Auto-discover agents on session start
1067
1138
  pi.on("session_start", async (event, ctx) => {
1068
1139
  latestSessionCtx = ctx;
1140
+ updateCombinedUsageStatus(ctx);
1069
1141
  try {
1070
1142
  // Always repair sessions left on the synthetic resume model, even in
1071
1143
  // nested subagents that can no longer delegate. Those leaf processes
@@ -1175,10 +1247,26 @@ export default function (pi: ExtensionAPI) {
1175
1247
  }
1176
1248
  });
1177
1249
 
1178
- pi.on("agent_end", async () => {
1250
+ pi.on("agent_end", async (_event, ctx) => {
1251
+ updateCombinedUsageStatus(ctx);
1179
1252
  await restoreModelAfterResumeFailure();
1180
1253
  });
1181
1254
 
1255
+ pi.on("message_end", (_event, ctx) => {
1256
+ latestSessionCtx = ctx;
1257
+ updateCombinedUsageStatus(ctx);
1258
+ setTimeout(() => updateCombinedUsageStatus(ctx), 0);
1259
+ });
1260
+
1261
+ pi.on("tool_execution_end", (event, ctx) => {
1262
+ latestSessionCtx = ctx;
1263
+ if (event.toolName === "subagent") {
1264
+ activeSubagentUsageSummaries.delete(event.toolCallId);
1265
+ updateCombinedUsageStatus(ctx);
1266
+ setTimeout(() => updateCombinedUsageStatus(ctx), 0);
1267
+ }
1268
+ });
1269
+
1182
1270
  pi.on("resources_discover", (_event, ctx) => {
1183
1271
  const prompt = pendingInteractiveResumePrompt;
1184
1272
  if (!prompt) return;
@@ -1308,7 +1396,9 @@ calls one after another. Do NOT put dependent tasks in the same array.
1308
1396
  nextActiveSubagentId += tasks.length;
1309
1397
  const trackedOnUpdate = (partial: any) => {
1310
1398
  if (isSubagentDetails(partial?.details)) {
1399
+ activeSubagentUsageSummaries.set(toolCallId, collectLiveUsageSummary(partial.details));
1311
1400
  updateLatestBroadcastTargets(partial.details, topLevelBaseId);
1401
+ updateCombinedUsageStatus(ctx);
1312
1402
  emitNestedProgressToParent(toolCallId, partial.details);
1313
1403
  }
1314
1404
  onUpdate?.(partial);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-subagent",
3
- "version": "0.2.28",
3
+ "version": "0.2.30",
4
4
  "description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/tree.ts CHANGED
@@ -76,8 +76,14 @@ export function formatCombinedUsageStatusLine(
76
76
  const input = usage.input ?? 0;
77
77
  const output = usage.output ?? 0;
78
78
  const cacheRead = usage.cacheRead ?? 0;
79
+ const cacheWrite = usage.cacheWrite ?? 0;
80
+ const turns = usage.turns ?? 0;
79
81
  const cost = usage.cost ?? 0;
80
- return `↑${formatTokens(input)} ↓${formatTokens(output)} R${formatTokens(cacheRead)} $${cost.toFixed(3)} (with ${subagentCount} subagents)`;
82
+ const total = input + output + cacheRead + cacheWrite;
83
+ const cache = cacheRead || cacheWrite
84
+ ? ` R${formatTokens(cacheRead)} W${formatTokens(cacheWrite)}`
85
+ : "";
86
+ return `Σ ↑${formatTokens(input)} ↓${formatTokens(output)}${cache} T${formatTokens(total)} • ${turns} turn${turns === 1 ? "" : "s"} • $${cost.toFixed(4)} • ${subagentCount} subagent${subagentCount === 1 ? "" : "s"}`;
81
87
  }
82
88
 
83
89
  export function formatUsage(usage: Partial<UsageStats>, model?: string): string {
package/types.ts CHANGED
@@ -173,12 +173,16 @@ export function mergeToolCalls(target: ToolCallCounts, source: ToolCallCounts):
173
173
  }
174
174
  }
175
175
 
176
+ function asMessageArray(messages: unknown): Message[] {
177
+ return Array.isArray(messages) ? (messages as Message[]) : [];
178
+ }
179
+
176
180
  /** Extract all tool calls made by assistant turns in a message list */
177
- export function extractToolCalls(messages: Message[]): ToolCallCounts {
181
+ export function extractToolCalls(messages: unknown): ToolCallCounts {
178
182
  const counts: ToolCallCounts = {};
179
- for (const msg of messages) {
183
+ for (const msg of asMessageArray(messages)) {
180
184
  if (msg.role !== "assistant") continue;
181
- for (const part of (msg.content as any[]) ?? []) {
185
+ for (const part of (Array.isArray((msg as any).content) ? (msg as any).content : [])) {
182
186
  if ((part as any)?.type !== "toolCall") continue;
183
187
  const name: string = typeof (part as any).name === "string" ? (part as any).name : "unknown";
184
188
  counts[name] = (counts[name] ?? 0) + 1;
@@ -393,13 +397,14 @@ export function isSubagentDetails(value: unknown): value is SubagentDetails {
393
397
  }
394
398
 
395
399
  /** Extract the last assistant text from a message history. */
396
- export function getFinalOutput(messages: Message[], fallback?: string): string {
397
- for (let i = messages.length - 1; i >= 0; i--) {
398
- const msg = messages[i];
399
- if (msg.role === "assistant") {
400
- for (let j = msg.content.length - 1; j >= 0; j--) {
401
- const part = msg.content[j];
402
- if (part.type === "text") return part.text;
400
+ export function getFinalOutput(messages: unknown, fallback?: string): string {
401
+ const history = asMessageArray(messages);
402
+ for (let i = history.length - 1; i >= 0; i--) {
403
+ const msg = history[i];
404
+ if (msg.role === "assistant" && Array.isArray((msg as any).content)) {
405
+ for (let j = (msg as any).content.length - 1; j >= 0; j--) {
406
+ const part = (msg as any).content[j];
407
+ if (part?.type === "text") return part.text;
403
408
  }
404
409
  }
405
410
  }
@@ -407,14 +412,14 @@ export function getFinalOutput(messages: Message[], fallback?: string): string {
407
412
  }
408
413
 
409
414
  /** Extract all display-worthy items from a message history. */
410
- export function getDisplayItems(messages: Message[]): DisplayItem[] {
415
+ export function getDisplayItems(messages: unknown): DisplayItem[] {
411
416
  const items: DisplayItem[] = [];
412
- for (const msg of messages) {
413
- if (msg.role === "assistant") {
414
- for (const part of msg.content) {
415
- if (part.type === "text") {
417
+ for (const msg of asMessageArray(messages)) {
418
+ if (msg.role === "assistant" && Array.isArray((msg as any).content)) {
419
+ for (const part of (msg as any).content) {
420
+ if (part?.type === "text") {
416
421
  items.push({ type: "text", text: part.text });
417
- } else if (part.type === "toolCall") {
422
+ } else if (part?.type === "toolCall") {
418
423
  items.push({ type: "toolCall", name: part.name, args: part.arguments });
419
424
  }
420
425
  }
@@ -424,16 +429,16 @@ export function getDisplayItems(messages: Message[]): DisplayItem[] {
424
429
  }
425
430
 
426
431
  /** Extract nested subagent tool results from a message history. */
427
- export function getNestedSubagentResults(messages: Message[]): NestedSubagentResult[] {
432
+ export function getNestedSubagentResults(messages: unknown): NestedSubagentResult[] {
428
433
  const results: NestedSubagentResult[] = [];
429
- for (const msg of messages) {
430
- if (msg.role !== "toolResult") continue;
431
- if (msg.toolName !== "subagent") continue;
432
- if (!isSubagentDetails(msg.details)) continue;
434
+ for (const msg of asMessageArray(messages)) {
435
+ if ((msg as any)?.role !== "toolResult") continue;
436
+ if ((msg as any).toolName !== "subagent") continue;
437
+ if (!isSubagentDetails((msg as any).details)) continue;
433
438
  results.push({
434
- details: msg.details,
435
- isError: msg.isError,
436
- toolCallId: msg.toolCallId,
439
+ details: (msg as any).details,
440
+ isError: Boolean((msg as any).isError),
441
+ toolCallId: typeof (msg as any).toolCallId === "string" ? (msg as any).toolCallId : "",
437
442
  });
438
443
  }
439
444
  return results;