@gotgenes/pi-subagents 6.8.1 → 6.8.2

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/CHANGELOG.md CHANGED
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [6.8.2](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.8.1...pi-subagents-v6.8.2) (2026-05-22)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * mark Step C complete in architecture.md ([#112](https://github.com/gotgenes/pi-packages/issues/112)) ([9f6e783](https://github.com/gotgenes/pi-packages/commit/9f6e783ab4ddcabdc24224e02aa631b93b0fb6d4))
14
+ * plan replace AgentManager callbacks with observer interface ([#112](https://github.com/gotgenes/pi-packages/issues/112)) ([b32dde1](https://github.com/gotgenes/pi-packages/commit/b32dde1e473611b2734a287e88c8d155642405a7))
15
+ * **retro:** add retro notes for issue [#123](https://github.com/gotgenes/pi-packages/issues/123) ([e9333ee](https://github.com/gotgenes/pi-packages/commit/e9333ee2af70e821ba1bace63bdbd7befa72ce84))
16
+
8
17
  ## [6.8.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.8.0...pi-subagents-v6.8.1) (2026-05-21)
9
18
 
10
19
 
@@ -384,7 +384,7 @@ Each step is sequenced so it makes the next step easier.
384
384
  | ~~Mutable state bag~~ | ~~`AgentActivity` (7 fields)~~ | **Fixed #110**: `AgentActivityTracker` class; `ui-observer.ts` calls transition methods; widget, notification, agent-tool use read-only accessors |
385
385
  | ~~Settings relay~~ | ~~`AgentMenuDeps` (13 fields)~~ | **Fixed #109**: `SettingsManager` class; 6 callback fields collapsed to `settings: SettingsManager`; `AgentMenuDeps` now 8 fields |
386
386
  | ~~Post-construction mutation~~ | ~~`AgentRecord` non-transition state~~ | **Fixed #111**: `ExecutionState`, `WorktreeState`, `NotificationState` collaborators; `pendingSteers` moved to `AgentManager`; stats encapsulated behind mutation methods |
387
- | Fire-and-forget callbacks | `AgentManagerOptions` | `onStart`, `onComplete`, `onCompact` wired as closures in `index.ts` |
387
+ | ~~Fire-and-forget callbacks~~ | ~~`AgentManagerOptions`~~ | **Fixed #112**: `AgentManagerObserver` interface; `observer` replaces 3 callbacks; `index.ts` constructs one observer object instead of 3 closure lambdas |
388
388
  | Duplicate `SpawnOptions` | `service.ts` + `agent-manager.ts` | Two incompatible shapes (JSON-friendly vs runtime types) with the same name |
389
389
  | Type dumping ground | `types.ts` | `NotificationDetails` used only by notification/renderer; ~~`DEFAULT_AGENT_NAMES` moved to `AgentTypeRegistry` (#108)~~; `AgentConfig` (21 fields) consumers use 2–4 each |
390
390
  | Wide dependency bags | `AgentToolDeps` (7), `AgentMenuDeps` (8) | Settings narrowed (#109); registry narrowed (#108); more narrowing planned in D steps |
@@ -445,12 +445,12 @@ Split post-construction mutation into phase-specific collaborators, each born co
445
445
  Each piece is born complete at the moment its information is available.
446
446
  The record doesn't accumulate half-baked state — it receives fully constructed collaborators.
447
447
 
448
- ### Step C: Replace `AgentManager` callbacks with observer (#112)
448
+ ### Step C: Replace `AgentManager` callbacks with observer (#112)
449
449
 
450
- Replace the `onStart`/`onComplete`/`onCompact` callback parameters with an `AgentManagerObserver` interface (or typed event emitter).
451
- The observer methods receive the same data the callbacks receive today.
452
- `index.ts` constructs the observer once instead of building 3 closure callbacks that capture `runtime`, `pi`, `notifications`, etc.
453
- `AgentManagerOptions` drops from 85 fields.
450
+ **Done.**
451
+ `AgentManagerObserver` interface replaces `onStart`/`onComplete`/`onCompact`.
452
+ `index.ts` constructs one observer object instead of 3 closure lambdas.
453
+ `AgentManagerOptions` drops from 97 fields.
454
454
 
455
455
  ### Step D: Disambiguate `SpawnOptions` and narrow dependency bags
456
456
 
@@ -0,0 +1,241 @@
1
+ ---
2
+ issue: 112
3
+ issue_title: "refactor(pi-subagents): replace AgentManager callbacks with observer interface"
4
+ ---
5
+
6
+ # Replace AgentManager callbacks with observer interface
7
+
8
+ ## Problem Statement
9
+
10
+ `AgentManagerOptions` accepts three fire-and-forget callbacks — `onStart`, `onComplete`, `onCompact` — that `index.ts` wires as closure lambdas capturing `runtime`, `pi`, `notifications`, and other extension state.
11
+ This is the same callback-threading pattern that was replaced with direct session subscriptions in #100, but one level up.
12
+ The callbacks are notification-style (fire-and-forget, no return value) and match the observer pattern exactly.
13
+
14
+ ## Goals
15
+
16
+ - Replace the three callback parameters on `AgentManagerOptions` with a single `observer?: AgentManagerObserver` interface.
17
+ - `index.ts` constructs one observer object instead of three independent closure lambdas.
18
+ - `AgentManagerOptions` shrinks by two net fields (remove 3 callbacks, add 1 observer).
19
+ - Preserve all existing behavior: lifecycle events, record persistence, notification dispatch, compaction events.
20
+ - Non-breaking refactor — no public API changes (the callbacks are internal to the package).
21
+
22
+ ## Non-Goals
23
+
24
+ - Extracting the observer implementation from `index.ts` into its own module — that can follow if the object grows.
25
+ - Changing `RecordObserverOptions` (session-level observer) — it remains a separate concern at a different layer.
26
+ - Narrowing `AgentToolDeps` or `AgentMenuDeps` — tracked in #114.
27
+ - Disambiguating `SpawnOptions` — tracked in #113.
28
+
29
+ ## Background
30
+
31
+ ### Current callback wiring
32
+
33
+ `agent-manager.ts` defines three callback type aliases and stores them as private fields:
34
+
35
+ ```typescript
36
+ export type OnAgentStart = (record: AgentRecord) => void;
37
+ export type OnAgentComplete = (record: AgentRecord) => void;
38
+ export type OnAgentCompact = (record: AgentRecord, info: CompactionInfo) => void;
39
+ ```
40
+
41
+ `AgentManager` invokes them at three points:
42
+
43
+ 1. `onStart` — called in `startAgent()` after `record.markRunning()`.
44
+ 2. `onComplete` — called in `startAgent()`'s `.then()` and `.catch()` handlers for background agents, and in `drainQueue()` on late failure.
45
+ 3. `onCompact` — relayed through `subscribeRecordObserver()` → `RecordObserverOptions.onCompact`.
46
+
47
+ `index.ts` builds ~30 lines of closure lambdas (lines 73–116) that capture `pi`, `notifications`, and `buildEventData`.
48
+
49
+ ### Observer pattern already established
50
+
51
+ `record-observer.ts` and `ui/ui-observer.ts` are session-level observers that subscribe directly to session events.
52
+ This refactoring applies the same principle at the manager level — grouping related notification callbacks into a single interface.
53
+
54
+ ### Dependency: issue #111 (AgentRecord lifecycle split)
55
+
56
+ Issue #111 is closed.
57
+ The observer interface is designed against the current record shape, which reflects the post-#111 lifecycle split.
58
+
59
+ ### Architecture reference
60
+
61
+ Phase 7, Step C in `docs/architecture/architecture.md`.
62
+
63
+ ## Design Overview
64
+
65
+ ### Observer interface
66
+
67
+ ```typescript
68
+ export interface AgentManagerObserver {
69
+ onAgentStarted(record: AgentRecord): void;
70
+ onAgentCompleted(record: AgentRecord): void;
71
+ onAgentCompacted(record: AgentRecord, info: CompactionInfo): void;
72
+ }
73
+ ```
74
+
75
+ All three methods are fire-and-forget (void return, no async).
76
+ The interface uses past-tense naming (`Started`, `Completed`, `Compacted`) to signal that these are after-the-fact notifications, not hooks that influence the operation.
77
+
78
+ ### `AgentManagerOptions` change
79
+
80
+ ```typescript
81
+ export interface AgentManagerOptions {
82
+ runner: AgentRunner;
83
+ worktrees: WorktreeManager;
84
+ exec: ShellExec;
85
+ registry: AgentTypeRegistry;
86
+ getMaxConcurrent?: () => number;
87
+ getRunConfig?: () => RunConfig;
88
+ observer?: AgentManagerObserver; // replaces onStart, onComplete, onCompact
89
+ }
90
+ ```
91
+
92
+ Fields go from 9 → 7 (remove 3 callbacks, add 1 observer).
93
+
94
+ ### AgentManager internal changes
95
+
96
+ The three private fields (`onStart`, `onComplete`, `onCompact`) become one: `private observer?: AgentManagerObserver`.
97
+ Call sites change from `this.onStart?.(record)` to `this.observer?.onAgentStarted(record)`.
98
+
99
+ The `onCompact` relay into `subscribeRecordObserver` changes from:
100
+
101
+ ```typescript
102
+ onCompact: (r, info) => this.onCompact?.(r, info),
103
+ ```
104
+
105
+ to:
106
+
107
+ ```typescript
108
+ onCompact: (r, info) => this.observer?.onAgentCompacted(r, info),
109
+ ```
110
+
111
+ ### `CompactionInfo` stays in `agent-manager.ts`
112
+
113
+ `CompactionInfo` is a data shape consumed by the observer interface and already defined in `agent-manager.ts`.
114
+ It stays co-located since both `AgentManagerObserver` and `CompactionInfo` are exported from the same module.
115
+ `record-observer.ts` continues to import `CompactionInfo` from `agent-manager.ts`.
116
+
117
+ ### Observer construction in `index.ts`
118
+
119
+ The three closure lambdas collapse into one object literal:
120
+
121
+ ```typescript
122
+ const observer: AgentManagerObserver = {
123
+ onAgentStarted(record) {
124
+ pi.events.emit("subagents:started", {
125
+ id: record.id, type: record.type, description: record.description,
126
+ });
127
+ },
128
+ onAgentCompleted(record) {
129
+ const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
130
+ const eventData = buildEventData(record);
131
+ if (isError) pi.events.emit("subagents:failed", eventData);
132
+ else pi.events.emit("subagents:completed", eventData);
133
+
134
+ pi.appendEntry("subagents:record", { /* same fields as today */ });
135
+
136
+ if (record.notification?.resultConsumed) {
137
+ notifications.cleanupCompleted(record.id);
138
+ return;
139
+ }
140
+ notifications.sendCompletion(record);
141
+ },
142
+ onAgentCompacted(record, info) {
143
+ pi.events.emit("subagents:compacted", {
144
+ id: record.id, type: record.type, description: record.description,
145
+ reason: info.reason, tokensBefore: info.tokensBefore,
146
+ compactionCount: record.compactionCount,
147
+ });
148
+ },
149
+ };
150
+ ```
151
+
152
+ Passed as `observer` to `new AgentManager({ ..., observer })`.
153
+
154
+ ### Removed exports
155
+
156
+ The three callback type aliases are removed:
157
+
158
+ - `OnAgentStart`
159
+ - `OnAgentComplete`
160
+ - `OnAgentCompact`
161
+
162
+ These are only imported in `agent-manager.test.ts` — no production consumers outside `agent-manager.ts`.
163
+
164
+ ## Module-Level Changes
165
+
166
+ ### `src/agent-manager.ts`
167
+
168
+ - Add `AgentManagerObserver` interface (3 methods).
169
+ - Remove `OnAgentStart`, `OnAgentComplete`, `OnAgentCompact` type aliases.
170
+ - Replace `onStart`, `onComplete`, `onCompact` fields in `AgentManagerOptions` with `observer?: AgentManagerObserver`.
171
+ - Replace three private fields with `private observer?: AgentManagerObserver`.
172
+ - Update constructor to assign `this.observer = options.observer`.
173
+ - Update all call sites: `this.onStart?.(record)` → `this.observer?.onAgentStarted(record)`, etc.
174
+ - The `onCompact` relay to `subscribeRecordObserver` uses `this.observer?.onAgentCompacted`.
175
+
176
+ ### `src/index.ts`
177
+
178
+ - Replace the three closure-lambda properties (`onComplete:`, `onStart:`, `onCompact:`) with a single `observer` object literal.
179
+ - Import `AgentManagerObserver` from `agent-manager.ts`.
180
+ - Remove any now-unused callback type imports.
181
+
182
+ ### `src/record-observer.ts`
183
+
184
+ - No changes.
185
+ `RecordObserverOptions.onCompact` stays as-is — it is the session-level relay, not the manager-level observer.
186
+ `CompactionInfo` import from `agent-manager.ts` stays unchanged.
187
+
188
+ ### `test/agent-manager.test.ts`
189
+
190
+ - Update imports: remove `OnAgentStart`, `OnAgentComplete`, `OnAgentCompact`; add `AgentManagerObserver`.
191
+ - Update `createManager` helper: replace the three callback overrides with `observer?: Partial<AgentManagerObserver>`.
192
+ The factory spreads a no-op default observer with test-provided overrides.
193
+ - Update each test that wires a callback to construct an observer object instead.
194
+
195
+ ## Test Impact Analysis
196
+
197
+ 1. No new unit tests are enabled by this refactoring — it's a 1:1 shape change.
198
+ 2. No existing tests become redundant — each test exercises a specific lifecycle scenario (race condition, foreground vs background, error handling, queue drain).
199
+ 3. All existing `agent-manager.test.ts` tests stay, with mechanical updates to the observer wiring.
200
+ The assertions remain the same (e.g., "observer.onAgentCompleted fires with `resultConsumed=false` when `markConsumed` called after await").
201
+
202
+ ## TDD Order
203
+
204
+ ### Step 1: Introduce `AgentManagerObserver` interface alongside existing callbacks
205
+
206
+ Add the interface to `agent-manager.ts`.
207
+ No production behavior changes — the interface is unused at this point.
208
+
209
+ - Commit: `refactor: add AgentManagerObserver interface`
210
+
211
+ ### Step 2: Switch `AgentManager` internals to observer
212
+
213
+ 1. Replace the three `onStart`/`onComplete`/`onCompact` fields on `AgentManagerOptions` with `observer?: AgentManagerObserver`.
214
+ 2. Replace the three private fields with one `private observer?: AgentManagerObserver`.
215
+ 3. Update all internal call sites (`this.onStart?.(record)` → `this.observer?.onAgentStarted(record)`, etc.).
216
+ 4. Remove the three callback type aliases (`OnAgentStart`, `OnAgentComplete`, `OnAgentCompact`).
217
+ 5. Update `test/agent-manager.test.ts`: change imports, update `createManager` helper and all test call sites to construct observer objects instead of individual callbacks.
218
+ 6. Run `pnpm run check` to verify types.
219
+
220
+ - Commit: `refactor: replace AgentManager callbacks with observer interface (#112)`
221
+
222
+ ### Step 3: Update `index.ts` to construct observer
223
+
224
+ 1. Replace the three closure-lambda properties in the `new AgentManager({...})` call with a single `observer` object.
225
+ 2. Import `AgentManagerObserver` type.
226
+ 3. Remove unused callback type imports.
227
+ 4. Run full test suite.
228
+
229
+ - Commit: `refactor: construct AgentManagerObserver in index.ts (#112)`
230
+
231
+ ## Risks and Mitigations
232
+
233
+ | Risk | Mitigation |
234
+ | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
235
+ | Tests break silently because esbuild ignores excess properties — old `onComplete` fields pass through without error | Step 2 removes the type aliases, so TypeScript catches any test still using the old shape via `pnpm run check` |
236
+ | `onComplete` error-swallowing behavior changes | The `try/catch` around `this.onComplete?.(record)` in the `.then()` path moves exactly to `this.observer?.onAgentCompleted(record)` — same guard, same catch, same debugLog |
237
+ | `Partial<AgentManagerObserver>` in test factory silently drops required methods | Tests that need a specific method construct it explicitly; the factory only provides a convenient default for tests that don't care about any observer method |
238
+
239
+ ## Open Questions
240
+
241
+ - None — the issue's proposed design is unambiguous and aligns with the established session-observer pattern.
@@ -0,0 +1,49 @@
1
+ ---
2
+ issue: 123
3
+ issue_title: "refactor(pi-subagents): remove vi.fn() cast smell from test helpers"
4
+ ---
5
+
6
+ # Retro: #123 — remove vi.fn() cast smell from test helpers
7
+
8
+ ## Final Retrospective (2026-05-22T00:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Planned and implemented removal of all 9 `as ReturnType<typeof vi.fn>` casts and 5 `vi.mocked()` calls across 3 test files, replacing them with named typed mock variables.
13
+ Released as `pi-subagents-v6.8.1`.
14
+ Pure test hygiene — no production code changes, no behavioral changes, 652 tests unchanged.
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - **Three-file scope executed cleanly once the plan was right.**
21
+ Each file was an independent commit with no cross-file dependencies.
22
+ The named-variable pattern (`const mockGetRecord = vi.fn<AgentManagerLike["getRecord"]>()`) worked identically across all three test files despite different mock construction styles (factory function vs `beforeEach` assignment).
23
+
24
+ #### What caused friction (agent side)
25
+
26
+ - `scope-drift` — The initial plan scoped the fix to `service-adapter.test.ts` only, listing `lifecycle.test.ts` and `tool-start.test.ts` as explicit Non-Goals — despite the planning-phase grep showing all 9 cast sites across 3 files.
27
+ The user redirected with "let's eliminate this pattern of behavior."
28
+ Updated the GitHub issue body, rewrote the plan, and amended the commit.
29
+ Impact: plan rewrite (~2 minutes), no implementation rework.
30
+ User-caught.
31
+ - `missing-context` — Imported `MockInstance` from `vitest` during step 1 (`service-adapter.test.ts`) but actually used `ReturnType<typeof vi.fn<...>>` — matching the pattern used in the other two files.
32
+ The unused import was caught by `pnpm run lint` at the post-implementation check, not proactively.
33
+ Impact: one extra edit + amend cycle.
34
+ Self-identified (via lint).
35
+ - `other` — Ran `git commit --amend` to fix the unused import but HEAD was step 3's commit (`tool-start`), not step 1's (`service-adapter`).
36
+ The amend landed the `service-adapter.test.ts` change into the wrong commit with the wrong message.
37
+ Required `git reset --soft` back to the plan commit and recommitting all 3 files.
38
+ Impact: ~1 minute of git surgery, clean result.
39
+ Self-identified.
40
+
41
+ #### What caused friction (user side)
42
+
43
+ - The issue body scoped the fix to `service-adapter.test.ts` only, which the agent followed literally.
44
+ The broader intent ("eliminate this pattern") was implicit.
45
+ Flagging the desired scope as "all files with this pattern" in the issue body would have avoided the plan rewrite.
46
+
47
+ ### Changes made
48
+
49
+ 1. Wrote retro file at `packages/pi-subagents/docs/retro/0123-remove-vi-fn-cast-smell.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "6.8.1",
3
+ "version": "6.8.2",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },
@@ -13,7 +13,6 @@ import { AgentRecord } from "./agent-record.js";
13
13
  import type { AgentRunner } from "./agent-runner.js";
14
14
  import { AgentTypeRegistry } from "./agent-types.js";
15
15
  import { debugLog } from "./debug.js";
16
- import type { ExecutionState } from "./execution-state.js";
17
16
  import { buildParentSnapshot } from "./parent-snapshot.js";
18
17
  import { subscribeRecordObserver } from "./record-observer.js";
19
18
  import type { RunConfig } from "./runtime.js";
@@ -21,11 +20,15 @@ import type { AgentInvocation, IsolationMode, ParentSnapshot, ShellExec, Subagen
21
20
  import type { WorktreeManager } from "./worktree.js";
22
21
  import { WorktreeState } from "./worktree-state.js";
23
22
 
24
- export type OnAgentComplete = (record: AgentRecord) => void;
25
- export type OnAgentStart = (record: AgentRecord) => void;
26
- export type OnAgentCompact = (record: AgentRecord, info: CompactionInfo) => void;
27
23
  export type CompactionInfo = { reason: "manual" | "threshold" | "overflow"; tokensBefore: number };
28
24
 
25
+ /** Observer interface for agent lifecycle notifications. */
26
+ export interface AgentManagerObserver {
27
+ onAgentStarted(record: AgentRecord): void;
28
+ onAgentCompleted(record: AgentRecord): void;
29
+ onAgentCompacted(record: AgentRecord, info: CompactionInfo): void;
30
+ }
31
+
29
32
  /** Default max concurrent background agents. */
30
33
  const DEFAULT_MAX_CONCURRENT = 4;
31
34
 
@@ -37,9 +40,7 @@ export interface AgentManagerOptions {
37
40
  /** Injected getter for the concurrency limit — owned by SettingsManager. */
38
41
  getMaxConcurrent?: () => number;
39
42
  getRunConfig?: () => RunConfig;
40
- onStart?: OnAgentStart;
41
- onComplete?: OnAgentComplete;
42
- onCompact?: OnAgentCompact;
43
+ observer?: AgentManagerObserver;
43
44
  }
44
45
 
45
46
  interface SpawnArgs {
@@ -80,9 +81,7 @@ export interface SpawnOptions {
80
81
  export class AgentManager {
81
82
  private agents = new Map<string, AgentRecord>();
82
83
  private cleanupInterval: ReturnType<typeof setInterval>;
83
- private onComplete?: OnAgentComplete;
84
- private onStart?: OnAgentStart;
85
- private onCompact?: OnAgentCompact;
84
+ private readonly observer?: AgentManagerObserver;
86
85
  private readonly runner: AgentRunner;
87
86
  private readonly worktrees: WorktreeManager;
88
87
  private readonly exec: ShellExec;
@@ -102,9 +101,7 @@ export class AgentManager {
102
101
  this.worktrees = options.worktrees;
103
102
  this.exec = options.exec;
104
103
  this.registry = options.registry;
105
- this.onComplete = options.onComplete;
106
- this.onStart = options.onStart;
107
- this.onCompact = options.onCompact;
104
+ this.observer = options.observer;
108
105
  this.getRunConfig = options.getRunConfig;
109
106
  this._getMaxConcurrent = options.getMaxConcurrent ?? (() => DEFAULT_MAX_CONCURRENT);
110
107
  // Cleanup completed agents after 10 minutes (but keep sessions for resume)
@@ -196,7 +193,7 @@ export class AgentManager {
196
193
 
197
194
  record.markRunning(Date.now());
198
195
  if (options.isBackground) this.runningBackground++;
199
- this.onStart?.(record);
196
+ this.observer?.onAgentStarted(record);
200
197
 
201
198
  // Wire parent abort signal to stop the subagent when the parent is interrupted
202
199
  let detachParentSignal: (() => void) | undefined;
@@ -239,7 +236,7 @@ export class AgentManager {
239
236
  }
240
237
  // Subscribe record observer for stats accumulation
241
238
  unsubRecordObserver = subscribeRecordObserver(session, record, {
242
- onCompact: (r, info) => this.onCompact?.(r, info),
239
+ onCompact: (r, info) => this.observer?.onAgentCompacted(r, info),
243
240
  });
244
241
  options.onSessionCreated?.(session);
245
242
  },
@@ -268,7 +265,7 @@ export class AgentManager {
268
265
 
269
266
  if (options.isBackground) {
270
267
  this.runningBackground--;
271
- try { this.onComplete?.(record); } catch (err) { debugLog("onComplete callback", err); }
268
+ try { this.observer?.onAgentCompleted(record); } catch (err) { debugLog("onAgentCompleted observer", err); }
272
269
  this.drainQueue();
273
270
  }
274
271
  return responseText;
@@ -290,7 +287,7 @@ export class AgentManager {
290
287
 
291
288
  if (options.isBackground) {
292
289
  this.runningBackground--;
293
- this.onComplete?.(record);
290
+ this.observer?.onAgentCompleted(record);
294
291
  this.drainQueue();
295
292
  }
296
293
  return "";
@@ -311,7 +308,7 @@ export class AgentManager {
311
308
  // Late failure (e.g. strict worktree-isolation) — surface on the record
312
309
  // so the user/agent can see it via /agents, then keep draining.
313
310
  record.markError(err);
314
- this.onComplete?.(record);
311
+ this.observer?.onAgentCompleted(record);
315
312
  }
316
313
  }
317
314
  }
@@ -347,7 +344,7 @@ export class AgentManager {
347
344
  record.resetForResume(Date.now());
348
345
 
349
346
  const unsubResume = subscribeRecordObserver(session, record, {
350
- onCompact: (r, info) => this.onCompact?.(r, info),
347
+ onCompact: (r, info) => this.observer?.onAgentCompacted(r, info),
351
348
  });
352
349
 
353
350
  try {
package/src/index.ts CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { join } from "node:path";
14
14
  import { defineTool, type ExtensionAPI, getAgentDir } from "@earendil-works/pi-coding-agent";
15
- import { AgentManager } from "./agent-manager.js";
15
+ import { AgentManager, type AgentManagerObserver } from "./agent-manager.js";
16
16
  import { getAgentConversation, resumeAgent, runAgent, steerAgent } from "./agent-runner.js";
17
17
  import { AgentTypeRegistry } from "./agent-types.js";
18
18
  import { loadCustomAgents } from "./custom-agents.js";
@@ -64,14 +64,18 @@ export default function (pi: ExtensionAPI) {
64
64
  });
65
65
  settings.load();
66
66
 
67
- // Background completion: emit lifecycle event and delegate to notification system
68
- const manager = new AgentManager({
69
- runner: { run: runAgent, resume: resumeAgent },
70
- worktrees: new GitWorktreeManager(process.cwd()),
71
- exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
72
- registry,
73
- onComplete: (record) => {
74
- // Emit lifecycle event based on terminal status
67
+ // Observer: receives agent lifecycle notifications and dispatches events/notifications.
68
+ const observer: AgentManagerObserver = {
69
+ onAgentStarted(record) {
70
+ // Emit started event when agent transitions to running (including from queue).
71
+ pi.events.emit("subagents:started", {
72
+ id: record.id,
73
+ type: record.type,
74
+ description: record.description,
75
+ });
76
+ },
77
+ onAgentCompleted(record) {
78
+ // Emit lifecycle event based on terminal status.
75
79
  const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
76
80
  const eventData = buildEventData(record);
77
81
  if (isError) {
@@ -80,14 +84,14 @@ export default function (pi: ExtensionAPI) {
80
84
  pi.events.emit("subagents:completed", eventData);
81
85
  }
82
86
 
83
- // Persist final record for cross-extension history reconstruction
87
+ // Persist final record for cross-extension history reconstruction.
84
88
  pi.appendEntry("subagents:record", {
85
89
  id: record.id, type: record.type, description: record.description,
86
90
  status: record.status, result: record.result, error: record.error,
87
91
  startedAt: record.startedAt, completedAt: record.completedAt,
88
92
  });
89
93
 
90
- // Skip notification if result was already consumed via get_subagent_result
94
+ // Skip notification if result was already consumed via get_subagent_result.
91
95
  if (record.notification?.resultConsumed) {
92
96
  notifications.cleanupCompleted(record.id);
93
97
  return;
@@ -95,15 +99,7 @@ export default function (pi: ExtensionAPI) {
95
99
 
96
100
  notifications.sendCompletion(record);
97
101
  },
98
- onStart: (record) => {
99
- // Emit started event when agent transitions to running (including from queue)
100
- pi.events.emit("subagents:started", {
101
- id: record.id,
102
- type: record.type,
103
- description: record.description,
104
- });
105
- },
106
- onCompact: (record, info) => {
102
+ onAgentCompacted(record, info) {
107
103
  // Emit compacted event when agent's session compacts (preserves count on record).
108
104
  pi.events.emit("subagents:compacted", {
109
105
  id: record.id,
@@ -114,6 +110,14 @@ export default function (pi: ExtensionAPI) {
114
110
  compactionCount: record.compactionCount,
115
111
  });
116
112
  },
113
+ };
114
+
115
+ const manager = new AgentManager({
116
+ runner: { run: runAgent, resume: resumeAgent },
117
+ worktrees: new GitWorktreeManager(process.cwd()),
118
+ exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
119
+ registry,
120
+ observer,
117
121
  getMaxConcurrent: () => settings.maxConcurrent,
118
122
  getRunConfig: () => settings,
119
123
  });