@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
|
|
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,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
|
});
|