@gotgenes/pi-subagents 6.8.0 → 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 +17 -0
- package/docs/architecture/architecture.md +6 -6
- package/docs/plans/0112-replace-agent-manager-callbacks.md +241 -0
- package/docs/plans/0123-remove-vi-fn-cast-smell.md +179 -0
- package/docs/retro/0111-split-agent-record-lifecycle.md +61 -0
- package/docs/retro/0123-remove-vi-fn-cast-smell.md +49 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +16 -19
- package/src/index.ts +24 -20
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ 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
|
+
|
|
17
|
+
## [6.8.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.8.0...pi-subagents-v6.8.1) (2026-05-21)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
|
|
22
|
+
* plan remove vi.fn() cast smell from test helpers ([#123](https://github.com/gotgenes/pi-packages/issues/123)) ([d0e33b3](https://github.com/gotgenes/pi-packages/commit/d0e33b39cf419bb03a2d69c39992600752ce8517))
|
|
23
|
+
* **retro:** add retro notes for issue [#111](https://github.com/gotgenes/pi-packages/issues/111) ([37eea32](https://github.com/gotgenes/pi-packages/commit/37eea32b32135b792c6c94933267c3e5a5f2cd7b))
|
|
24
|
+
|
|
8
25
|
## [6.8.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.7.0...pi-subagents-v6.8.0) (2026-05-21)
|
|
9
26
|
|
|
10
27
|
|
|
@@ -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
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
`index.ts` constructs
|
|
453
|
-
`AgentManagerOptions` drops from
|
|
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 9 → 7 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,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 123
|
|
3
|
+
issue_title: "refactor(pi-subagents): remove vi.fn() cast smell from test helpers"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Remove vi.fn() cast smell from test helpers
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
Several test files construct mock objects typed to narrow interfaces (`AgentManagerLike`, `LifecycleRuntime`, `LifecycleManager`, `ToolStartRuntime`).
|
|
11
|
+
Because the returned objects are typed to the interface — not to Vitest's mock types — tests that need to configure individual method stubs are forced to cast:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
(deps.manager.abort as ReturnType<typeof vi.fn>).mockReturnValue(false);
|
|
15
|
+
(deps.manager.getRecord as ReturnType<typeof vi.fn>).mockReturnValue(record);
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This silences TypeScript without constraining the call's return type — if `getRecord`'s return type changes, the cast won't catch it.
|
|
19
|
+
Nine occurrences exist across three test files.
|
|
20
|
+
|
|
21
|
+
## Goals
|
|
22
|
+
|
|
23
|
+
- Eliminate all `as ReturnType<typeof vi.fn>` casts from the test suite.
|
|
24
|
+
- Preserve type safety: mock configuration calls should be checked against the real method signatures.
|
|
25
|
+
- Keep the change minimal — this is a test hygiene fix, not a structural redesign.
|
|
26
|
+
|
|
27
|
+
## Non-Goals
|
|
28
|
+
|
|
29
|
+
- Changing `AgentManagerLike`, `LifecycleRuntime`, `LifecycleManager`, `ToolStartRuntime`, or any production code.
|
|
30
|
+
- Restructuring test layout or merging describe blocks.
|
|
31
|
+
|
|
32
|
+
## Background
|
|
33
|
+
|
|
34
|
+
The cast pattern was noted during #111 implementation and preserved to keep scope tight.
|
|
35
|
+
Issue #111 (split `AgentRecord` lifecycle state) is now closed and implemented.
|
|
36
|
+
|
|
37
|
+
### Affected files
|
|
38
|
+
|
|
39
|
+
| File | Occurrences | Interface |
|
|
40
|
+
| ---------------------------------- | ----------- | -------------------------------------- |
|
|
41
|
+
| `test/service-adapter.test.ts` | 5 | `AgentManagerLike` |
|
|
42
|
+
| `test/handlers/lifecycle.test.ts` | 2 | `LifecycleRuntime`, `LifecycleManager` |
|
|
43
|
+
| `test/handlers/tool-start.test.ts` | 2 | `ToolStartRuntime` |
|
|
44
|
+
|
|
45
|
+
### Cast sites by file
|
|
46
|
+
|
|
47
|
+
`service-adapter.test.ts` — the "steer, abort, waitForAll, hasRunning" block's `createDeps` returns `AdapterDeps` directly.
|
|
48
|
+
Five casts reconfigure `getRecord`, `abort`, or `queueSteer` after construction:
|
|
49
|
+
|
|
50
|
+
1. `(deps.manager.abort as ReturnType<typeof vi.fn>).mockReturnValue(false)`
|
|
51
|
+
2. `(deps.manager.getRecord as ReturnType<typeof vi.fn>).mockReturnValue({...})` (×4)
|
|
52
|
+
|
|
53
|
+
`lifecycle.test.ts` — mock objects are assigned to `let` variables in `beforeEach`, typed to `LifecycleRuntime` and `LifecycleManager`.
|
|
54
|
+
Two casts reconfigure methods to track call order:
|
|
55
|
+
|
|
56
|
+
1. `(runtime.setSessionContext as ReturnType<typeof vi.fn>).mockImplementation(...)`
|
|
57
|
+
2. `(manager.clearCompleted as ReturnType<typeof vi.fn>).mockImplementation(...)`
|
|
58
|
+
|
|
59
|
+
Note: the same file already uses `vi.mocked()` in the shutdown-order test — both patterns coexist, which is itself a consistency smell.
|
|
60
|
+
|
|
61
|
+
`tool-start.test.ts` — mock object assigned to a `let` variable typed to `ToolStartRuntime`.
|
|
62
|
+
Two casts reconfigure methods to track call order:
|
|
63
|
+
|
|
64
|
+
1. `(runtime.setUICtx as ReturnType<typeof vi.fn>).mockImplementation(...)`
|
|
65
|
+
2. `(runtime.onTurnStart as ReturnType<typeof vi.fn>).mockImplementation(...)`
|
|
66
|
+
|
|
67
|
+
### Approach: named-variable extraction
|
|
68
|
+
|
|
69
|
+
Extract individual `vi.fn()` stubs into named variables.
|
|
70
|
+
This is the approach the issue recommends and it aligns with the testing skill's guidance on extractable stubs.
|
|
71
|
+
|
|
72
|
+
The alternative — `vi.mocked()` — is already used in `lifecycle.test.ts` for the shutdown-order test and works for hand-built mocks, but is semantically less clean: `vi.mocked()` asserts that a value is already a mock, which is true here but opaque to readers.
|
|
73
|
+
Named variables make the mock-ness explicit at the construction site.
|
|
74
|
+
|
|
75
|
+
For `lifecycle.test.ts`, the named-variable approach also eliminates the inconsistency between the two ordering tests — one currently uses `vi.mocked()` and the other uses casts.
|
|
76
|
+
After this change both will use named stubs.
|
|
77
|
+
|
|
78
|
+
## Design Overview
|
|
79
|
+
|
|
80
|
+
### service-adapter.test.ts
|
|
81
|
+
|
|
82
|
+
Refactor the "steer, abort, waitForAll, hasRunning" block's `createDeps` to return named stubs:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
function createDeps(overrides: Partial<AdapterDeps> = {}) {
|
|
86
|
+
const mockGetRecord = vi.fn<AgentManagerLike["getRecord"]>();
|
|
87
|
+
const mockAbort = vi.fn<AgentManagerLike["abort"]>(() => true);
|
|
88
|
+
const mockQueueSteer = vi.fn<AgentManagerLike["queueSteer"]>(() => true);
|
|
89
|
+
|
|
90
|
+
const deps: AdapterDeps = {
|
|
91
|
+
manager: {
|
|
92
|
+
spawn: vi.fn(() => "id"),
|
|
93
|
+
getRecord: mockGetRecord,
|
|
94
|
+
listAgents: vi.fn(() => []),
|
|
95
|
+
abort: mockAbort,
|
|
96
|
+
waitForAll: vi.fn(async () => {}),
|
|
97
|
+
hasRunning: vi.fn(() => true),
|
|
98
|
+
queueSteer: mockQueueSteer,
|
|
99
|
+
},
|
|
100
|
+
resolveModel: vi.fn(),
|
|
101
|
+
getCtx: () => ({ pi: {}, ctx: {} }),
|
|
102
|
+
getModelRegistry: () => ({ find: () => null, getAll: () => [] }),
|
|
103
|
+
...overrides,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return { deps, mockGetRecord, mockAbort, mockQueueSteer };
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Callers destructure what they need:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const { deps, mockAbort } = createDeps();
|
|
114
|
+
mockAbort.mockReturnValue(false); // ← type-checked, no cast
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### lifecycle.test.ts
|
|
118
|
+
|
|
119
|
+
Promote the `beforeEach`-scoped `runtime` and `manager` mock construction to use named stubs.
|
|
120
|
+
The stubs that need reconfiguration (`setSessionContext`, `clearCompleted`) become named `let` variables alongside the existing `runtime`/`manager` lets, reset in `beforeEach`:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
let mockSetSessionContext: MockInstance<LifecycleRuntime["setSessionContext"]>;
|
|
124
|
+
let mockClearCompleted: MockInstance<LifecycleManager["clearCompleted"]>;
|
|
125
|
+
// ...assigned in beforeEach when building runtime/manager
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Also convert the shutdown-order test's `vi.mocked()` calls to the same pattern for consistency — `unpublishService`, `clearSessionContext`, `abortAll`, `disposeNotifications`, `dispose` all become named stubs.
|
|
129
|
+
|
|
130
|
+
### tool-start.test.ts
|
|
131
|
+
|
|
132
|
+
Same pattern: promote `setUICtx` and `onTurnStart` to named `let` variables:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
let mockSetUICtx: MockInstance<ToolStartRuntime["setUICtx"]>;
|
|
136
|
+
let mockOnTurnStart: MockInstance<ToolStartRuntime["onTurnStart"]>;
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Module-Level Changes
|
|
140
|
+
|
|
141
|
+
| File | Change |
|
|
142
|
+
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
143
|
+
| `test/service-adapter.test.ts` | Refactor `createDeps` in the "steer, abort, waitForAll, hasRunning" block to return named mock stubs alongside `deps`. Update all 5 cast sites to use named stubs. |
|
|
144
|
+
| `test/handlers/lifecycle.test.ts` | Extract `mockSetSessionContext`, `mockClearCompleted`, `mockAbortAll`, `mockDispose`, `mockClearSessionContext` as named `let` variables. Replace 2 casts and 5 `vi.mocked()` calls with named stubs. |
|
|
145
|
+
| `test/handlers/tool-start.test.ts` | Extract `mockSetUICtx` and `mockOnTurnStart` as named `let` variables. Replace 2 casts with named stubs. |
|
|
146
|
+
|
|
147
|
+
No production files are changed.
|
|
148
|
+
|
|
149
|
+
## Test Impact Analysis
|
|
150
|
+
|
|
151
|
+
1. No new tests are added — this is a refactoring of existing test infrastructure.
|
|
152
|
+
2. No tests become redundant — every existing assertion stays.
|
|
153
|
+
3. All existing tests must pass unchanged; only the mock-wiring changes.
|
|
154
|
+
|
|
155
|
+
## TDD Order
|
|
156
|
+
|
|
157
|
+
1. **Commit:** Refactor `createDeps` in `service-adapter.test.ts` to return named stubs; update all 5 cast sites.
|
|
158
|
+
All tests pass before and after.
|
|
159
|
+
Commit: `test: remove vi.fn() cast smell from service-adapter tests (#123)`
|
|
160
|
+
2. **Commit:** Extract named stubs in `lifecycle.test.ts`; replace 2 casts and 5 `vi.mocked()` calls.
|
|
161
|
+
All tests pass.
|
|
162
|
+
Commit: `test: remove vi.fn() cast smell from lifecycle tests (#123)`
|
|
163
|
+
3. **Commit:** Extract named stubs in `tool-start.test.ts`; replace 2 casts.
|
|
164
|
+
All tests pass.
|
|
165
|
+
Commit: `test: remove vi.fn() cast smell from tool-start tests (#123)`
|
|
166
|
+
|
|
167
|
+
Each step is an independent file — order doesn't matter, but one-file-per-commit keeps diffs reviewable.
|
|
168
|
+
|
|
169
|
+
## Risks and Mitigations
|
|
170
|
+
|
|
171
|
+
| Risk | Mitigation |
|
|
172
|
+
| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
173
|
+
| Overrides via `...overrides` in `service-adapter.test.ts` could replace a manager method, leaving the named stub disconnected | Only `manager`-level overrides are spread; individual method overrides aren't used in this block. |
|
|
174
|
+
| Named stubs add return-surface to helpers | Each helper is test-local and the extra names are self-documenting. The alternative (casting) is worse. |
|
|
175
|
+
| Converting `vi.mocked()` in `lifecycle.test.ts` shutdown test expands scope slightly beyond the cast pattern | Worth it for consistency — mixing `vi.mocked()` and named stubs in the same file is a different smell. |
|
|
176
|
+
|
|
177
|
+
## Open Questions
|
|
178
|
+
|
|
179
|
+
None — the issue is fully scoped and the approach is established in the codebase.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 111
|
|
3
|
+
issue_title: "refactor(pi-subagents): split AgentRecord lifecycle state into phase-specific objects"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #111 — split AgentRecord lifecycle state into phase-specific objects
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-22T01:50:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned and implemented the `AgentRecord` lifecycle split across 12 TDD cycles plus doc updates, released as `pi-subagents-v6.8.0`.
|
|
13
|
+
Three new phase-specific collaborators (`ExecutionState`, `WorktreeState`, `NotificationState`) replace 9 post-construction mutable fields.
|
|
14
|
+
`pendingSteers` moved to a `Map` on `AgentManager`; stats (`toolUses`, `lifetimeUsage`, `compactionCount`) encapsulated behind mutation methods with read-only getters.
|
|
15
|
+
`AgentRecordInit` trimmed from 19 optional fields to 4.
|
|
16
|
+
|
|
17
|
+
### Observations
|
|
18
|
+
|
|
19
|
+
#### What went well
|
|
20
|
+
|
|
21
|
+
- **Lift-and-shift scaled from 7 files (#110) to 18 files (#111) without any intermediate test breakage.**
|
|
22
|
+
Every commit left all 41 test files passing.
|
|
23
|
+
The pattern — add new alongside old, migrate consumers with fallbacks (`record.execution?.session ?? record.session`), strip fallbacks in a final commit — is reliable for multi-step encapsulation refactors.
|
|
24
|
+
- **Stats encapsulation was simpler than expected.**
|
|
25
|
+
Converting `toolUses`, `lifetimeUsage`, `compactionCount` to private fields with getters and mutation methods required zero changes to read-only consumers because the getter names match the old field names.
|
|
26
|
+
Only `record-observer.ts` (the sole writer) needed updating.
|
|
27
|
+
- **The `createTestRecord` factory intersection type trick preserved backward compatibility.**
|
|
28
|
+
The factory accepts `toolUses?: number` via `Partial<AgentRecordInit> & { toolUses?: number; ... }` and internally calls `record.incrementToolUses()` in a loop.
|
|
29
|
+
This let 10+ test files continue passing `toolUses: 5` without rewriting each to call mutation methods directly.
|
|
30
|
+
- **`Promise.withResolvers` timing analysis in the plan was unnecessary.**
|
|
31
|
+
The plan spent ~40 lines analyzing whether `promise` should live inside `ExecutionState` and concluded it should stay separate.
|
|
32
|
+
Implementation confirmed: `record.execution` is set in `onSessionCreated` (async callback), `record.promise` is set after `runner.run()` (synchronous return) — different moments, straightforward.
|
|
33
|
+
|
|
34
|
+
#### What caused friction (agent side)
|
|
35
|
+
|
|
36
|
+
- `missing-context` — In the step 7 test for `record.execution`, the initial mock runner used `mockResolvedValue(...)` which doesn't call `onSessionCreated`, so `record.execution` stayed `undefined`.
|
|
37
|
+
Had to switch to `mockImplementation(async (..., opts) => { opts.onSessionCreated?.(session); ... })`.
|
|
38
|
+
The existing tests in the same file already use this pattern for record-observer tests, but I didn't check them first.
|
|
39
|
+
Impact: one test rewrite (~2 minutes), no rework to production code.
|
|
40
|
+
- `scope-drift` — Step 4 absorbed step 5 (adding collaborator fields) without noting the merge in the commit or session log.
|
|
41
|
+
Step 5 became a no-op.
|
|
42
|
+
Impact: no rework, but the session narrative skipped a plan step without explanation.
|
|
43
|
+
- `wrong-abstraction` — Step 12 was planned as a simple cleanup ("remove old fields and trim `AgentRecordInit`") but required coordinated changes across 18 files: removing 9 fields from `AgentRecordInit`, updating the `createTestRecord` factory, fixing 5 test files that passed removed fields, and stripping all fallback patterns.
|
|
44
|
+
This was 2-3 steps' worth of work compressed into one.
|
|
45
|
+
Impact: step 12 took significantly longer than other steps, though it landed cleanly.
|
|
46
|
+
- `missing-context` — Did not proactively flag the `as ReturnType<typeof vi.fn>` cast smell in `service-adapter.test.ts` while migrating that file.
|
|
47
|
+
The user noticed it and asked about it.
|
|
48
|
+
Filed as #123.
|
|
49
|
+
Impact: added friction but no rework; follow-up issue created.
|
|
50
|
+
User-caught.
|
|
51
|
+
|
|
52
|
+
#### What caused friction (user side)
|
|
53
|
+
|
|
54
|
+
- No material friction observed.
|
|
55
|
+
The user's `ask_user` decisions during planning (NotificationState collaborator, Map on AgentManager) gave clear direction.
|
|
56
|
+
Quick "follow-up" response on the cast smell kept scope tight.
|
|
57
|
+
|
|
58
|
+
### Changes made
|
|
59
|
+
|
|
60
|
+
1. `packages/pi-subagents/docs/retro/0111-split-agent-record-lifecycle.md` — this retro file.
|
|
61
|
+
2. `.pi/skills/testing/SKILL.md` — added field-removal rule symmetric to the existing field-addition rule (esbuild silent pass-through on unknown init properties).
|
|
@@ -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
package/src/agent-manager.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
});
|