pi-subagents-lite 1.4.4 → 1.4.6

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.4",
3
+ "version": "1.4.6",
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,6 +8,7 @@ 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,
@@ -18,7 +19,7 @@ import {
18
19
  type ToolActivity,
19
20
  } from "../types.js";
20
21
  import type { SubagentType } from "./types.js";
21
- import { addUsage, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage } from "./usage.js";
22
+ import { addUsage, getLifetimeTotal, getSessionContextPercent, type AgentUsage } from "./usage.js";
22
23
  import { errorMessage } from "../utils.js";
23
24
 
24
25
  /** How often to check for expired agent records (milliseconds). */
@@ -373,7 +374,7 @@ export class AgentManager {
373
374
  options?: Pick<SpawnOptions, "onToolActivity" | "onAssistantUsage" | "onCompaction">,
374
375
  ): {
375
376
  onToolActivity: (activity: ToolActivity) => void;
376
- onAssistantUsage: (usage: LifetimeUsage) => void;
377
+ onAssistantUsage: (usage: AgentUsage) => void;
377
378
  onCompaction: (info: CompactionInfo) => void;
378
379
  } {
379
380
  return {
@@ -382,7 +383,17 @@ export class AgentManager {
382
383
  options?.onToolActivity?.(activity);
383
384
  },
384
385
  onAssistantUsage: (usage) => {
385
- addUsage(record.stats.lifetimeUsage, usage);
386
+ // vLLM doesn't report cache hits, so usage.input is full prompt_tokens.
387
+ // Estimate new tokens as delta from previous message's input.
388
+ const deltaEnabled = getStore().agent.deltaInputTokens;
389
+ const cacheRead = usage.cacheRead;
390
+ let inputDelta = usage.input;
391
+ if (deltaEnabled && cacheRead === 0 && record.stats.prevInputTokens != null && usage.input > record.stats.prevInputTokens) {
392
+ inputDelta = usage.input - record.stats.prevInputTokens;
393
+ }
394
+ record.stats.prevInputTokens = usage.input;
395
+
396
+ addUsage(record.stats.lifetimeUsage, { ...usage, input: inputDelta });
386
397
  options?.onAssistantUsage?.(usage);
387
398
  },
388
399
  onCompaction: (info) => {
@@ -20,14 +20,14 @@ 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";
27
27
  import { preloadSkills, loadSkillMeta, type SkillMeta } from "../prompt/skill-loader.js";
28
28
  import { type EnvInfo, type RunCallbacks, type RunTunables, SHORT_ID_LENGTH } from "../types.js";
29
29
  import type { SubagentType, SystemPromptMode } from "./types.js";
30
- import { getStore } from "../shell.js";
30
+ import { getStore, enterSubagentSpawn, exitSubagentSpawn } from "../shell.js";
31
31
  import { DEFAULT_GRACE_TURNS, CUSTOM_PROMPT_PATH } from "../config/config-io.js";
32
32
 
33
33
  /** Normalize max turns. undefined or 0 = unlimited, otherwise minimum 1. */
@@ -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
  }
@@ -516,6 +517,22 @@ export async function runAgent(
516
517
  type: SubagentType,
517
518
  prompt: string,
518
519
  options: RunOptions,
520
+ ): Promise<RunResult> {
521
+ // Bracket the whole subagent lifecycle so the extension factory can detect
522
+ // it's being re-loaded inside a subagent and avoid clobbering the parent shell.
523
+ enterSubagentSpawn();
524
+ try {
525
+ return await runAgentImpl(ctx, type, prompt, options);
526
+ } finally {
527
+ exitSubagentSpawn();
528
+ }
529
+ }
530
+
531
+ async function runAgentImpl(
532
+ ctx: ExtensionContext,
533
+ type: SubagentType,
534
+ prompt: string,
535
+ options: RunOptions,
519
536
  ): Promise<RunResult> {
520
537
  const store = getStore();
521
538
  const config = getConfig(type, store.agent.loadSkillsImplicitly, store.agent.loadExtensionsImplicitly);
@@ -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",
package/src/index.ts CHANGED
@@ -24,11 +24,15 @@
24
24
  */
25
25
 
26
26
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
27
- import { setPiInstance } from "./shell.js";
27
+ import { setPiInstance, isInsideSubagentSpawn } from "./shell.js";
28
28
  import { registerTools } from "./registration.js";
29
29
  import { setupEventListeners } from "./events.js";
30
30
 
31
31
  export default function (pi: ExtensionAPI) {
32
+ // Subagents re-load this extension under their own pi/runtime. Stay inert so
33
+ // we never overwrite the parent-owned shell (pi, sessionCtx, manager, ...).
34
+ // The completion nudge relies on those still pointing at the parent session.
35
+ if (isInsideSubagentSpawn()) return;
32
36
  setPiInstance(pi);
33
37
  registerTools(pi);
34
38
  setupEventListeners(pi);
package/src/shell.ts CHANGED
@@ -99,3 +99,31 @@ export function setWidget(w: AgentWidget | null): void {
99
99
  export function setCoordinator(c: SpawnCoordinator | null): void {
100
100
  shell.coordinator = c;
101
101
  }
102
+
103
+ // ============================================================================
104
+ // Subagent spawn context
105
+ // ============================================================================
106
+
107
+ /**
108
+ * Nesting depth of in-flight subagent spawns.
109
+ *
110
+ * Subagents are created via runAgent(), which re-loads this extension fresh
111
+ * (new runtime, new pi/ctx). Without protection those re-loads clobber the
112
+ * parent-owned shell singletons below, so the nudge would later route to a
113
+ * dead subagent session instead of the parent. The factory checks this flag
114
+ * and stays inert while a subagent is spawning.
115
+ */
116
+ let subagentSpawnDepth = 0;
117
+
118
+ export function enterSubagentSpawn(): void {
119
+ subagentSpawnDepth++;
120
+ }
121
+
122
+ export function exitSubagentSpawn(): void {
123
+ if (subagentSpawnDepth > 0) subagentSpawnDepth--;
124
+ }
125
+
126
+ /** True while a subagent is being spawned (factory/session_start run in subagent context). */
127
+ export function isInsideSubagentSpawn(): boolean {
128
+ return subagentSpawnDepth > 0;
129
+ }
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
 
@@ -156,6 +156,8 @@ export interface AgentAccumulatedStats {
156
156
  maxTurns?: number;
157
157
  /** Number of times this agent's session has compacted. Initialized to 0 at spawn. */
158
158
  compactionCount: number;
159
+ /** Previous input token count for delta estimation (vLLM doesn't report cache hits). */
160
+ prevInputTokens?: number;
159
161
  /** Last-known context usage percentage (0–100), captured at completion. */
160
162
  contextPercent?: number | null;
161
163
  }
@@ -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.",