pi-subagents-lite 1.4.5 → 1.4.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents-lite",
3
- "version": "1.4.5",
3
+ "version": "1.4.7",
4
4
  "description": "Lightweight sub-agents for pi — spawn specialized agents with isolated sessions, tools, and models.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -8,17 +8,19 @@ import { randomUUID } from "node:crypto";
8
8
  import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
9
9
  import { runAgent } from "./agent-runner.js";
10
10
  import { AgentOutputLog } from "./output-file.js";
11
+ import { getStore } from "../shell.js";
11
12
  import {
12
13
  type AgentRecord,
13
14
  type AgentStatus,
14
15
  type CompactionInfo,
15
16
  type RunCallbacks,
17
+ type StopInitiator,
16
18
  SHORT_ID_LENGTH,
17
19
  type SpawnConfig,
18
20
  type ToolActivity,
19
21
  } from "../types.js";
20
22
  import type { SubagentType } from "./types.js";
21
- import { addUsage, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage } from "./usage.js";
23
+ import { addUsage, getLifetimeTotal, getSessionContextPercent, type AgentUsage } from "./usage.js";
22
24
  import { errorMessage } from "../utils.js";
23
25
 
24
26
  /** How often to check for expired agent records (milliseconds). */
@@ -266,7 +268,7 @@ export class AgentManager {
266
268
 
267
269
  // Wire parent abort signal to stop the subagent when the parent is interrupted
268
270
  if (options.signal) {
269
- options.signal.addEventListener("abort", () => this.abort(id), { once: true });
271
+ options.signal.addEventListener("abort", () => this.abort(id, "agent"), { once: true });
270
272
  }
271
273
 
272
274
  const promise = runAgent(ctx, type, prompt, {
@@ -373,7 +375,7 @@ export class AgentManager {
373
375
  options?: Pick<SpawnOptions, "onToolActivity" | "onAssistantUsage" | "onCompaction">,
374
376
  ): {
375
377
  onToolActivity: (activity: ToolActivity) => void;
376
- onAssistantUsage: (usage: LifetimeUsage) => void;
378
+ onAssistantUsage: (usage: AgentUsage) => void;
377
379
  onCompaction: (info: CompactionInfo) => void;
378
380
  } {
379
381
  return {
@@ -382,7 +384,17 @@ export class AgentManager {
382
384
  options?.onToolActivity?.(activity);
383
385
  },
384
386
  onAssistantUsage: (usage) => {
385
- addUsage(record.stats.lifetimeUsage, usage);
387
+ // vLLM doesn't report cache hits, so usage.input is full prompt_tokens.
388
+ // Estimate new tokens as delta from previous message's input.
389
+ const deltaEnabled = getStore().agent.deltaInputTokens;
390
+ const cacheRead = usage.cacheRead;
391
+ let inputDelta = usage.input;
392
+ if (deltaEnabled && cacheRead === 0 && record.stats.prevInputTokens != null && usage.input > record.stats.prevInputTokens) {
393
+ inputDelta = usage.input - record.stats.prevInputTokens;
394
+ }
395
+ record.stats.prevInputTokens = usage.input;
396
+
397
+ addUsage(record.stats.lifetimeUsage, { ...usage, input: inputDelta });
386
398
  options?.onAssistantUsage?.(usage);
387
399
  },
388
400
  onCompaction: (info) => {
@@ -454,18 +466,18 @@ export class AgentManager {
454
466
  );
455
467
  }
456
468
 
457
- abort(id: string): boolean {
469
+ abort(id: string, stoppedBy?: StopInitiator): boolean {
458
470
  const record = this.agents.get(id);
459
471
  if (!record) return false;
460
472
 
461
- return this.stopAgent(record);
473
+ return this.stopAgent(record, stoppedBy);
462
474
  }
463
475
 
464
476
  /**
465
477
  * Stop an agent by aborting its session or removing it from the queue.
466
478
  * Returns true if the agent was stopped, false if it wasn't running/queued.
467
479
  */
468
- private stopAgent(record: AgentRecord): boolean {
480
+ private stopAgent(record: AgentRecord, stoppedBy?: StopInitiator): boolean {
469
481
  if (record.lifecycle.status === "queued") {
470
482
  this.queue = this.queue.filter(q => q.id !== record.id);
471
483
  } else if (record.lifecycle.status !== "running") {
@@ -474,6 +486,7 @@ export class AgentManager {
474
486
  record.execution.abortController?.abort();
475
487
  }
476
488
  record.lifecycle.status = "stopped";
489
+ record.lifecycle.stoppedBy = stoppedBy;
477
490
  record.lifecycle.completedAt = Date.now();
478
491
  return true;
479
492
  }
@@ -20,7 +20,7 @@ import {
20
20
  } from "@earendil-works/pi-coding-agent";
21
21
  import { getAgentConfig, getConfig, getToolNamesForType, resolveVisibleTools } from "./agent-types.js";
22
22
  import { extractText } from "../prompt/context.js";
23
- import type { LifetimeUsage } from "./usage.js";
23
+ import type { AgentUsage } from "./usage.js";
24
24
  import { findModelInRegistry, GIT_EXEC_TIMEOUT_MS } from "../utils.js";
25
25
  import { DEFAULT_AGENTS } from "./default-agents.js";
26
26
  import { buildAgentPrompt, type PromptExtras } from "../prompt/prompts.js";
@@ -106,13 +106,14 @@ function forwardAbortSignal(session: AgentSession, signal?: AbortSignal): () =>
106
106
  * assistant messages at runtime, but this shape isn't reflected in the
107
107
  * AgentSessionEvent public types.
108
108
  */
109
- function usageFromAssistantMessage(msg: Record<string, unknown>): LifetimeUsage | undefined {
109
+ function usageFromAssistantMessage(msg: Record<string, unknown>): AgentUsage | undefined {
110
110
  const usage = msg.usage as Record<string, unknown> | undefined;
111
111
  if (!usage) return undefined;
112
112
  return {
113
113
  input: (usage.input as number) ?? 0,
114
114
  output: (usage.output as number) ?? 0,
115
115
  cacheWrite: (usage.cacheWrite as number) ?? 0,
116
+ cacheRead: (usage.cacheRead as number) ?? 0,
116
117
  cost: ((usage.cost as Record<string, unknown>)?.total as number) ?? 0,
117
118
  };
118
119
  }
@@ -88,6 +88,17 @@ export function buildAgentDetails(
88
88
  return details;
89
89
  }
90
90
 
91
+ /**
92
+ * Result text plus status note, for display.
93
+ *
94
+ * Shared by the foreground tool result and the subagent-result nudge so both
95
+ * callers stay in sync on the nullish default and separator handling — they
96
+ * have diverged before. getStatusNote owns the leading separator.
97
+ */
98
+ export function formatResultContent(record: AgentRecord): string {
99
+ return (record.result ?? "") + getStatusNote(record.lifecycle);
100
+ }
101
+
91
102
  // ============================================================================
92
103
  // Tool execute handlers
93
104
  // ============================================================================
@@ -186,8 +197,7 @@ export async function executeAgentTool(
186
197
  return errorResult(`Agent failed: ${record.error || "unknown error"}`, details);
187
198
  }
188
199
 
189
- const statusNote = getStatusNote(record.lifecycle.status);
190
- return successResult((record.result ?? "") + statusNote, details);
200
+ return successResult(formatResultContent(record), details);
191
201
  }
192
202
 
193
203
  // ============================================================================
@@ -244,7 +254,7 @@ export async function executeStopAgentTool(
244
254
  }
245
255
 
246
256
  // Attempt to stop the running/queued agent
247
- if (getManager()!.abort(agentId)) {
257
+ if (getManager()!.abort(agentId, "agent")) {
248
258
  return successResult(`Stopped agent ${agentId.slice(0, SHORT_ID_LENGTH)}`);
249
259
  }
250
260
 
@@ -9,6 +9,13 @@
9
9
  */
10
10
  export type LifetimeUsage = { input: number; output: number; cacheWrite: number; cost: number };
11
11
 
12
+ /**
13
+ * A single per-turn usage event as emitted upstream. Adds `cacheRead`, which
14
+ * LifetimeUsage omits from totals (see issue #38). Used to estimate input
15
+ * deltas for providers like vLLM that don't report cache hits.
16
+ */
17
+ export type AgentUsage = LifetimeUsage & { cacheRead: number };
18
+
12
19
  /** Sum of lifetime usage components (including cost), or 0 if undefined. */
13
20
  export function getLifetimeTotal(u?: LifetimeUsage): number {
14
21
  return u ? u.input + u.output + u.cacheWrite + u.cost : 0;
@@ -42,6 +42,7 @@ const DEFAULT_AGENT: SubagentsConfig["agent"] = {
42
42
  showContext: true,
43
43
  showCost: false,
44
44
  showTime: true,
45
+ deltaInputTokens: false,
45
46
  };
46
47
 
47
48
  /**
@@ -74,6 +74,8 @@ export interface ResolvedAgentSettings {
74
74
  readonly showContext: boolean;
75
75
  /** Whether to show elapsed time in widget stats line. */
76
76
  readonly showTime: boolean;
77
+ /** Whether to estimate input token delta for vLLM (no cache reporting). */
78
+ readonly deltaInputTokens: boolean;
77
79
  /** Buffer size for streaming thinking blocks to output file. 0 = disabled. */
78
80
  readonly outputThinkingBufferSize: number;
79
81
  }
@@ -133,6 +135,7 @@ export class ConfigStore {
133
135
  showOutput: a.showOutput !== false,
134
136
  showContext: a.showContext !== false,
135
137
  showTime: a.showTime !== false,
138
+ deltaInputTokens: a.deltaInputTokens !== false,
136
139
  outputThinkingBufferSize: a.outputThinkingBufferSize ?? 0,
137
140
  };
138
141
  }
@@ -265,6 +268,10 @@ export class ConfigStore {
265
268
  setShowOutput: (enabled: boolean) => this.setAgentVisibility("showOutput", enabled),
266
269
  setShowContext: (enabled: boolean) => this.setAgentVisibility("showContext", enabled),
267
270
  setShowTime: (enabled: boolean) => this.setAgentVisibility("showTime", enabled),
271
+ setDeltaInputTokens: (enabled: boolean): void => {
272
+ this.config.agent.deltaInputTokens = enabled;
273
+ this.persist();
274
+ },
268
275
  setOutputThinkingBufferSize: (size: number): void => {
269
276
  this.config.agent.outputThinkingBufferSize = size;
270
277
  this.persist();
@@ -10,6 +10,7 @@ export const CONFIG_AGENT_NON_MODEL_KEYS = [
10
10
  "showOutput",
11
11
  "showContext",
12
12
  "showTime",
13
+ "deltaInputTokens",
13
14
  "widgetMaxLines",
14
15
  "widgetMaxLinesCompact",
15
16
  "widgetDescLengthFull",
@@ -1,5 +1,11 @@
1
- import { getStatusNote } from "../status-note.js";
2
1
  import { getPiInstance, getSessionCtx, getWidget } from "../shell.js";
2
+ import { SHORT_ID_LENGTH } from "../types.js";
3
+
4
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
+ import type { AgentRecord, SpawnConfig, ToolActivity } from "../types.js";
6
+ import type { AgentManager, SpawnOptions } from "../agents/agent-manager.js";
7
+ import { buildAgentDetails, formatResultContent } from "../agents/tool-execution.js";
8
+
3
9
  /**
4
10
  * spawn-coordinator.ts — Spawn-and-track coordination for subagents.
5
11
  *
@@ -11,11 +17,6 @@ import { getPiInstance, getSessionCtx, getWidget } from "../shell.js";
11
17
  * D6 (Nudge owned here), D2 (peers with AgentManager).
12
18
  */
13
19
 
14
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
15
- import type { AgentRecord, SpawnConfig, ToolActivity } from "../types.js";
16
- import type { AgentManager, SpawnOptions } from "../agents/agent-manager.js";
17
- import { buildAgentDetails } from "../agents/tool-execution.js";
18
-
19
20
  // ============================================================================
20
21
  // Types
21
22
  // ============================================================================
@@ -234,7 +235,7 @@ export class SpawnCoordinator {
234
235
  pi.sendMessage(
235
236
  {
236
237
  customType: "subagent-result",
237
- content: `[Subagent "${record.display.type}" ${record.lifecycle.status}]\n\n${record.result ?? ""} ${getStatusNote(record.lifecycle.status)}`,
238
+ content: `[Subagent "${record.display.type}" ${record.id.slice(0, SHORT_ID_LENGTH)} ${record.lifecycle.status}]\n\n${formatResultContent(record)}`,
238
239
  details,
239
240
  display: true,
240
241
  },
@@ -1,10 +1,20 @@
1
- const NOTES: Record<string, string> = {
2
- stopped: "STOPPED BY THE USER before completion — output is partial; the task was NOT finished",
1
+ import type { AgentLifecycle, AgentStatus, StopInitiator } from "./types.js";
2
+
3
+ const STATUS_NOTES: Partial<Record<AgentStatus, string>> = {
3
4
  aborted: "hit the turn limit before completion; output may be incomplete",
4
5
  turn_limited: "wrapped up at the turn limit — output may be partial",
5
6
  };
6
7
 
7
- export function getStatusNote(status: string): string {
8
- const note = NOTES[status];
8
+ const STOP_NOTES: Record<StopInitiator, string> = {
9
+ user: "STOPPED BY THE USER before completion — output is partial; the task was NOT finished",
10
+ agent: "stopped before completion — output is partial; the task was NOT finished",
11
+ };
12
+
13
+ export function getStatusNote(lifecycle: AgentLifecycle): string {
14
+ const note =
15
+ lifecycle.status === "stopped"
16
+ // A stopped agent with no recorded initiator reads as an agent stop.
17
+ ? STOP_NOTES[lifecycle.stoppedBy ?? "agent"]
18
+ : STATUS_NOTES[lifecycle.status];
9
19
  return note ? ` (${note})` : "";
10
20
  }
package/src/types.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  import type { Model } from "@earendil-works/pi-ai";
6
6
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
7
7
  import type { AgentOutputLog } from "./agents/output-file.js";
8
- import type { LifetimeUsage } from "./agents/usage.js";
8
+ import type { LifetimeUsage, AgentUsage } from "./agents/usage.js";
9
9
  import type { SubagentType, AgentInvocation } from "./agents/types.js";
10
10
 
11
11
  /** Thinking level for agent models. */
@@ -59,7 +59,7 @@ export interface RunCallbacks {
59
59
  onTextDelta?: (delta: string, fullText: string) => void;
60
60
  onSessionCreated?: (session: AgentSession) => void;
61
61
  onTurnEnd?: (turnCount: number) => void;
62
- onAssistantUsage?: (usage: LifetimeUsage) => void;
62
+ onAssistantUsage?: (usage: AgentUsage) => void;
63
63
  onCompaction?: (info: CompactionInfo) => void;
64
64
  }
65
65
 
@@ -95,6 +95,9 @@ export interface CompactionInfo {
95
95
  /** Possible agent lifecycle statuses. */
96
96
  export type AgentStatus = "queued" | "running" | "completed" | "turn_limited" | "aborted" | "stopped" | "error";
97
97
 
98
+ /** Who initiated an agent stop: "user" via UI menu, or "agent" via StopAgent tool. */
99
+ export type StopInitiator = "user" | "agent";
100
+
98
101
  /**
99
102
  * Lifecycle state: when the agent started, completed, and its current status.
100
103
  * Used by agent-manager (lifecycle control), menus (status display), widget (linger logic).
@@ -103,6 +106,7 @@ export interface AgentLifecycle {
103
106
  status: AgentStatus;
104
107
  startedAt: number;
105
108
  completedAt?: number;
109
+ stoppedBy?: StopInitiator;
106
110
  }
107
111
 
108
112
  /**
@@ -156,6 +160,8 @@ export interface AgentAccumulatedStats {
156
160
  maxTurns?: number;
157
161
  /** Number of times this agent's session has compacted. Initialized to 0 at spawn. */
158
162
  compactionCount: number;
163
+ /** Previous input token count for delta estimation (vLLM doesn't report cache hits). */
164
+ prevInputTokens?: number;
159
165
  /** Last-known context usage percentage (0–100), captured at completion. */
160
166
  contextPercent?: number | null;
161
167
  }
@@ -134,7 +134,7 @@ export function buildAgentActionsList(
134
134
  input.onEscape = () => setActive(list);
135
135
  setActive(input);
136
136
  } else if (item.value === "stop") {
137
- getManager()?.abort(record.id);
137
+ getManager()?.abort(record.id, "user");
138
138
  ctx.ui.notify(`Stopped ${shortId}`, "info");
139
139
  onClose();
140
140
  }
@@ -186,7 +186,7 @@ export async function showRunningAgentsMenu(
186
186
  agentList.onSelect = async (item) => {
187
187
  if (item.value === "__stop-all") {
188
188
  for (const r of running) {
189
- getManager()?.abort(r.id);
189
+ getManager()?.abort(r.id, "user");
190
190
  }
191
191
  ctx.ui.notify(`Stopped ${running.length} agent(s)`, "info");
192
192
  done(undefined);
@@ -26,6 +26,7 @@ function buildStatConfig(store: ReturnType<typeof getStore>) {
26
26
  ["showTools", { label: "Show tools", get: () => store.agent.showTools, set: (v) => store.mutate.agent.setShowTools(v) }],
27
27
  ["showTurns", { label: "Show turns", get: () => store.agent.showTurns, set: (v) => store.mutate.agent.setShowTurns(v) }],
28
28
  ["showInput", { label: "Show input tokens", get: () => store.agent.showInput, set: (v) => store.mutate.agent.setShowInput(v) }],
29
+ ["deltaInputTokens", { label: "Delta input tokens", get: () => store.agent.deltaInputTokens, set: (v) => store.mutate.agent.setDeltaInputTokens(v) }],
29
30
  ["showOutput", { label: "Show output tokens", get: () => store.agent.showOutput, set: (v) => store.mutate.agent.setShowOutput(v) }],
30
31
  ["showContext", { label: "Show context %", get: () => store.agent.showContext, set: (v) => store.mutate.agent.setShowContext(v) }],
31
32
  ["showCost", { label: "Show cost", get: () => store.agent.showCost, set: (v) => store.mutate.agent.setShowCost(v) }],
@@ -66,6 +67,7 @@ export async function showWidgetSettingsMenu(ctx: ExtensionCommandContext): Prom
66
67
  showTools: "Show tool count (🛠 ) in the widget.",
67
68
  showTurns: "Show turn count (⟳ ) in the widget.",
68
69
  showInput: "Show input tokens (↑) in the widget.",
70
+ deltaInputTokens: "Estimate input token delta for vLLM (no cache reporting).",
69
71
  showOutput: "Show output tokens (↓) in the widget.",
70
72
  showContext: "Show context-fill percent (%) in the widget.",
71
73
  showCost: "Show dollar cost ($) in the widget.",