@gotgenes/pi-subagents 6.5.0 → 6.7.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.
@@ -0,0 +1,108 @@
1
+ /**
2
+ * agent-activity-tracker.ts — Per-agent live activity state with explicit transition methods.
3
+ *
4
+ * Replaces the mutable `AgentActivity` interface that was written via output arguments
5
+ * in `ui-observer.ts`. Callers use named transition methods; readers use read-only accessors.
6
+ */
7
+
8
+ import { addUsage, type LifetimeUsage, type SessionLike } from "../usage.js";
9
+
10
+ /** Usage delta accepted by onUsageUpdate — matches the LifetimeUsage accumulator shape. */
11
+ export interface UsageDelta {
12
+ input: number;
13
+ output: number;
14
+ cacheWrite: number;
15
+ }
16
+
17
+ /** Per-agent live activity state with explicit transition methods and read-only accessors. */
18
+ export class AgentActivityTracker {
19
+ private _activeTools = new Map<string, string>();
20
+ private _toolKeySeq = 0;
21
+ private _toolUses = 0;
22
+ private _responseText = "";
23
+ private _session: SessionLike | undefined = undefined;
24
+ private _turnCount = 1;
25
+ private _lifetimeUsage: LifetimeUsage = { input: 0, output: 0, cacheWrite: 0 };
26
+
27
+ constructor(private readonly _maxTurns?: number) {}
28
+
29
+ // ── Transition methods (write surface) ──────────────────────────────────
30
+
31
+ /** Record that a tool has started executing. */
32
+ onToolStart(toolName: string): void {
33
+ this._activeTools.set(toolName + "_" + (++this._toolKeySeq), toolName);
34
+ }
35
+
36
+ /** Record that a tool has finished executing; increments toolUses. No-op when no matching tool is active. */
37
+ onToolEnd(toolName: string): void {
38
+ for (const [key, name] of this._activeTools) {
39
+ if (name === toolName) {
40
+ this._activeTools.delete(key);
41
+ this._toolUses++;
42
+ break;
43
+ }
44
+ }
45
+ }
46
+
47
+ /** Reset the current response text (called at the start of each assistant message). */
48
+ onMessageStart(): void {
49
+ this._responseText = "";
50
+ }
51
+
52
+ /** Append a text delta to the current response text. */
53
+ onMessageUpdate(delta: string): void {
54
+ this._responseText += delta;
55
+ }
56
+
57
+ /** Record that a turn has ended; increments turnCount. */
58
+ onTurnEnd(): void {
59
+ this._turnCount++;
60
+ }
61
+
62
+ /** Accumulate a usage delta into the lifetime usage totals. */
63
+ onUsageUpdate(delta: UsageDelta): void {
64
+ addUsage(this._lifetimeUsage, delta);
65
+ }
66
+
67
+ /** Bind the session reference (called once when the agent session is created). */
68
+ setSession(session: SessionLike): void {
69
+ this._session = session;
70
+ }
71
+
72
+ // ── Read-only accessors ──────────────────────────────────────────────────
73
+
74
+ /** Currently-active tools: key → tool name. Multiple entries for concurrent same-name tools. */
75
+ get activeTools(): ReadonlyMap<string, string> {
76
+ return this._activeTools;
77
+ }
78
+
79
+ /** Total completed tool invocations. */
80
+ get toolUses(): number {
81
+ return this._toolUses;
82
+ }
83
+
84
+ /** The agent's latest partial response text (reset at each message start). */
85
+ get responseText(): string {
86
+ return this._responseText;
87
+ }
88
+
89
+ /** The active SDK session, or undefined before the first session is created. */
90
+ get session(): SessionLike | undefined {
91
+ return this._session;
92
+ }
93
+
94
+ /** Current turn count (starts at 1). */
95
+ get turnCount(): number {
96
+ return this._turnCount;
97
+ }
98
+
99
+ /** Effective max turns for this agent, or undefined for unlimited. */
100
+ get maxTurns(): number | undefined {
101
+ return this._maxTurns;
102
+ }
103
+
104
+ /** Accumulated lifetime token usage (survives compaction). */
105
+ get lifetimeUsage(): Readonly<LifetimeUsage> {
106
+ return this._lifetimeUsage;
107
+ }
108
+ }
@@ -9,7 +9,7 @@ import {
9
9
  } from "../agent-types.js";
