@gotgenes/pi-subagents 16.6.0 → 17.0.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.
@@ -12,22 +12,8 @@ import { buildDetails, buildTypeListText, textResult } from "#src/tools/helpers"
12
12
  import { renderAgentResult } from "#src/tools/result-renderer";
13
13
  import { type ModelInfo, resolveSpawnConfig } from "#src/tools/spawn-config";
14
14
  import type { ParentSessionInfo, Subagent } from "#src/types";
15
- import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
16
- import { type UICtx } from "#src/ui/agent-widget";
17
15
  import { type AgentDetails, getDisplayName } from "#src/ui/display";
18
16
 
19
- // ---- Shared interfaces (also used by background-spawner and foreground-runner) ----
20
-
21
- /**
22
- * Narrow read/write interface for the agent-tool's agentActivity access.
23
- * The full Map satisfies this structurally — no wrapper needed.
24
- */
25
- export interface AgentActivityAccess {
26
- get(id: string): AgentActivityTracker | undefined;
27
- set(id: string, tracker: AgentActivityTracker): void;
28
- delete(id: string): void;
29
- }
30
-
31
17
  // ---- Deps interfaces ----
32
18
 
33
19
  /** Narrow manager interface — only the methods the Agent tool calls. */
@@ -40,24 +26,11 @@ export interface AgentToolManager {
40
26
 
41
27
  /** Narrow runtime interface — the Agent tool's slice of SubagentRuntime. */
42
28
  export interface AgentToolRuntime {
43
- readonly agentActivity: AgentActivityAccess;
44
29
  buildSnapshot(inheritContext: boolean): ParentSnapshot;
45
30
  getModelInfo(): ModelInfo;
46
31
  getSessionInfo(): { parentSessionFile: string; parentSessionId: string };
47
32
  }
48
33
 
49
- /**
50
- * Narrow widget interface the Agent tool drives directly.
51
- * Superset of the runner/spawner widget deps plus `setUICtx`.
52
- * AgentWidget satisfies it structurally.
53
- */
54
- export interface AgentToolWidget {
55
- setUICtx(ctx: UICtx): void;
56
- ensureTimer(): void;
57
- update(): void;
58
- markFinished(id: string): void;
59
- }
60
-
61
34
  /** Narrow settings accessor — only the fields the Agent tool reads. */
62
35
  export type AgentToolSettings = {
63
36
  readonly defaultMaxTurns: number | undefined;
@@ -73,7 +46,6 @@ export class AgentTool {
73
46
  constructor(
74
47
  private readonly manager: AgentToolManager,
75
48
  private readonly runtime: AgentToolRuntime,
76
- private readonly widget: AgentToolWidget,
77
49
  private readonly settings: AgentToolSettings,
78
50
  private readonly registry: AgentTypeRegistry,
79
51
  private readonly agentDir: string,
@@ -87,11 +59,8 @@ export class AgentTool {
87
59
  params: Record<string, unknown>,
88
60
  signal: AbortSignal | undefined,
89
61
  onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
90
- ctx: any,
62
+ _ctx: any,
91
63
  ) {
92
- // Ensure we have UI context for widget rendering
93
- this.widget.setUICtx(ctx.ui as UICtx);
94
-
95
64
  // Reload custom agents so new .pi/agents/*.md files are picked up without restart
96
65
  this.registry.reload();
97
66
 
@@ -140,8 +109,6 @@ export class AgentTool {
140
109
  if (config.execution.runInBackground) {
141
110
  return spawnBackground(
142
111
  this.manager,
143
- this.widget,
144
- this.runtime.agentActivity,
145
112
  { config, snapshot, parentSession, settings: this.settings },
146
113
  );
147
114
  }
@@ -149,8 +116,6 @@ export class AgentTool {
149
116
  // ---- Foreground execution — stream progress via onUpdate ----
150
117
  return runForeground(
151
118
  this.manager,
152
- this.widget,
153
- this.runtime.agentActivity,
154
119
  { config, snapshot, parentSession },
155
120
  signal,
156
121
  onUpdate,
@@ -1,11 +1,8 @@
1
1
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
2
2
  import type { AgentSpawnConfig } from "#src/lifecycle/subagent-manager";
3
- import type { AgentActivityAccess } from "#src/tools/agent-tool";
4
3
  import { textResult } from "#src/tools/helpers";
5
4
  import type { ResolvedSpawnConfig } from "#src/tools/spawn-config";
6
5
  import type { ParentSessionInfo, Subagent } from "#src/types";
7
- import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
8
- import { subscribeUIObserver } from "#src/ui/ui-observer";
9
6
 
10
7
  /** Narrow manager interface for the background spawner. */
11
8
  export interface BackgroundManagerDeps {
@@ -13,12 +10,6 @@ export interface BackgroundManagerDeps {
13
10
  getRecord(id: string): Subagent | undefined;
14
11
  }
15
12
 
16
- /** Narrow widget interface for the background spawner. */
17
- export interface BackgroundWidgetDeps {
18
- ensureTimer(): void;
19
- update(): void;
20
- }
21
-
22
13
  /** All values the background spawner needs beyond the resolved config. */
23
14
  export interface BackgroundParams {
24
15
  config: ResolvedSpawnConfig;
@@ -29,17 +20,13 @@ export interface BackgroundParams {
29
20
 
30
21
  /**
31
22
  * Spawn a background agent and return the tool result immediately.
32
- * Owns: activity tracker creation, UI observer subscription, activity map
33
- * registration, widget update, and launch message formatting.
23
+ * Owns: launch message formatting.
34
24
  */
35
25
  export function spawnBackground(
36
26
  manager: BackgroundManagerDeps,
37
- widget: BackgroundWidgetDeps,
38
- agentActivity: AgentActivityAccess,
39
27
  params: BackgroundParams,
40
28
  ) {
41
29
  const { identity, execution, presentation } = params.config;
42
- const bgState = new AgentActivityTracker(execution.effectiveMaxTurns);
43
30
 
44
31
  let id: string;
45
32
  try {
@@ -52,13 +39,6 @@ export function spawnBackground(
52
39
  thinkingLevel: execution.thinking,
53
40
  isBackground: true,
54
41
  invocation: execution.agentInvocation,
55
- observer: {
56
- onSessionCreated: (agent) => {
57
- const sub = agent.subagentSession!;
58
- bgState.setSession(sub);
59
- subscribeUIObserver(sub, bgState);
60
- },
61
- },
62
42
  });
63
43
  } catch (err) {
64
44
  return textResult(err instanceof Error ? err.message : String(err));
@@ -66,10 +46,6 @@ export function spawnBackground(
66
46
 
67
47
  const record = manager.getRecord(id);
68
48
 
69
- agentActivity.set(id, bgState);
70
- widget.ensureTimer();
71
- widget.update();
72
-
73
49
  const isQueued = record?.status === "queued";
74
50
  return textResult(
75
51
  `Agent ${isQueued ? "queued" : "started"} in background.\n` +
@@ -1,7 +1,6 @@
1
1
  import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
2
2
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
3
3
  import type { AgentSpawnConfig } from "#src/lifecycle/subagent-manager";
4
- import type { AgentActivityAccess } from "#src/tools/agent-tool";
5
4
  import {
6
5
  buildDetails,
7
6
  formatLifetimeTokens,
@@ -10,14 +9,12 @@ import {
10
9
  } from "#src/tools/helpers";
11
10
  import type { ResolvedSpawnConfig } from "#src/tools/spawn-config";
12
11
  import type { ParentSessionInfo, Subagent } from "#src/types";
13
- import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
14
12
  import {
15
13
  type AgentDetails,
16
14
  describeActivity,
17
15
  formatMs,
18
16
  SPINNER,
19
17
  } from "#src/ui/display";
20
- import { subscribeUIObserver } from "#src/ui/ui-observer";
21
18
 
22
19
  /** Narrow manager interface for the foreground runner. */
23
20
  export interface ForegroundManagerDeps {
@@ -29,12 +26,6 @@ export interface ForegroundManagerDeps {
29
26
  ): Promise<Subagent>;
30
27
  }
31
28
 
32
- /** Narrow widget interface for the foreground runner. */
33
- export interface ForegroundWidgetDeps {
34
- ensureTimer(): void;
35
- markFinished(id: string): void;
36
- }
37
-
38
29
  /** All values the foreground runner needs beyond the resolved config. */
39
30
  export interface ForegroundParams {
40
31
  config: ResolvedSpawnConfig;
@@ -44,13 +35,10 @@ export interface ForegroundParams {
44
35
 
45
36
  /**
46
37
  * Run an agent synchronously in the foreground, streaming spinner updates.
47
- * Owns: spinner interval, AgentActivityTracker creation, UI observer subscription,
48
- * streaming onUpdate callbacks, cleanup, and result formatting.
38
+ * Owns: spinner interval, streaming onUpdate callbacks, cleanup, and result formatting.
49
39
  */
50
40
  export async function runForeground(
51
41
  manager: ForegroundManagerDeps,
52
- widget: ForegroundWidgetDeps,
53
- agentActivity: AgentActivityAccess,
54
42
  params: ForegroundParams,
55
43
  signal: AbortSignal | undefined,
56
44
  onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
@@ -58,10 +46,7 @@ export async function runForeground(
58
46
  const { identity, execution, presentation } = params.config;
59
47
  let spinnerFrame = 0;
60
48
  const startedAt = Date.now();
61
- let fgId: string | undefined;
62
49
 
63
- const fgState = new AgentActivityTracker(execution.effectiveMaxTurns);
64
- let unsubUI: (() => void) | undefined;
65
50
  let recordRef: Subagent | undefined;
66
51
 
67
52
  const streamUpdate = () => {
@@ -113,31 +98,17 @@ export async function runForeground(
113
98
  parentSession: params.parentSession,
114
99
  observer: {
115
100
  onSessionCreated: (agent) => {
116
- const sub = agent.subagentSession!;
117
- fgState.setSession(sub);
118
101
  recordRef = agent;
119
- unsubUI = subscribeUIObserver(sub, fgState, streamUpdate);
120
- fgId = agent.id;
121
- agentActivity.set(agent.id, fgState);
122
- widget.ensureTimer();
123
102
  },
124
103
  },
125
104
  },
126
105
  );
127
106
  } catch (err) {
128
107
  clearInterval(spinnerInterval);
129
- unsubUI?.();
130
108
  return textResult(err instanceof Error ? err.message : String(err));
131
109
  }
132
110
 
133
111
  clearInterval(spinnerInterval);
134
- unsubUI?.();
135
-
136
- // Clean up foreground agent from widget
137
- if (fgId) {
138
- agentActivity.delete(fgId);
139
- widget.markFinished(fgId);
140
- }
141
112
 
142
113
  const tokenText = formatLifetimeTokens(record);
143
114
  const details = buildDetails(presentation.detailBase, record, { tokens: tokenText });
package/src/types.ts CHANGED
@@ -12,7 +12,7 @@ export type { AgentSessionEvent, ThinkingLevel };
12
12
 
13
13
  /**
14
14
  * Narrow session interface for event subscription.
15
- * Used by record-observer and ui-observer — only the subscribe method is needed.
15
+ * Used by record-observer — only the subscribe method is needed.
16
16
  */
17
17
  export interface SubscribableSession {
18
18
  subscribe(fn: (event: AgentSessionEvent) => void): () => void;
@@ -8,7 +8,8 @@
8
8
 
9
9
  import { AgentTypeRegistry } from "#src/config/agent-types";
10
10
  import type { Subagent } from "#src/lifecycle/subagent";
11
- import type { SubagentManager } from "#src/lifecycle/subagent-manager";
11
+ import type { SubagentManager, SubagentManagerObserver } from "#src/lifecycle/subagent-manager";
12
+ import type { CompactionInfo } from "#src/types";
12
13
  import { ERROR_STATUSES, type Theme } from "#src/ui/display";
13
14
  import { renderWidgetLines, type WidgetAgent } from "#src/ui/widget-renderer";
14
15
 
@@ -61,7 +62,7 @@ export type UICtx = {
61
62
 
62
63
  // ---- Widget manager ----
63
64
 
64
- export class AgentWidget {
65
+ export class AgentWidget implements SubagentManagerObserver {
65
66
  private uiCtx: UICtx | undefined;
66
67
  private widgetFrame = 0;
67
68
  private widgetInterval: ReturnType<typeof setInterval> | undefined;
@@ -107,9 +108,36 @@ export class AgentWidget {
107
108
  this.update();
108
109
  }
109
110
 
111
+ // ---- SubagentManagerObserver: react to lifecycle, self-drive the timer ----
112
+
113
+ /** A subagent started running — ensure the update loop is live and render. */
114
+ onSubagentStarted(_record: Subagent) {
115
+ this.startLoop();
116
+ }
117
+
118
+ /** A background subagent was created (queued) — ensure the loop is live and render. */
119
+ onSubagentCreated(_record: Subagent) {
120
+ this.startLoop();
121
+ }
122
+
123
+ /** A subagent completed — render so the finished state is seeded and shown. */
124
+ onSubagentCompleted(_record: Subagent) {
125
+ this.update();
126
+ }
127
+
128
+ /** A subagent's session compacted — render to refresh the compaction count. */
129
+ onSubagentCompacted(_record: Subagent, _info: CompactionInfo) {
130
+ this.update();
131
+ }
132
+
133
+ /** Start the update timer (if not already running) and render immediately. */
134
+ private startLoop() {
135
+ this.ensureTimer();
136
+ this.update();
137
+ }
138
+
110
139
  /** Ensure the widget update timer is running. */
111
- // fallow-ignore-next-line unused-class-member
112
- ensureTimer() {
140
+ private ensureTimer() {
113
141
  this.widgetInterval ??= setInterval(() => this.update(), 80);
114
142
  }
115
143
 
@@ -120,14 +148,6 @@ export class AgentWidget {
120
148
  return age < maxAge;
121
149
  }
122
150
 
123
- /** Record an agent as finished (call when agent completes). */
124
- // fallow-ignore-next-line unused-class-member
125
- markFinished(agentId: string) {
126
- if (!this.finishedTurnAge.has(agentId)) {
127
- this.finishedTurnAge.set(agentId, 0);
128
- }
129
- }
130
-
131
151
  /** Project a live Subagent record onto a pure-data WidgetAgent snapshot. */
132
152
  private toWidgetAgent(record: Subagent): WidgetAgent {
133
153
  return {
@@ -203,8 +223,8 @@ export class AgentWidget {
203
223
 
204
224
  /**
205
225
  * Seed linger tracking for any newly-observed finished agent.
206
- * Replaces the external `markFinished` call NotificationManager used to make:
207
- * the widget owns detection of completions it sees via `listAgents()`.
226
+ * The widget owns detection of completions it observes via `listAgents()`,
227
+ * so no external bookkeeping call is needed.
208
228
  * Idempotent — only seeds when an entry is absent, so repeated updates within
209
229
  * a turn neither reset nor advance the age.
210
230
  */
@@ -1,84 +0,0 @@
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 type { SessionLike } from "#src/lifecycle/usage";
9
-
10
- /** Per-agent live activity state with explicit transition methods and read-only accessors. */
11
- export class AgentActivityTracker {
12
- private _activeTools = new Map<string, string>();
13
- private _toolKeySeq = 0;
14
- private _responseText = "";
15
- private _session: SessionLike | undefined = undefined;
16
- private _turnCount = 1;
17
-
18
- constructor(private readonly _maxTurns?: number) {}
19
-
20
- // ── Transition methods (write surface) ──────────────────────────────────
21
-
22
- /** Record that a tool has started executing. */
23
- onToolStart(toolName: string): void {
24
- this._activeTools.set(toolName + "_" + (++this._toolKeySeq), toolName);
25
- }
26
-
27
- /** Remove a tool from active tools (called when tool execution ends). No-op when no matching tool is active. */
28
- onToolDone(toolName: string): void {
29
- for (const [key, name] of this._activeTools) {
30
- if (name === toolName) {
31
- this._activeTools.delete(key);
32
- break;
33
- }
34
- }
35
- }
36
-
37
- /** Reset the current response text (called at the start of each assistant message). */
38
- onMessageStart(): void {
39
- this._responseText = "";
40
- }
41
-
42
- /** Append a text delta to the current response text. */
43
- onMessageUpdate(delta: string): void {
44
- this._responseText += delta;
45
- }
46
-
47
- /** Record that a turn has ended; increments turnCount. */
48
- onTurnEnd(): void {
49
- this._turnCount++;
50
- }
51
-
52
- /** Bind the session reference (called once when the agent session is created). */
53
- setSession(session: SessionLike): void {
54
- this._session = session;
55
- }
56
-
57
- // ── Read-only accessors ──────────────────────────────────────────────────
58
-
59
- /** Currently-active tools: key → tool name. Multiple entries for concurrent same-name tools. */
60
- get activeTools(): ReadonlyMap<string, string> {
61
- return this._activeTools;
62
- }
63
-
64
- /** The agent's latest partial response text (reset at each message start). */
65
- get responseText(): string {
66
- return this._responseText;
67
- }
68
-
69
- /** The active SDK session, or undefined before the first session is created. */
70
- get session(): SessionLike | undefined {
71
- return this._session;
72
- }
73
-
74
- /** Current turn count (starts at 1). */
75
- get turnCount(): number {
76
- return this._turnCount;
77
- }
78
-
79
- /** Effective max turns for this agent, or undefined for unlimited. */
80
- get maxTurns(): number | undefined {
81
- return this._maxTurns;
82
- }
83
-
84
- }
@@ -1,61 +0,0 @@
1
- /**
2
- * ui-observer.ts — Subscribes to session events and updates AgentActivityTracker state.
3
- *
4
- * Replaces the callback-based createActivityTracker pattern with a direct
5
- * session subscription for streaming UI state (active tools, response text,
6
- * turn count, lifetime usage).
7
- */
8
-
9
- import type { SubscribableSession } from "#src/types";
10
- import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
11
-
12
- /**
13
- * Subscribe to session events and stream UI state into an AgentActivityTracker.
14
- *
15
- * Handles:
16
- * - `tool_execution_start` → `tracker.onToolStart(name)`
17
- * - `tool_execution_end` → `tracker.onToolEnd(name)`
18
- * - `message_start` → `tracker.onMessageStart()`
19
- * - `message_update` (text_delta) → `tracker.onMessageUpdate(delta)`
20
- * - `turn_end` → `tracker.onTurnEnd()`
21
- * - `message_end` (assistant, with usage) → `tracker.onUsageUpdate(usage)`
22
- *
23
- * Calls `onUpdate?.()` after each state mutation to trigger re-renders.
24
- *
25
- * @returns An unsubscribe function.
26
- */
27
- export function subscribeUIObserver(
28
- session: SubscribableSession,
29
- tracker: AgentActivityTracker,
30
- onUpdate?: () => void,
31
- ): () => void {
32
- return session.subscribe((event) => {
33
- if (event.type === "tool_execution_start") {
34
- tracker.onToolStart(event.toolName);
35
- onUpdate?.();
36
- }
37
-
38
- if (event.type === "tool_execution_end") {
39
- tracker.onToolDone(event.toolName);
40
- onUpdate?.();
41
- }
42
-
43
- if (event.type === "message_start") {
44
- tracker.onMessageStart();
45
- }
46
-
47
- if (
48
- event.type === "message_update" &&
49
- event.assistantMessageEvent.type === "text_delta"
50
- ) {
51
- tracker.onMessageUpdate(event.assistantMessageEvent.delta);
52
- onUpdate?.();
53
- }
54
-
55
- if (event.type === "turn_end") {
56
- tracker.onTurnEnd();
57
- onUpdate?.();
58
- }
59
-
60
- });
61
- }