@gotgenes/pi-subagents 13.0.0 → 13.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/public.d.ts +1 -1
- package/docs/architecture/architecture.md +80 -43
- package/docs/plans/0265-born-complete-subagent-session.md +330 -0
- package/docs/plans/0277-encapsulate-agent-session.md +304 -0
- package/docs/retro/0264-remove-extension-lifecycle-control.md +41 -0
- package/docs/retro/0265-born-complete-subagent-session.md +97 -0
- package/docs/retro/0277-encapsulate-agent-session.md +39 -0
- package/package.json +1 -1
- package/src/index.ts +3 -3
- package/src/lifecycle/agent-manager.ts +11 -10
- package/src/lifecycle/agent.ts +99 -59
- package/src/lifecycle/create-subagent-session.ts +242 -0
- package/src/lifecycle/subagent-session.ts +234 -0
- package/src/lifecycle/turn-limits.ts +13 -0
- package/src/observation/notification.ts +2 -2
- package/src/runtime.ts +1 -1
- package/src/service/service-adapter.ts +1 -7
- package/src/session/conversation.ts +49 -0
- package/src/session/session-config.ts +8 -8
- package/src/settings.ts +1 -1
- package/src/tools/agent-tool.ts +1 -1
- package/src/tools/background-spawner.ts +4 -3
- package/src/tools/foreground-runner.ts +4 -3
- package/src/tools/get-result-tool.ts +4 -8
- package/src/tools/spawn-config.ts +1 -1
- package/src/tools/steer-tool.ts +7 -13
- package/src/ui/agent-menu.ts +1 -3
- package/src/ui/conversation-viewer.ts +5 -10
- package/src/lifecycle/agent-runner.ts +0 -464
- package/src/lifecycle/execution-state.ts +0 -17
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 277
|
|
3
|
+
issue_title: "Encapsulate AgentSession behind SubagentSession; retire the remaining agent.session reach-throughs"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Encapsulate AgentSession behind SubagentSession
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
Callers outside the `lifecycle/` domain reach through `Agent.session` (which returns the raw SDK `AgentSession` via `this.subagentSession?.session`) and operate on the session object directly.
|
|
11
|
+
This violates Law of Demeter and Tell-Don't-Ask — the missing abstractions are intent-revealing methods on `Agent` and `SubagentSession` that delegate internally.
|
|
12
|
+
|
|
13
|
+
Issue #265 introduced `SubagentSession` and routed the run/resume/dispose path through it.
|
|
14
|
+
The remaining consumer-facing reach-throughs were deferred to this issue.
|
|
15
|
+
|
|
16
|
+
## Goals
|
|
17
|
+
|
|
18
|
+
- Add intent-revealing methods to `Agent` and `SubagentSession` that replace every raw `AgentSession` access outside `lifecycle/`.
|
|
19
|
+
- Collapse the duplicated steer buffer-or-deliver logic into a single `Agent.steer()` method.
|
|
20
|
+
- Narrow the `onSessionCreated` observer callback to stop delivering the raw `AgentSession` to spawners.
|
|
21
|
+
- Remove the `Agent.session` getter.
|
|
22
|
+
- After this change, no production module outside `lifecycle/` references the raw `AgentSession`.
|
|
23
|
+
|
|
24
|
+
## Non-Goals
|
|
25
|
+
|
|
26
|
+
- The `Agent` → `Subagent` class rename — independent of this issue, can land in either order.
|
|
27
|
+
- Changing the public `SubagentsService` API surface in `service/service.ts` — it already uses `SubagentRecord` (no live session objects).
|
|
28
|
+
- Refactoring the conversation viewer's rich-rendering internals — only its session reference changes.
|
|
29
|
+
- Architecture doc updates for the file-layout listing — `SubagentSession` and `Agent` files are not being added, moved, or removed.
|
|
30
|
+
|
|
31
|
+
## Background
|
|
32
|
+
|
|
33
|
+
### Affected reach-through sites
|
|
34
|
+
|
|
35
|
+
Every site reaches the raw `AgentSession` exposed by the `Agent.session` getter.
|
|
36
|
+
|
|
37
|
+
| Reach-through | Production files | Current pattern |
|
|
38
|
+
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
39
|
+
| Steer buffer-or-deliver (duplicated) | `service/service-adapter.ts:93`, `tools/steer-tool.ts:43` | `record.session` → `session.steer()` or `record.queueSteer()` |
|
|
40
|
+
| Context-percent stats | `tools/get-result-tool.ts:56`, `tools/steer-tool.ts:57`, `observation/notification.ts:49`, `ui/conversation-viewer.ts:145` | `getSessionContextPercent(record.session)` |
|
|
41
|
+
| Conversation viewing | `tools/get-result-tool.ts:83-84`, `ui/agent-menu.ts:255`, `ui/conversation-viewer.ts:63,223` | `getAgentConversation(record.session)` / `session.messages` |
|
|
42
|
+
| Session-readiness guard | `tools/agent-tool.ts:111`, `lifecycle/agent-manager.ts:205` | `!record.session` / `!agent?.session` |
|
|
43
|
+
| Observer callback delivers raw session | `tools/background-spawner.ts:56`, `tools/foreground-runner.ts:111` | `onSessionCreated(_agent, session)` → `tracker.setSession(session)` + `subscribeUIObserver(session, tracker)` |
|
|
44
|
+
| Activity-tracker session stats | `ui/widget-renderer.ts:106` | `activity?.session` → `getSessionContextPercent(activity.session)` (via `AgentActivityTracker.session`) |
|
|
45
|
+
|
|
46
|
+
### Existing delegate methods
|
|
47
|
+
|
|
48
|
+
`SubagentSession` already has `steer(message)`, `runTurnLoop()`, `resumeTurnLoop()`, and `dispose()`.
|
|
49
|
+
The `session` getter is marked "retired by #277" in its JSDoc.
|
|
50
|
+
|
|
51
|
+
`subscribeAgentObserver` in `observation/record-observer.ts` already accepts `SubscribableSession` (not `AgentSession`), so it can accept `SubagentSession` directly once `SubagentSession` implements the `subscribe()` delegate.
|
|
52
|
+
|
|
53
|
+
### Public API surface
|
|
54
|
+
|
|
55
|
+
The public API (`SubagentsService` + `SubagentRecord` in `service/service.ts`) does not expose `Agent` or `AgentSession`.
|
|
56
|
+
Removing `Agent.session` is not a breaking change for cross-extension consumers.
|
|
57
|
+
|
|
58
|
+
## Design Overview
|
|
59
|
+
|
|
60
|
+
### New delegate methods on `SubagentSession`
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// Delegates to getAgentConversation(this._session)
|
|
64
|
+
getConversation(): string
|
|
65
|
+
|
|
66
|
+
// Delegates to getSessionContextPercent(this._session)
|
|
67
|
+
getContextPercent(): number | null
|
|
68
|
+
|
|
69
|
+
// Delegates to this._session.subscribe(fn) — satisfies SubscribableSession
|
|
70
|
+
subscribe(fn: (event: AgentSessionEvent) => void): () => void
|
|
71
|
+
|
|
72
|
+
// Delegates to this._session.getSessionStats() — satisfies SessionLike
|
|
73
|
+
getSessionStats(): SessionStatsLike
|
|
74
|
+
|
|
75
|
+
// Delegates to this._session.messages
|
|
76
|
+
get messages(): readonly unknown[]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
With `subscribe()` and `getSessionStats()`, `SubagentSession` structurally satisfies both `SubscribableSession` and `SessionLike`.
|
|
80
|
+
This lets spawners pass a `SubagentSession` directly to `subscribeUIObserver()` and `tracker.setSession()` without exposing the raw `AgentSession`.
|
|
81
|
+
|
|
82
|
+
### New intent-revealing methods on `Agent`
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// Buffer-or-deliver: returns true if delivered, false if buffered
|
|
86
|
+
async steer(message: string): Promise<boolean>
|
|
87
|
+
|
|
88
|
+
// Returns true when a SubagentSession is available
|
|
89
|
+
isSessionReady(): boolean
|
|
90
|
+
|
|
91
|
+
// Delegates to SubagentSession.getConversation(), returns undefined if no session
|
|
92
|
+
getConversation(): string | undefined
|
|
93
|
+
|
|
94
|
+
// Delegates to SubagentSession.getContextPercent(), returns null if no session
|
|
95
|
+
getContextPercent(): number | null
|
|
96
|
+
|
|
97
|
+
// Delegates to SubagentSession.subscribe() for conversation-viewer live updates
|
|
98
|
+
subscribeToUpdates(fn: (event: AgentSessionEvent) => void): (() => void) | undefined
|
|
99
|
+
|
|
100
|
+
// Delegates to SubagentSession.messages for conversation-viewer rendering
|
|
101
|
+
get messages(): readonly unknown[]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The `steer()` method replaces the duplicated buffer-or-deliver logic:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
async steer(message: string): Promise<boolean> {
|
|
108
|
+
if (!this.subagentSession) {
|
|
109
|
+
this.queueSteer(message);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
await this.subagentSession.steer(message);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Observer callback narrowing
|
|
118
|
+
|
|
119
|
+
`AgentLifecycleObserver.onSessionCreated` changes from `(agent: Agent, session: AgentSession)` to `(agent: Agent)`.
|
|
120
|
+
Spawners access `agent.subagentSession!` (already public) which satisfies `SubscribableSession & SessionLike`:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// tools/background-spawner.ts — after
|
|
124
|
+
onSessionCreated: (agent) => {
|
|
125
|
+
const sub = agent.subagentSession!;
|
|
126
|
+
bgState.setSession(sub); // SubagentSession satisfies SessionLike
|
|
127
|
+
subscribeUIObserver(sub, bgState); // SubagentSession satisfies SubscribableSession
|
|
128
|
+
},
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Internal lifecycle wiring
|
|
132
|
+
|
|
133
|
+
`Agent.run()` already passes the session to `subscribeAgentObserver()`, which accepts `SubscribableSession`.
|
|
134
|
+
After the change, it passes `this.subagentSession` directly instead of `this.subagentSession.session`:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// lifecycle/agent.ts — Agent.run(), after
|
|
138
|
+
this.attachObserver(subscribeAgentObserver(this.subagentSession, this, { ... }));
|
|
139
|
+
this.observer?.onSessionCreated?.(this);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Removals
|
|
143
|
+
|
|
144
|
+
- `Agent.session` getter — removed entirely.
|
|
145
|
+
- `SubagentSession.session` getter — marked `@internal`, kept for lifecycle-internal uses (the `getLastAssistantText()` private helper reads `this._session.messages`).
|
|
146
|
+
- `Agent.queueSteer()` — becomes private (called only from `Agent.steer()` and `Agent.flushPendingSteers()`).
|
|
147
|
+
- `Agent.flushPendingSteers()` — becomes private (called only from `Agent.run()`).
|
|
148
|
+
|
|
149
|
+
## Module-Level Changes
|
|
150
|
+
|
|
151
|
+
### `src/lifecycle/subagent-session.ts`
|
|
152
|
+
|
|
153
|
+
- Add `getConversation()`, `getContextPercent()`, `subscribe()`, `getSessionStats()`, `messages` getter.
|
|
154
|
+
- Import `getAgentConversation` from `#src/session/conversation` and `getSessionContextPercent`, `SessionStatsLike` from `#src/lifecycle/usage`.
|
|
155
|
+
- Mark the `session` getter with `@internal` JSDoc.
|
|
156
|
+
|
|
157
|
+
### `src/lifecycle/agent.ts`
|
|
158
|
+
|
|
159
|
+
- Add `steer()`, `isSessionReady()`, `getConversation()`, `getContextPercent()`, `subscribeToUpdates()`, `messages` getter.
|
|
160
|
+
- Remove the `session` getter.
|
|
161
|
+
- Make `queueSteer()` and `flushPendingSteers()` private.
|
|
162
|
+
- In `run()`: pass `this.subagentSession` (not `this.subagentSession.session`) to `subscribeAgentObserver()` and fire `onSessionCreated(this)` without the session param.
|
|
163
|
+
- In `resume()`: pass `subagentSession` (not `subagentSession.session`) to `subscribeAgentObserver()`.
|
|
164
|
+
- Remove the `AgentSession` import.
|
|
165
|
+
- Update `AgentLifecycleObserver.onSessionCreated` to `(agent: Agent)` — no session param.
|
|
166
|
+
|
|
167
|
+
### `src/service/service-adapter.ts`
|
|
168
|
+
|
|
169
|
+
- Replace the 6-line steer reach-through with `await record.steer(message)`.
|
|
170
|
+
- Remove the `!session` guard — `Agent.steer()` owns it.
|
|
171
|
+
|
|
172
|
+
### `src/tools/steer-tool.ts`
|
|
173
|
+
|
|
174
|
+
- Replace the buffer-or-deliver dance with `const delivered = await record.steer(message)`.
|
|
175
|
+
- Use `record.getContextPercent()` instead of `getSessionContextPercent(session)`.
|
|
176
|
+
- Remove the `getSessionContextPercent` import.
|
|
177
|
+
|
|
178
|
+
### `src/tools/get-result-tool.ts`
|
|
179
|
+
|
|
180
|
+
- Use `record.getContextPercent()` instead of `getSessionContextPercent(record.session)`.
|
|
181
|
+
- Use `record.getConversation()` instead of `getAgentConversation(record.session)`.
|
|
182
|
+
- Remove the `getSessionContextPercent` and `getAgentConversation` imports.
|
|
183
|
+
|
|
184
|
+
### `src/tools/agent-tool.ts`
|
|
185
|
+
|
|
186
|
+
- Use `existing.isSessionReady()` instead of `!existing.session`.
|
|
187
|
+
|
|
188
|
+
### `src/lifecycle/agent-manager.ts`
|
|
189
|
+
|
|
190
|
+
- Use `agent.isSessionReady()` instead of `agent?.session`.
|
|
191
|
+
- Update the `onSessionCreated` relay to `(agent) => options.observer!.onSessionCreated!(agent)` — no session param.
|
|
192
|
+
|
|
193
|
+
### `src/observation/notification.ts`
|
|
194
|
+
|
|
195
|
+
- Use `record.getContextPercent()` instead of `getSessionContextPercent(record.session)`.
|
|
196
|
+
- Remove the `getSessionContextPercent` import.
|
|
197
|
+
|
|
198
|
+
### `src/ui/conversation-viewer.ts`
|
|
199
|
+
|
|
200
|
+
- Remove `session: AgentSession` from `ConversationViewerOptions`.
|
|
201
|
+
- Remove the `private session: AgentSession` field.
|
|
202
|
+
- Use `this.record.subscribeToUpdates(() => ...)` instead of `session.subscribe(...)`.
|
|
203
|
+
- Use `this.record.messages` instead of `this.session.messages`.
|
|
204
|
+
- Use `this.record.getContextPercent()` instead of `getSessionContextPercent(this.record.session)`.
|
|
205
|
+
- Remove the `AgentSession` and `getSessionContextPercent` imports.
|
|
206
|
+
|
|
207
|
+
### `src/ui/agent-menu.ts`
|
|
208
|
+
|
|
209
|
+
- Use `record.isSessionReady()` instead of `record.session` check.
|
|
210
|
+
- Remove the `session` variable and stop passing it to `ConversationViewer`.
|
|
211
|
+
|
|
212
|
+
### `src/tools/background-spawner.ts`
|
|
213
|
+
|
|
214
|
+
- Update `onSessionCreated` callback: `(agent) => { const sub = agent.subagentSession!; ... }`.
|
|
215
|
+
- Pass `sub` instead of `session` to `bgState.setSession()` and `subscribeUIObserver()`.
|
|
216
|
+
|
|
217
|
+
### `src/tools/foreground-runner.ts`
|
|
218
|
+
|
|
219
|
+
- Same pattern as `background-spawner.ts`.
|
|
220
|
+
|
|
221
|
+
### `test/helpers/mock-session.ts`
|
|
222
|
+
|
|
223
|
+
- Update `createSubagentSessionStub` to include the new delegate methods (`getConversation`, `getContextPercent`, `subscribe`, `getSessionStats`, `messages`).
|
|
224
|
+
- The stub's `subscribe` can delegate to the underlying mock session's `subscribe`.
|
|
225
|
+
|
|
226
|
+
### Test files requiring updates
|
|
227
|
+
|
|
228
|
+
- `test/lifecycle/subagent-session.test.ts` — add tests for new delegate methods.
|
|
229
|
+
- `test/lifecycle/agent.test.ts` — add tests for `steer()`, `isSessionReady()`, `getConversation()`, `getContextPercent()`; update `queueSteer` and `flushPendingSteers` tests (now private, tested through `steer()`); update `session` getter tests to `isSessionReady()`.
|
|
230
|
+
- `test/service/service-adapter.test.ts` — update steer tests (no more session reach-through).
|
|
231
|
+
- `test/tools/steer-tool.test.ts` — update to use `Agent.steer()` semantics.
|
|
232
|
+
- `test/tools/get-result-tool.test.ts` — update context-percent and conversation assertions.
|
|
233
|
+
- `test/tools/agent-tool.test.ts` — update resume guard to use `isSessionReady()`.
|
|
234
|
+
- `test/tools/background-spawner.test.ts` — update `onSessionCreated` callback assertions.
|
|
235
|
+
- `test/tools/foreground-runner.test.ts` — update `onSessionCreated` callback assertions.
|
|
236
|
+
- `test/observation/notification.test.ts` — update context-percent assertions.
|
|
237
|
+
- `test/conversation-viewer.test.ts` — remove `session` from viewer options, update to use `record` methods.
|
|
238
|
+
- `test/ui/agent-menu.test.ts` — update session guard assertions.
|
|
239
|
+
- `test/lifecycle/agent-manager.test.ts` — update resume guard + observer relay tests.
|
|
240
|
+
|
|
241
|
+
## Test Impact Analysis
|
|
242
|
+
|
|
243
|
+
1. **New unit tests enabled:** Direct testing of `SubagentSession.getConversation()`, `getContextPercent()`, `subscribe()`, `getSessionStats()`, `messages` — these were previously only testable by reaching through the raw session.
|
|
244
|
+
Direct testing of `Agent.steer()` buffer-or-deliver logic — previously scattered across consumer tests.
|
|
245
|
+
2. **Existing tests that become simpler:** `steer-tool.test.ts` and `service-adapter.test.ts` no longer need to set up the session-present/session-absent dance — they test through `Agent.steer()`.
|
|
246
|
+
`get-result-tool.test.ts` no longer needs mock sessions for context-percent assertions.
|
|
247
|
+
3. **Existing tests that stay as-is:** `SubagentSession.runTurnLoop()` and `resumeTurnLoop()` tests — they exercise turn driving, which is unrelated.
|
|
248
|
+
`Agent.run()` integration tests — they verify the full lifecycle including observer wiring.
|
|
249
|
+
|
|
250
|
+
## TDD Order
|
|
251
|
+
|
|
252
|
+
1. Add delegate methods to `SubagentSession` (`getConversation`, `getContextPercent`, `subscribe`, `getSessionStats`, `messages`).
|
|
253
|
+
Update `createSubagentSessionStub` test helper to include the new methods.
|
|
254
|
+
Test: unit tests for each delegate method in `subagent-session.test.ts`.
|
|
255
|
+
Commit: `feat: add delegate methods to SubagentSession for session encapsulation (#277)`.
|
|
256
|
+
|
|
257
|
+
2. Add intent-revealing methods to `Agent` (`steer`, `isSessionReady`, `getConversation`, `getContextPercent`, `subscribeToUpdates`, `messages`).
|
|
258
|
+
Test: unit tests for each method in `agent.test.ts`.
|
|
259
|
+
Commit: `feat: add session-encapsulation methods to Agent (#277)`.
|
|
260
|
+
|
|
261
|
+
3. Migrate steer callers: replace the buffer-or-deliver reach-through in `service-adapter.ts` and `steer-tool.ts` with `record.steer()`.
|
|
262
|
+
Replace `getSessionContextPercent(session)` in `steer-tool.ts` with `record.getContextPercent()`.
|
|
263
|
+
Make `queueSteer()` and `flushPendingSteers()` private on `Agent`.
|
|
264
|
+
Update tests in `service-adapter.test.ts`, `steer-tool.test.ts`, `agent.test.ts` (the `queueSteer`/`flushPendingSteers` tests become tests for `Agent.steer()`; existing tests from step 2 may cover this).
|
|
265
|
+
Commit: `refactor: use Agent.steer for buffer-or-deliver (#277)`.
|
|
266
|
+
|
|
267
|
+
4. Migrate context-percent, conversation, and readiness callers: `get-result-tool.ts`, `agent-tool.ts`, `agent-manager.ts`, `notification.ts`.
|
|
268
|
+
Update corresponding test files.
|
|
269
|
+
Commit: `refactor: replace session reach-throughs in tools and observation (#277)`.
|
|
270
|
+
|
|
271
|
+
5. Refactor conversation viewer and agent-menu: remove `session: AgentSession` from `ConversationViewerOptions`, use `record` methods for subscribe/messages/contextPercent.
|
|
272
|
+
Update `conversation-viewer.test.ts` and `agent-menu.test.ts`.
|
|
273
|
+
Commit: `refactor: remove raw session from conversation viewer (#277)`.
|
|
274
|
+
|
|
275
|
+
6. Narrow `onSessionCreated` callback: change `AgentLifecycleObserver.onSessionCreated` to `(agent: Agent)` — no session param.
|
|
276
|
+
Update `Agent.run()` and `Agent.resume()` to pass `SubagentSession` (not raw session) to `subscribeAgentObserver`.
|
|
277
|
+
Update spawners (`background-spawner.ts`, `foreground-runner.ts`) and `agent-manager.ts` relay.
|
|
278
|
+
Update corresponding test files.
|
|
279
|
+
Commit: `refactor: narrow onSessionCreated to hide raw AgentSession (#277)`.
|
|
280
|
+
|
|
281
|
+
7. Remove `Agent.session` getter.
|
|
282
|
+
Mark `SubagentSession.session` getter as `@internal`.
|
|
283
|
+
Remove `AgentSession` import from `agent.ts`.
|
|
284
|
+
Update any remaining tests that reference `Agent.session` (use `agent.isSessionReady()` or `agent.subagentSession`).
|
|
285
|
+
Verify with `pnpm run check` and `pnpm -r run test`.
|
|
286
|
+
Commit: `refactor: remove Agent.session getter (#277)`.
|
|
287
|
+
|
|
288
|
+
8. Update the architecture doc's "Session encapsulation debt" section to reflect completion.
|
|
289
|
+
Commit: `docs: mark session encapsulation debt resolved (#277)`.
|
|
290
|
+
|
|
291
|
+
## Risks and Mitigations
|
|
292
|
+
|
|
293
|
+
| Risk | Mitigation |
|
|
294
|
+
| --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
295
|
+
| `SubagentSession` might not structurally satisfy `SubscribableSession` or `SessionLike` | Verify with `pnpm run check` after step 1 — the delegate methods must match the expected signatures exactly. |
|
|
296
|
+
| Making `queueSteer`/`flushPendingSteers` private breaks tests | Step 3 explicitly migrates those tests to exercise `Agent.steer()` instead. |
|
|
297
|
+
| The conversation viewer's `messages` accessor returns `readonly unknown[]` which loses typing | The viewer already casts messages to `{ role: string; [key: string]: unknown }` — the cast is unchanged, and the viewer's rendering is unaffected. |
|
|
298
|
+
| Narrowing `onSessionCreated` could break the `agent-manager.ts` relay's non-null assertion | The relay checks `options.observer?.onSessionCreated` before wiring — the narrowed signature is structurally compatible; the only change is dropping the second parameter. |
|
|
299
|
+
| Large number of test files to update | Steps 3–6 group migrations by concern (steer, stats/conversation/readiness, viewer, observer) so each commit is self-contained and reviewable. |
|
|
300
|
+
|
|
301
|
+
## Open Questions
|
|
302
|
+
|
|
303
|
+
- None — the issue's proposed methods are unambiguous and the acceptance criteria are clear.
|
|
304
|
+
The only design addition beyond the issue is narrowing `onSessionCreated` and adding `messages`/`subscribeToUpdates` for the conversation viewer, both of which are required to satisfy the "no production module outside lifecycle/ references AgentSession" acceptance criterion.
|
|
@@ -46,3 +46,44 @@ Test count went 1016 → 951 (−65): deleted `skill-loader.test.ts` and `safe-f
|
|
|
46
46
|
- The `spawn-config.test.ts` `agentInvocation` snapshot carried a stale `isolation: undefined` leftover (from the #263 worktree eviction) that `toEqual` had been silently ignoring; removed it alongside `isolated: false` for a clean exact-match assertion.
|
|
47
47
|
- `verify:public-types` confirmed the breaking `SpawnOptions.isolated` removal type-checks against an external consumer; no lockfile changes; `dist/` correctly gitignored after the type-bundle build.
|
|
48
48
|
- Pre-completion reviewer: **PASS** — both acceptance criteria code-verified, all deterministic checks green, 6 Mermaid diagrams render, docs accurate, zero dead code.
|
|
49
|
+
|
|
50
|
+
## Stage: Final Retrospective (2026-05-30T01:13:26Z)
|
|
51
|
+
|
|
52
|
+
### Session summary
|
|
53
|
+
|
|
54
|
+
Shipped #264 end-to-end across three stages (Planning → TDD → Ship) in one conversation: planned the four-cycle removal, implemented it as three `feat!:` commits plus docs, and released `pi-subagents-v13.0.0` via the release-please PR (#276).
|
|
55
|
+
The run had zero rework and a PASS pre-completion review; test count moved 1016 → 951.
|
|
56
|
+
|
|
57
|
+
### Observations
|
|
58
|
+
|
|
59
|
+
#### What went well
|
|
60
|
+
|
|
61
|
+
- The planning-stage `ask-user` gate caught a real scope trap before any code was written.
|
|
62
|
+
The issue named three fields (`isolated`, `extensions: false`, `noSkills`), but `noSkills` turned out to be the single mechanism behind **two** skill-restriction modes (skill-disable and `skills: string[]` preload).
|
|
63
|
+
Removing it while keeping `AgentConfig.skills` would have left a field that silently stops restricting — a mid-cycle-3 wall.
|
|
64
|
+
Catching it at planning time expanded the clean scope to four collapsing fields and produced a symmetric, reviewable plan.
|
|
65
|
+
- The four-cycle split (`isolated` → `extensions` + unconditional guard → `skills`/`noSkills`/preload → docs) held with no cross-cycle dangling references, validating the lift-and-shift discipline for shared-interface removal.
|
|
66
|
+
- Verification ran incrementally (`pnpm run check` + `pnpm run test` after every cycle), so each commit landed green; the only end-of-run surprise was a pre-commit `end-of-file-fixer` hook touching `custom-agents.ts` (handled with a re-add, no rework).
|
|
67
|
+
|
|
68
|
+
#### What caused friction (agent side)
|
|
69
|
+
|
|
70
|
+
- `other` (tooling) — BSD `sed` on macOS does not support `\|` alternation in basic regex; the first bulk fixture-deletion attempt silently matched nothing.
|
|
71
|
+
Impact: one wasted tool call, no rework; resolved by switching to `sed -E`.
|
|
72
|
+
|
|
73
|
+
#### What caused friction (user side)
|
|
74
|
+
|
|
75
|
+
- The opening sequencing question ("is there work that should precede this") was first read literally as a prerequisite check (#261/#262/#263), and the user had to rephrase to get at the within-issue sequencing point that surfaced the `noSkills`/`skills` coupling.
|
|
76
|
+
Opportunity, not criticism: offering both readings of an ambiguous "what comes first" question up front would have reached the valuable discovery one turn sooner.
|
|
77
|
+
|
|
78
|
+
#### Diagnostic details
|
|
79
|
+
|
|
80
|
+
- **Model-performance correlation** — one subagent dispatch (`pre-completion-reviewer`, 325s, 43 tool uses) on a judgment-heavy review task; appropriate assignment.
|
|
81
|
+
No Explore/Plan subagents were needed — planning-stage exploration was direct file reads and targeted greps, efficient for this removal's scope.
|
|
82
|
+
- **Escalation-delay tracking** — no rabbit-holes; the `sed` issue resolved in one retry.
|
|
83
|
+
- **Feedback-loop gap analysis** — no gap; `check`/`test`/`lint` ran after each cycle, with `fallow dead-code` and `verify:public-types` at the end.
|
|
84
|
+
|
|
85
|
+
### Changes made
|
|
86
|
+
|
|
87
|
+
1. Appended this Final Retrospective stage entry to `packages/pi-subagents/docs/retro/0264-remove-extension-lifecycle-control.md`.
|
|
88
|
+
2. Considered but **declined** (user: "too narrow") a removal-coupling detection rule for `.pi/prompts/plan-issue.md` — the heuristic that a field named for removal may be the mechanism behind a separate surviving feature.
|
|
89
|
+
No prompt or `AGENTS.md` changes were made this retro.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 265
|
|
3
|
+
issue_title: "Born-complete child execution; dissolve the runner"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #265 — Born-complete child execution; dissolve the runner
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-30T02:30:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Produced the implementation plan for dissolving the `agent-runner` and introducing a born-complete `SubagentSession`.
|
|
13
|
+
Most of the session was a design dialogue that resolved naming, the turn-loop home, a discovered Law-of-Demeter cluster, and the workspace-ownership fork before any plan text was written.
|
|
14
|
+
Plan committed as `0265-born-complete-subagent-session.md`; a side-quest filed #277 and added an architecture-doc breadcrumb for discovered debt.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
- Vocabulary was pinned down explicitly because "execution" is overloaded: granular execution = one turn loop (one `session.prompt()`, run or resume); the born-complete object spans the whole session lifetime (run + resumes).
|
|
19
|
+
The object is named `SubagentSession` (matches the existing `SubagentType` / `SubagentSessionDir` / `SubagentSessionRegistry` family; cohesive with the deferred `Agent` → `Subagent` rename).
|
|
20
|
+
Turn driving is `runTurnLoop` / `resumeTurnLoop`; resume is *not* an SDK `session.resume()` — it is `session.prompt()` again on the retained session.
|
|
21
|
+
- The turn-loop home is **on `SubagentSession`** (methods), not inline on `Agent` and not a free function.
|
|
22
|
+
The user caught that `subagent.driveTurnLoop(subagentSession.session, …)` is a Law-of-Demeter reach-through; putting the behavior on the object that owns the `AgentSession` is both LoD-correct and more testable (satisfying the user's conditional "inline only if straightforward to test").
|
|
23
|
+
- Workspace ownership locked to **Option A** (session-only `SubagentSession`; `Agent` keeps workspace prepare/dispose).
|
|
24
|
+
Decisive reasoning: the workspace and the session have genuinely different lifetimes (workspace dies at run-completion to fold its `resultAddendum` into the result; session survives to cleanup for resume + the new registry boundary), so they are different resources.
|
|
25
|
+
Option B would fuse them into one object needing two teardown methods, and would thread the `WorkspaceProvider` + prepare-context through the factory just to call `prepare()` — a parameter-relay smell the user flagged.
|
|
26
|
+
The factory takes a resolved `cwd` value (used directly), never the provider.
|
|
27
|
+
- Worktrees are already out of the core (#263) — confirmed zero git code in `pi-subagents/src/` (only doc comments).
|
|
28
|
+
The A/B fork is purely about how the core sequences its abstract `WorkspaceProvider` seam; `@gotgenes/pi-subagents-worktrees` is untouched.
|
|
29
|
+
- Registry semantics: moving `disposed` from run-completion to true session disposal makes resume executions registry-detected (closes the gap deferred from #261).
|
|
30
|
+
The permission system's subscription code does not change; only *when* `disposed` fires moves.
|
|
31
|
+
Edge case planned: `createSubagentSession` must dispose on a post-`session-created` failure to avoid a registry leak.
|
|
32
|
+
- Discovered debt captured (the user's "it is in doing the work that we discover the work to be done"): filed #277 for the remaining `agent.session` reach-throughs (steer buffer-or-deliver duplicated across `steer-tool` + `service-adapter`, conversation viewing, resume-readiness guards) and added a "Session encapsulation debt (Law of Demeter)" subsection to `architecture.md` (commit `038a1283`).
|
|
33
|
+
`SubagentSession` exposes a `.session` accessor in #265 so observer wiring + consumers keep working; #277 retires those.
|
|
34
|
+
- Two follow-ups deliberately deferred and noted in the plan's Non-Goals / Open Questions: the `Agent` → `Subagent` class rename (mechanical, ~19 files — separate issue) and resume-aware workspaces (a worktree's lifetime is one turn loop; worktree + resume is degenerate today).
|
|
35
|
+
- The change is non-breaking (no `feat!:`): the dissolved types (`RunOptions`, `RunResult`, `AgentRunner`) are internal, so `public.d.ts` is unaffected.
|
|
36
|
+
TDD order uses lift-and-shift across 7 steps to keep each commit compiling; transient duplication of the turn-loop helpers/assembly exists between steps 3–5 and is deleted in step 6.
|
|
37
|
+
|
|
38
|
+
## Stage: Implementation — TDD (2026-05-29T22:18:00Z)
|
|
39
|
+
|
|
40
|
+
### Session summary
|
|
41
|
+
|
|
42
|
+
Executed all 7 TDD steps from the plan via lift-and-shift, one commit per step, each leaving the suite green.
|
|
43
|
+
Introduced `SubagentSession` (`runTurnLoop`/`resumeTurnLoop`/`steer`/`dispose`) and the `createSubagentSession()` assembly factory, swapped `Agent`/`AgentManager`/`index.ts` onto them, then deleted `agent-runner.ts` + `execution-state.ts` and the three runner test files.
|
|
44
|
+
Package test count went 951 → 960 (net +9: new `subagent-session`/`create-subagent-session`/`turn-limits` suites added, the redundant runner suites deleted).
|
|
45
|
+
Pre-completion reviewer: initial FAIL (MD060 table alignment in SKILL.md, auto-fixed by `rumdl fmt`), PASS on re-check after fix + stale doc cleanup.
|
|
46
|
+
|
|
47
|
+
### Observations
|
|
48
|
+
|
|
49
|
+
- The plan sketch's `TurnLoopOptions` listed only `maxTurns`/`graceTurns`/`signal`, but preserving the old `runAgent` precedence `per-call ?? agentMaxTurns ?? defaultMaxTurns` required threading `defaultMaxTurns` through `TurnLoopOptions` and storing `agentMaxTurns` + `parentContext` in `SubagentSession` meta (both are session-level facts known at creation).
|
|
50
|
+
This is a correctness-preserving deviation, well covered by three precedence tests plus a parent-context-prepend test in `subagent-session.test.ts`.
|
|
51
|
+
- The atomic call-site swap (step 5) touched more test files than the plan's step-5 list anticipated: every tool/service test that set `record.execution = { session, outputFile }` (`steer-tool`, `agent-tool`, `background-spawner`, `foreground-runner`, `get-result-tool`, `service-adapter`) had to migrate to `record.subagentSession = toSubagentSession(createSubagentSessionStub(...))`.
|
|
52
|
+
Added `createSubagentSessionStub`/`toSubagentSession` to `mock-session.ts` so the migration was a one-line change per call site; the stub's `steer`/`dispose` delegate to the underlying `MockSession` so existing session-spy assertions kept working unchanged.
|
|
53
|
+
- `disposed` moved from `runAgent`'s `finally` (run-completion) to `SubagentSession.dispose()`, invoked by `AgentManager` via the new `Agent.disposeSession()` (routing both `record.session?.dispose?.()` call sites at `agent-manager.ts:235,309`).
|
|
54
|
+
The full cross-package suite confirms the permission system (1504 tests) is unaffected — its subscription code did not change, only *when* `disposed` fires.
|
|
55
|
+
- Test-helper gotcha: `makeSubagentSession`'s `outputFile` default initially swallowed an explicit `undefined` via `?? default`; fixed with an `"outputFile" in metaOverrides` presence check (the testing-skill "Partial spread erases explicit undefined" family).
|
|
56
|
+
- `print-mode.test.ts` now mocks `#src/lifecycle/create-subagent-session` (was `#src/lifecycle/agent-runner`); `index.ts` wraps the factory as `(params) => createSubagentSession(params, deps)`, so the module mock still intercepts it.
|
|
57
|
+
- fallow stayed clean throughout — the transient duplication of IO interfaces + turn-loop helpers between `agent-runner.ts` and the new modules (steps 3–5) was removed in step 6 before the pre-completion gate ran.
|
|
58
|
+
- Reviewer's two minor non-blocking notes: SKILL.md Session-domain count now lists `conversation.ts` but still omits the pre-existing `content-items.ts` (drift predates this issue); `create-subagent-session.ts` keeps an accurate "old runner's runAgent()" provenance comment.
|
|
59
|
+
|
|
60
|
+
## Stage: Final Retrospective (2026-05-30T13:37:00Z)
|
|
61
|
+
|
|
62
|
+
### Session summary
|
|
63
|
+
|
|
64
|
+
Shipped issue #265 (`pi-subagents-v13.1.0`) and ran the retrospective across all three stages (planning, TDD implementation, ship).
|
|
65
|
+
The implementation landed cleanly in 7 TDD commits + 1 docs commit; test count 951 → 960.
|
|
66
|
+
|
|
67
|
+
### Observations
|
|
68
|
+
|
|
69
|
+
#### What went well
|
|
70
|
+
|
|
71
|
+
- The lift-and-shift strategy (new modules alongside old, swap consumers, delete old) kept every intermediate commit compiling and the suite green — zero broken-baseline moments across 7 steps.
|
|
72
|
+
- The `createSubagentSessionStub` pattern (steer/dispose delegate to the wrapped `MockSession`) let 6 tool/service test files migrate with a one-line change each, preserving all existing session-spy assertions.
|
|
73
|
+
- Verification ran incrementally: `pnpm vitest run <file>` after every Red/Green, `pnpm run check` after every interface change, and `pnpm -r run test` (full cross-package) after step 6's deletion to confirm the permission system (1504 tests) was unaffected.
|
|
74
|
+
|
|
75
|
+
#### What caused friction (agent side)
|
|
76
|
+
|
|
77
|
+
1. `missing-context` — The plan's step-5 file list omitted 6 tool/service test files (`steer-tool`, `agent-tool`, `background-spawner`, `foreground-runner`, `get-result-tool`, `service-adapter`) that directly set `record.execution = { session, outputFile }`.
|
|
78
|
+
The existing planning rule ("grep all test files for every removed symbol") was present but was not applied to the renamed `.execution` property — only to removed type imports.
|
|
79
|
+
Impact: step 5 took ~2× expected time; each file was discovered reactively via `tsc --noEmit` errors.
|
|
80
|
+
2. `rabbit-hole` — Step 6's `sed` invocation for import-path renames failed silently on macOS BSD `sed` because the `#` delimiter clashed with `#test/helpers/...` paths. 3 consecutive tool calls were spent diagnosing and retrying before switching to per-file `sed` with `@` delimiters.
|
|
81
|
+
Impact: added ~3 minutes of friction; the `edit` tool would have been safer for targeted, known-file replacements.
|
|
82
|
+
3. `other` (autoformat race) — The `pi-autoformat` extension ran concurrently with `git commit` twice during the docs phase, causing `.git/index.lock` conflicts.
|
|
83
|
+
Recovery was mechanical (remove lock, retry) but required user intervention once.
|
|
84
|
+
Impact: one user prompt to retry; no rework.
|
|
85
|
+
4. `other` (markdown table alignment) — Replacing short table cells with long module lists in SKILL.md broke MD060's compact-table rule.
|
|
86
|
+
The pre-completion reviewer caught it (initial FAIL); `rumdl fmt` auto-fixed it.
|
|
87
|
+
Impact: one amend + re-lint cycle; self-identified after reviewer report.
|
|
88
|
+
|
|
89
|
+
#### What caused friction (user side)
|
|
90
|
+
|
|
91
|
+
- No user-side friction observed.
|
|
92
|
+
The user's only intervention was a retry prompt after the autoformat/git-lock race — a timing issue, not a judgment or context gap.
|
|
93
|
+
|
|
94
|
+
### Changes made
|
|
95
|
+
|
|
96
|
+
1. `.pi/skills/testing/SKILL.md` — added rule about `??` swallowing explicit `undefined` in factory overrides (under "Vitest mock patterns").
|
|
97
|
+
2. `packages/pi-subagents/docs/retro/0265-born-complete-subagent-session.md` — appended Final Retrospective stage entry.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 277
|
|
3
|
+
issue_title: "Encapsulate AgentSession behind SubagentSession; retire the remaining agent.session reach-throughs"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #277 — Encapsulate AgentSession behind SubagentSession
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-30T12:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Produced an 8-step TDD plan covering delegate methods on `SubagentSession`, intent-revealing methods on `Agent`, caller migration across tools/service/UI/observation, observer callback narrowing, and `Agent.session` getter removal.
|
|
13
|
+
The plan extends the issue's three proposed methods with `getContextPercent()`, `subscribeToUpdates()`, and `messages` to fully satisfy the acceptance criterion that no production module outside `lifecycle/` references the raw `AgentSession`.
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- `subscribeAgentObserver` already accepts `SubscribableSession` (not `AgentSession`), so adding `subscribe()` to `SubagentSession` enables passing it directly — no `session` getter needed for observer wiring.
|
|
18
|
+
- The `onSessionCreated` observer callback delivers raw `AgentSession` to spawners in `tools/`.
|
|
19
|
+
The plan narrows it to `(agent: Agent)` and has spawners use `agent.subagentSession!` which structurally satisfies both `SubscribableSession` and `SessionLike` via the new delegate methods.
|
|
20
|
+
- The conversation viewer uses `session.messages` for rendering and `session.subscribe()` for live updates — both require delegate methods beyond the issue's three proposed methods.
|
|
21
|
+
- `queueSteer()` and `flushPendingSteers()` become private after migration, which requires migrating existing tests that call them directly.
|
|
22
|
+
- The public API surface (`SubagentsService` + `SubagentRecord`) is unaffected — `Agent` is internal.
|
|
23
|
+
|
|
24
|
+
## Stage: Implementation — TDD (2026-05-30T15:10:00Z)
|
|
25
|
+
|
|
26
|
+
### Session summary
|
|
27
|
+
|
|
28
|
+
All 8 TDD steps completed in one session. 13 commits landed: 2 `feat:`, 5 `refactor:`, 2 `docs:`, 1 `style:`, plus the earlier plan and retro commits.
|
|
29
|
+
Test count went from 960 → 973 (+13 net: +18 new tests, −5 removed tests for the retired `session` getter and `queueSteer`/`flushPendingSteers` private methods).
|
|
30
|
+
|
|
31
|
+
### Observations
|
|
32
|
+
|
|
33
|
+
- `subscribeAgentObserver` already accepted `SubscribableSession`, so `SubagentSession` (once it grew a `subscribe()` delegate) could be passed directly in `Agent.run()` and `Agent.resume()` — no cast needed.
|
|
34
|
+
- Adding `messages` to `MockSession` interface was a minor unplanned step: the field existed at runtime (via `createMockSession`'s spread) but the interface didn't declare it, causing a type error when `createSubagentSessionStub` tried to forward it.
|
|
35
|
+
- `onSessionCreated` tests in `foreground-runner.test.ts` called the callback with 2 args `(record, mockSess)`.
|
|
36
|
+
After narrowing, the first test case was missing a `subagentSession` setup; fixed by setting `record.subagentSession` before invoking the callback.
|
|
37
|
+
- The `get-result-tool.test.ts` verbose conversation test needed updating: previously built a mock session with messages and passed it directly; now the stub's `getConversation` must return the expected text via `mockReturnValue`.
|
|
38
|
+
- Pre-completion reviewer returned **WARN** for two stale `architecture.md` passages: (1) conversation-viewer description still said "subscribes directly to `AgentSession`"; (2) `Agent` classDiagram still listed `queueSteer`/`flushPendingSteers` as public and omitted the 6 new public members.
|
|
39
|
+
Both fixed in a `docs:` commit before closing.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -24,9 +24,9 @@ import { AgentTypeRegistry } from "#src/config/agent-types";
|
|
|
24
24
|
import { loadCustomAgents } from "#src/config/custom-agents";
|
|
25
25
|
import { SessionLifecycleHandler, ToolStartHandler } from "#src/handlers/index";
|
|
26
26
|
import { AgentManager, type AgentManagerObserver } from "#src/lifecycle/agent-manager";
|
|
27
|
-
import { ConcreteAgentRunner, type RunnerDeps } from "#src/lifecycle/agent-runner";
|
|
28
27
|
import { createChildLifecyclePublisher } from "#src/lifecycle/child-lifecycle";
|
|
29
28
|
import { ConcurrencyQueue } from "#src/lifecycle/concurrency-queue";
|
|
29
|
+
import { createSubagentSession, type SubagentSessionDeps } from "#src/lifecycle/create-subagent-session";
|
|
30
30
|
import { buildParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
31
31
|
import { buildEventData, type NotificationDetails, NotificationManager } from "#src/observation/notification";
|
|
32
32
|
import { createNotificationRenderer } from "#src/observation/renderer";
|
|
@@ -132,7 +132,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
132
132
|
},
|
|
133
133
|
};
|
|
134
134
|
|
|
135
|
-
const
|
|
135
|
+
const subagentSessionDeps: SubagentSessionDeps = {
|
|
136
136
|
io: {
|
|
137
137
|
detectEnv,
|
|
138
138
|
getAgentDir,
|
|
@@ -162,7 +162,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
162
162
|
);
|
|
163
163
|
|
|
164
164
|
const manager = new AgentManager({
|
|
165
|
-
|
|
165
|
+
createSubagentSession: (params) => createSubagentSession(params, subagentSessionDeps),
|
|
166
166
|
baseCwd: process.cwd(),
|
|
167
167
|
observer,
|
|
168
168
|
queue,
|
|
@@ -10,9 +10,10 @@ import { randomUUID } from "node:crypto";
|
|
|
10
10
|
import type { Model } from "@earendil-works/pi-ai";
|
|
11
11
|
import { debugLog } from "#src/debug";
|
|
12
12
|
import { Agent, type AgentLifecycleObserver } from "#src/lifecycle/agent";
|
|
13
|
-
import type { AgentRunner } from "#src/lifecycle/agent-runner";
|
|
14
13
|
import type { ConcurrencyQueue } from "#src/lifecycle/concurrency-queue";
|
|
14
|
+
import type { CreateSubagentSessionParams } from "#src/lifecycle/create-subagent-session";
|
|
15
15
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
16
|
+
import type { SubagentSession } from "#src/lifecycle/subagent-session";
|
|
16
17
|
import type { WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
17
18
|
|
|
18
19
|
import type { RunConfig } from "#src/runtime";
|
|
@@ -28,7 +29,8 @@ export interface AgentManagerObserver {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export interface AgentManagerOptions {
|
|
31
|
-
|
|
32
|
+
/** Assembly factory that produces a born-complete SubagentSession per spawn. */
|
|
33
|
+
createSubagentSession: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
|
|
32
34
|
/** Concurrency queue — owns scheduling, limit checks, and drain logic. */
|
|
33
35
|
queue: ConcurrencyQueue;
|
|
34
36
|
/** Base working directory handed to a workspace provider (the parent cwd). */
|
|
@@ -64,7 +66,7 @@ export class AgentManager {
|
|
|
64
66
|
private agents = new Map<string, Agent>();
|
|
65
67
|
private cleanupInterval: ReturnType<typeof setInterval>;
|
|
66
68
|
private readonly observer?: AgentManagerObserver;
|
|
67
|
-
private readonly
|
|
69
|
+
private readonly createSubagentSession: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
|
|
68
70
|
private readonly queue: ConcurrencyQueue;
|
|
69
71
|
private readonly baseCwd: string;
|
|
70
72
|
private getRunConfig?: () => RunConfig;
|
|
@@ -76,7 +78,7 @@ export class AgentManager {
|
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
constructor(options: AgentManagerOptions) {
|
|
79
|
-
this.
|
|
81
|
+
this.createSubagentSession = options.createSubagentSession;
|
|
80
82
|
this.queue = options.queue;
|
|
81
83
|
this.baseCwd = options.baseCwd;
|
|
82
84
|
this.observer = options.observer;
|
|
@@ -111,7 +113,7 @@ export class AgentManager {
|
|
|
111
113
|
this.observer?.onAgentStarted(agent);
|
|
112
114
|
},
|
|
113
115
|
onSessionCreated: options.observer?.onSessionCreated
|
|
114
|
-
? (agent
|
|
116
|
+
? (agent) => options.observer!.onSessionCreated!(agent)
|
|
115
117
|
: undefined,
|
|
116
118
|
onRunFinished: (agent) => {
|
|
117
119
|
if (options.isBackground) {
|
|
@@ -152,7 +154,7 @@ export class AgentManager {
|
|
|
152
154
|
parentSession: options.parentSession,
|
|
153
155
|
signal: options.signal,
|
|
154
156
|
// Shared deps
|
|
155
|
-
|
|
157
|
+
createSubagentSession: this.createSubagentSession,
|
|
156
158
|
observer: this.buildObserver(options),
|
|
157
159
|
getRunConfig: this.getRunConfig,
|
|
158
160
|
baseCwd: this.baseCwd,
|
|
@@ -200,7 +202,7 @@ export class AgentManager {
|
|
|
200
202
|
signal?: AbortSignal,
|
|
201
203
|
): Promise<Agent | undefined> {
|
|
202
204
|
const agent = this.agents.get(id);
|
|
203
|
-
if (!agent?.
|
|
205
|
+
if (!agent?.isSessionReady()) return undefined;
|
|
204
206
|
await agent.resume(prompt, signal);
|
|
205
207
|
return agent;
|
|
206
208
|
}
|
|
@@ -231,8 +233,7 @@ export class AgentManager {
|
|
|
231
233
|
|
|
232
234
|
/** Dispose a record's session and remove it from the map. */
|
|
233
235
|
private removeRecord(id: string, record: Agent): void {
|
|
234
|
-
|
|
235
|
-
record.session?.dispose?.();
|
|
236
|
+
record.disposeSession();
|
|
236
237
|
this.agents.delete(id);
|
|
237
238
|
}
|
|
238
239
|
|
|
@@ -306,7 +307,7 @@ export class AgentManager {
|
|
|
306
307
|
// Clear queue
|
|
307
308
|
this.queue.clear();
|
|
308
309
|
for (const record of this.agents.values()) {
|
|
309
|
-
record.
|
|
310
|
+
record.disposeSession();
|
|
310
311
|
}
|
|
311
312
|
this.agents.clear();
|
|
312
313
|
}
|