10
10
  import type { ModelRegistry } from "../model-resolver.js";
11
11
  import type { AgentConfig, AgentRecord } from "../types.js";
12
- import type { AgentActivity } from "./agent-widget.js";
12
+ import type { AgentActivityTracker } from "./agent-activity-tracker.js";
13
13
  import { formatDuration, getDisplayName } from "./agent-widget.js";
14
14
 
15
15
  // ---- Deps interface ----
@@ -20,22 +20,22 @@ export interface AgentMenuManager {
20
20
  getRecord: (id: string) => AgentRecord | undefined;
21
21
  /** Used by generate wizard to spawn an agent that writes the .md file. */
22
22
  spawnAndWait: (ctx: ExtensionContext, type: string, prompt: string, opts: Omit<SpawnOptions, "isBackground">) => Promise<AgentRecord>;
23
- /** Drain the concurrency queue after maxConcurrent has been updated on SettingsManager. */
24
- notifyConcurrencyChanged: () => void;
25
23
  }
26
24
 
27
25
  /** Narrow settings interface required by the agent menu. */
28
26
  export interface AgentMenuSettings {
29
- maxConcurrent: number;
30
- defaultMaxTurns: number | undefined;
31
- graceTurns: number;
32
- saveAndNotify(msg: string): { message: string; level: "info" | "warning" };
27
+ readonly maxConcurrent: number;
28
+ readonly defaultMaxTurns: number | undefined;
29
+ readonly graceTurns: number;
30
+ applyMaxConcurrent(n: number): { message: string; level: "info" | "warning" };
31
+ applyDefaultMaxTurns(n: number): { message: string; level: "info" | "warning" };
32
+ applyGraceTurns(n: number): { message: string; level: "info" | "warning" };
33
33
  }
34
34
 
35
35
  export interface AgentMenuDeps {
36
36
  manager: AgentMenuManager;
37
37
  registry: AgentTypeRegistry;
38
- agentActivity: Map<string, AgentActivity>;
38
+ agentActivity: Map<string, AgentActivityTracker>;
39
39
  /** Resolve model label for a given agent type + registry. */
40
40
  getModelLabel: (type: string, registry?: ModelRegistry) => string;
41
41
  /** Settings manager — owns in-memory values and persistence. */
@@ -617,9 +617,8 @@ ${systemPrompt}
617
617
  if (val) {
618
618
  const n = parseInt(val, 10);
619
619
  if (n >= 1) {
620
- deps.settings.maxConcurrent = n;
621
- deps.manager.notifyConcurrencyChanged();
622
- notifyApplied(ctx, `Max concurrency set to ${n}`);
620
+ const toast = deps.settings.applyMaxConcurrent(n);
621
+ ctx.ui.notify(toast.message, toast.level);
623
622
  } else {
624
623
  ctx.ui.notify("Must be a positive integer.", "warning");
625
624
  }
@@ -631,12 +630,9 @@ ${systemPrompt}
631
630
  );
632
631
  if (val) {
633
632
  const n = parseInt(val, 10);
634
- if (n === 0) {
635
- deps.settings.defaultMaxTurns = undefined;
636
- notifyApplied(ctx, "Default max turns set to unlimited");
637
- } else if (n >= 1) {
638
- deps.settings.defaultMaxTurns = n;
639
- notifyApplied(ctx, `Default max turns set to ${n}`);
633
+ if (n >= 0) {
634
+ const toast = deps.settings.applyDefaultMaxTurns(n);
635
+ ctx.ui.notify(toast.message, toast.level);
640
636
  } else {
641
637
  ctx.ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
642
638
  }
@@ -649,8 +645,8 @@ ${systemPrompt}
649
645
  if (val) {
650
646
  const n = parseInt(val, 10);
651
647
  if (n >= 1) {
652
- deps.settings.graceTurns = n;
653
- notifyApplied(ctx, `Grace turns set to ${n}`);
648
+ const toast = deps.settings.applyGraceTurns(n);
649
+ ctx.ui.notify(toast.message, toast.level);
654
650
  } else {
655
651
  ctx.ui.notify("Must be a positive integer.", "warning");
656
652
  }
@@ -658,11 +654,6 @@ ${systemPrompt}
658
654
  }
659
655
  }
660
656
 
661
- function notifyApplied(ctx: ExtensionContext, successMsg: string) {
662
- const { message, level } = deps.settings.saveAndNotify(successMsg);
663
- ctx.ui.notify(message, level as "info" | "warning" | "error");
664
- }
665
-
666
657
  // Return the handler function
667
658
  return async (ctx: ExtensionContext) => {
668
659
  await showAgentsMenu(ctx);
@@ -9,7 +9,8 @@ import { truncateToWidth } from "@earendil-works/pi-tui";
9
9
  import type { AgentManager } from "../agent-manager.js";
10
10
  import { type AgentConfigLookup, AgentTypeRegistry } from "../agent-types.js";
11
11
  import type { AgentInvocation, SubagentType } from "../types.js";
12
- import { getLifetimeTotal, getSessionContextPercent, type LifetimeUsage, type SessionLike } from "../usage.js";
12
+ import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
13
+ import type { AgentActivityTracker } from "./agent-activity-tracker.js";
13
14
 
14
15
  // ---- Constants ----
15
16
 
@@ -49,20 +50,6 @@ export type UICtx = {
49
50
  ): void;
50
51
  };
51
52
 
52
- /** Per-agent live activity state. */
53
- export interface AgentActivity {
54
- activeTools: Map<string, string>;
55
- toolUses: number;
56
- responseText: string;
57
- session?: SessionLike;
58
- /** Current turn count. */
59
- turnCount: number;
60
- /** Effective max turns for this agent (undefined = unlimited). */
61
- maxTurns?: number;
62
- /** Lifetime usage breakdown — see LifetimeUsage docs. */
63
- lifetimeUsage: LifetimeUsage;
64
- }
65
-
66
53
  /** Metadata attached to Agent tool results for custom rendering. */
67
54
  export interface AgentDetails {
68
55
  displayName: string;
@@ -177,7 +164,7 @@ function truncateLine(text: string, len = 60): string {
177
164
  }
178
165
 
179
166
  /** Build a human-readable activity string from currently-running tools or response text. */
180
- export function describeActivity(activeTools: Map<string, string>, responseText?: string): string {
167
+ export function describeActivity(activeTools: ReadonlyMap<string, string>, responseText?: string): string {
181
168
  if (activeTools.size > 0) {
182
169
  const groups = new Map<string, number>();
183
170
  for (const toolName of activeTools.values()) {
@@ -224,7 +211,7 @@ export class AgentWidget {
224
211
 
225
212
  constructor(
226
213
  private manager: AgentManager,
227
- private agentActivity: Map<string, AgentActivity>,
214
+ private agentActivity: Map<string, AgentActivityTracker>,
228
215
  private registry: AgentTypeRegistry,
229
216
  ) {}
230
217
 
@@ -11,8 +11,8 @@ import type { AgentConfigLookup } from "../agent-types.js";
11
11
  import { extractText } from "../context.js";
12
12
  import type { AgentRecord } from "../types.js";
13
13
  import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
14
- import type { Theme } from "./agent-widget.js";
15
- import { type AgentActivity, buildInvocationTags, describeActivity, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel } from "./agent-widget.js";
14
+ import type { AgentActivityTracker } from "./agent-activity-tracker.js";
15
+ import { buildInvocationTags, describeActivity, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel, type Theme } from "./agent-widget.js";
16
16
 
17
17
  /** Base lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
18
18
  const CHROME_LINES_BASE = 6;
@@ -31,7 +31,7 @@ export class ConversationViewer implements Component {
31
31
  private tui: TUI,
32
32
  private session: AgentSession,
33
33
  private record: AgentRecord,
34
- private activity: AgentActivity | undefined,
34
+ private activity: AgentActivityTracker | undefined,
35
35
  private theme: Theme,
36
36
  private done: (result: undefined) => void,
37
37
  private registry: AgentConfigLookup,
@@ -1,13 +1,12 @@
1
1
  /**
2
- * ui-observer.ts — Subscribes to session events and updates AgentActivity state.
2
+ * ui-observer.ts — Subscribes to session events and updates AgentActivityTracker state.
3
3
  *
4
4
  * Replaces the callback-based createActivityTracker pattern with a direct
5
5
  * session subscription for streaming UI state (active tools, response text,
6
6
  * turn count, lifetime usage).
7
7
  */
8
8
 
9
- import { addUsage } from "../usage.js";
10
- import type { AgentActivity } from "./agent-widget.js";
9
+ import type { AgentActivityTracker } from "./agent-activity-tracker.js";
11
10
 
12
11
  /** Narrow session interface — only the subscribe method needed by the observer. */
13
12
  interface SubscribableSession {
@@ -15,15 +14,15 @@ interface SubscribableSession {
15
14
  }
16
15
 
17
16
  /**
18
- * Subscribe to session events and stream UI state into an AgentActivity object.
17
+ * Subscribe to session events and stream UI state into an AgentActivityTracker.
19
18
  *
20
19
  * Handles:
21
- * - `tool_execution_start` → add to `state.activeTools`
22
- * - `tool_execution_end` → remove from `state.activeTools`, `state.toolUses++`
23
- * - `message_start` → reset `state.responseText`
24
- * - `message_update` (text_delta) → append to `state.responseText`
25
- * - `turn_end` → `state.turnCount++`
26
- * - `message_end` (assistant, with usage) → `addUsage(state.lifetimeUsage, …)`
20
+ * - `tool_execution_start` → `tracker.onToolStart(name)`
21
+ * - `tool_execution_end` → `tracker.onToolEnd(name)`
22
+ * - `message_start` → `tracker.onMessageStart()`
23
+ * - `message_update` (text_delta) → `tracker.onMessageUpdate(delta)`
24
+ * - `turn_end` → `tracker.onTurnEnd()`
25
+ * - `message_end` (assistant, with usage) → `tracker.onUsageUpdate(usage)`
27
26
  *
28
27
  * Calls `onUpdate?.()` after each state mutation to trigger re-renders.
29
28
  *
@@ -31,47 +30,41 @@ interface SubscribableSession {
31
30
  */
32
31
  export function subscribeUIObserver(
33
32
  session: SubscribableSession,
34
- state: AgentActivity,
33
+ tracker: AgentActivityTracker,
35
34
  onUpdate?: () => void,
36
35
  ): () => void {
37
36
  return session.subscribe((event: any) => {
38
37
  if (event.type === "tool_execution_start") {
39
- state.activeTools.set(event.toolName + "_" + Date.now(), event.toolName);
38
+ tracker.onToolStart(event.toolName);
40
39
  onUpdate?.();
41
40
  }
42
41
 
43
42
  if (event.type === "tool_execution_end") {
44
- for (const [key, name] of state.activeTools) {
45
- if (name === event.toolName) {
46
- state.activeTools.delete(key);
47
- break;
48
- }
49
- }
50
- state.toolUses++;
43
+ tracker.onToolEnd(event.toolName);
51
44
  onUpdate?.();
52
45
  }
53
46
 
54
47
  if (event.type === "message_start") {
55
- state.responseText = "";
48
+ tracker.onMessageStart();
56
49
  }
57
50
 
58
51
  if (
59
52
  event.type === "message_update" &&
60
53
  event.assistantMessageEvent?.type === "text_delta"
61
54
  ) {
62
- state.responseText += event.assistantMessageEvent.delta;
55
+ tracker.onMessageUpdate(event.assistantMessageEvent.delta);
63
56
  onUpdate?.();
64
57
  }
65
58
 
66
59
  if (event.type === "turn_end") {
67
- state.turnCount++;
60
+ tracker.onTurnEnd();
68
61
  onUpdate?.();
69
62
  }
70
63
 
71
64
  if (event.type === "message_end" && event.message?.role === "assistant") {
72
65
  const u = event.message.usage;
73
66
  if (u) {
74
- addUsage(state.lifetimeUsage, {
67
+ tracker.onUsageUpdate({
75
68
  input: u.input ?? 0,
76
69
  output: u.output ?? 0,
77
70
  cacheWrite: u.cacheWrite ?? 0,