@gotgenes/pi-subagents 6.0.0 → 6.1.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 +23 -0
- package/docs/architecture/architecture.md +302 -210
- package/docs/plans/0098-extract-agent-record-state-machine.md +435 -0
- package/docs/plans/0102-consolidate-test-record-factory.md +176 -0
- package/docs/retro/0061-session-format-transcript.md +41 -0
- package/docs/retro/0102-consolidate-test-record-factory.md +30 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +25 -50
- package/src/agent-record.ts +179 -0
- package/src/types.ts +3 -39
|
@@ -1,59 +1,79 @@
|
|
|
1
1
|
# Architecture
|
|
2
2
|
|
|
3
|
-
This document describes the
|
|
3
|
+
This document describes the architecture of the pi-subagents fork: a focused, composable core with a stable API boundary that other extensions can build on.
|
|
4
4
|
|
|
5
5
|
## Design principles
|
|
6
6
|
|
|
7
7
|
1. **Narrow core** — the extension owns agent spawning, execution, and result retrieval.
|
|
8
8
|
Everything else is a consumer.
|
|
9
9
|
2. **Composable by default** — other extensions can spawn agents, observe their lifecycle, and display their state without importing this package directly.
|
|
10
|
-
3. **Typed API boundary** — this package exports a `
|
|
10
|
+
3. **Typed API boundary** — this package exports a `SubagentsService` interface and `Symbol.for()` accessors (`publishSubagentsService` / `getSubagentsService`).
|
|
11
11
|
Consumers declare this package as an optional peer dependency and use dynamic import for compile-time types.
|
|
12
|
-
The runtime bridge is `Symbol.for()` on `globalThis` — no separate API package.
|
|
12
|
+
The runtime bridge is `Symbol.for("@gotgenes/pi-subagents:service")` on `globalThis` — no separate API package.
|
|
13
13
|
4. **No scheduling** — in-process scheduling is removed from the core.
|
|
14
14
|
Scheduling is a separate concern that any extension can implement by calling `spawn()` on the published API.
|
|
15
15
|
5. **UI extraction is deferred** — the widget, conversation viewer, and `/agents` command menu stay in the core for now.
|
|
16
16
|
They are the first candidate for extraction once the API boundary is proven stable.
|
|
17
|
+
6. **Snapshot, don't capture** — mutable parent state (ctx, session, model) is read once at spawn time and frozen into a plain data object.
|
|
18
|
+
No live references survive past the spawn call.
|
|
19
|
+
7. **Subscribe, don't thread** — observation of agent progress uses event subscription on the session, not callback parameters threaded through multiple layers.
|
|
17
20
|
|
|
18
21
|
## Current state
|
|
19
22
|
|
|
20
|
-
The extension is
|
|
21
|
-
The
|
|
23
|
+
The extension is ~6,100 LOC across 35 focused modules with a typed `SubagentsService` API boundary.
|
|
24
|
+
The `index.ts` entry point is ~270 lines; the rest is decomposed into domain modules.
|
|
22
25
|
|
|
23
26
|
```text
|
|
24
|
-
index.ts (
|
|
25
|
-
agent-manager.ts
|
|
26
|
-
agent-runner.ts
|
|
27
|
-
|
|
28
|
-
types.ts
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
27
|
+
index.ts (274 LOC) — entry point, tool registration, event wiring
|
|
28
|
+
agent-manager.ts (499) — lifecycle, concurrency, queue
|
|
29
|
+
agent-runner.ts (512) — session creation, turn loop, tool filtering
|
|
30
|
+
session-config.ts (243) — pure session-config assembler
|
|
31
|
+
agent-types.ts (138) — type registry (defaults + custom .md files)
|
|
32
|
+
types.ts (126) — shared type definitions
|
|
33
|
+
runtime.ts (94) — SubagentRuntime factory (session-scoped state)
|
|
34
|
+
|
|
35
|
+
prompts.ts — system prompt assembly
|
|
36
|
+
context.ts — parent conversation extraction
|
|
37
|
+
memory.ts — persistent MEMORY.md per agent
|
|
38
|
+
skill-loader.ts — preload .pi/skills into prompts
|
|
39
|
+
env.ts — git/platform detection
|
|
40
|
+
|
|
41
|
+
worktree.ts — git worktree isolation
|
|
42
|
+
usage.ts — token usage tracking
|
|
43
|
+
model-resolver.ts — fuzzy model name resolution
|
|
44
|
+
invocation-config.ts — merge tool params with agent config
|
|
45
|
+
session-dir.ts — subagent session directory derivation
|
|
46
|
+
settings.ts — persistent operational settings
|
|
47
|
+
|
|
48
|
+
service.ts — SubagentsService interface + Symbol.for() accessors
|
|
49
|
+
service-adapter.ts — SubagentsService implementation wrapping AgentManager
|
|
50
|
+
|
|
51
|
+
tools/agent-tool.ts — Agent tool definition + execute
|
|
52
|
+
tools/get-result-tool.ts — get_subagent_result tool
|
|
53
|
+
tools/steer-tool.ts — steer_subagent tool
|
|
54
|
+
tools/helpers.ts — shared tool utilities
|
|
55
|
+
|
|
56
|
+
handlers/lifecycle.ts — session_start, session_before_switch, session_shutdown
|
|
57
|
+
handlers/tool-start.ts — tool_execution_start handler
|
|
58
|
+
|
|
59
|
+
notification.ts — completion nudges, custom message renderer
|
|
60
|
+
renderer.ts — notification TUI component
|
|
45
61
|
|
|
46
62
|
ui/agent-widget.ts — above-editor live status widget
|
|
63
|
+
ui/agent-menu.ts — /agents slash command menu
|
|
47
64
|
ui/conversation-viewer.ts — scrollable session overlay
|
|
65
|
+
|
|
66
|
+
default-agents.ts — embedded default agent configs (general-purpose, Explore, Plan)
|
|
67
|
+
custom-agents.ts — user-defined agent .md file loader
|
|
68
|
+
debug.ts — debug logging utility
|
|
48
69
|
```
|
|
49
70
|
|
|
50
71
|
### Coupling today
|
|
51
72
|
|
|
52
|
-
The widget reads agent state by holding a direct reference to `
|
|
53
|
-
|
|
54
|
-
Cross-extension consumers use an ad-hoc RPC layer over `pi.events` (`subagents:rpc:spawn`, `subagents:rpc:stop`, `subagents:rpc:ping`) with per-request reply channels and untyped envelopes.
|
|
73
|
+
The widget reads agent state by holding a direct reference to `SubagentRuntime` and polling a shared mutable `Map<string, AgentActivity>` every 80 ms. The conversation viewer subscribes directly to `AgentSession` objects.
|
|
55
74
|
|
|
56
|
-
|
|
75
|
+
Cross-extension consumers use the typed `SubagentsService` API published via `Symbol.for("@gotgenes/pi-subagents:service")` on `globalThis`.
|
|
76
|
+
The ad-hoc RPC layer and untyped `Symbol.for("pi-subagents:manager")` have been removed.
|
|
57
77
|
|
|
58
78
|
## Target state
|
|
59
79
|
|
|
@@ -62,20 +82,20 @@ There is also a `Symbol.for("pi-subagents:manager")` export on `globalThis` that
|
|
|
62
82
|
│ @gotgenes/pi-subagents (this package) │
|
|
63
83
|
│ │
|
|
64
84
|
│ Exports: │
|
|
65
|
-
│
|
|
66
|
-
│
|
|
67
|
-
│ SubagentRecord, SubagentStatus, LifetimeUsage types
|
|
68
|
-
│
|
|
85
|
+
│ SubagentsService interface │
|
|
86
|
+
│ publishSubagentsService() / getSubagentsService() │
|
|
87
|
+
│ SubagentRecord, SubagentStatus, LifetimeUsage types │
|
|
88
|
+
│ SUBAGENT_EVENTS constants │
|
|
69
89
|
│ │
|
|
70
90
|
│ Core: │
|
|
71
91
|
│ Agent + get_subagent_result + steer_subagent tools │
|
|
72
92
|
│ AgentManager, agent-runner, agent-types │
|
|
73
|
-
│
|
|
93
|
+
│ publishSubagentsService(impl) ← called at init │
|
|
74
94
|
│ │
|
|
75
95
|
│ Internal UI (widget, viewer, /agents menu) │
|
|
76
96
|
│ ← moves to pi-subagents-ui later │
|
|
77
97
|
└──────────────────────┬─────────────────────────────────┘
|
|
78
|
-
│ Symbol.for("pi:service
|
|
98
|
+
│ Symbol.for("@gotgenes/pi-subagents:service")
|
|
79
99
|
│
|
|
80
100
|
┌─────────────────┼──────────────────┐
|
|
81
101
|
│ │ │
|
|
@@ -87,7 +107,7 @@ There is also a `Symbol.for("pi-subagents:manager")` export on `globalThis` that
|
|
|
87
107
|
│ ext) │ └──────────────┘ └──────────────┘
|
|
88
108
|
└─────────┘
|
|
89
109
|
│
|
|
90
|
-
│
|
|
110
|
+
│ getSubagentsService()?.spawn(...)
|
|
91
111
|
│ (optional peer dep + dynamic import for types)
|
|
92
112
|
▼
|
|
93
113
|
```
|
|
@@ -97,10 +117,13 @@ There is also a `Symbol.for("pi-subagents:manager")` export on `globalThis` that
|
|
|
97
117
|
- The three tools: `Agent`, `get_subagent_result`, `steer_subagent`.
|
|
98
118
|
- `AgentManager` — spawn, queue, abort, resume, concurrency control.
|
|
99
119
|
- `agent-runner` — session creation, turn loop, tool filtering, extension binding (Patches 2 and 3).
|
|
120
|
+
- `session-config` — pure configuration assembler (extracted from `agent-runner`).
|
|
121
|
+
- `SubagentRuntime` — session-scoped state bag with methods.
|
|
100
122
|
- Agent type registry — default agents, custom `.md` file loading.
|
|
101
123
|
- Prompt assembly, context extraction, memory, skills, environment.
|
|
102
124
|
- Worktree isolation.
|
|
103
125
|
- Token usage tracking.
|
|
126
|
+
- Session directory derivation and persisted `SessionManager` for subagent transcripts.
|
|
104
127
|
- Settings persistence.
|
|
105
128
|
- Internal UI (widget, conversation viewer, `/agents` menu) — these stay until the API boundary is proven, then move to a separate extension.
|
|
106
129
|
|
|
@@ -108,8 +131,8 @@ There is also a `Symbol.for("pi-subagents:manager")` export on `globalThis` that
|
|
|
108
131
|
|
|
109
132
|
- **Scheduling** (`schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`) — 612 LOC removed.
|
|
110
133
|
The `schedule` parameter is removed from the `Agent` tool schema.
|
|
111
|
-
Any extension that wants scheduling can implement it by calling `
|
|
112
|
-
- **Ad-hoc RPC** (`cross-extension-rpc.ts`) — replaced by the typed `
|
|
134
|
+
Any extension that wants scheduling can implement it by calling `getSubagentsService()?.spawn(...)` on a timer.
|
|
135
|
+
- **Ad-hoc RPC** (`cross-extension-rpc.ts`) — replaced by the typed `SubagentsService` published via `Symbol.for()`.
|
|
113
136
|
The untyped event-bus RPC channels are removed.
|
|
114
137
|
- **Group join** (`group-join.ts`) — 141 LOC removed.
|
|
115
138
|
The grouped notification batching adds complexity for a marginal UX improvement.
|
|
@@ -117,21 +140,22 @@ There is also a `Symbol.for("pi-subagents:manager")` export on `globalThis` that
|
|
|
117
140
|
- **Output file** (`output-file.ts`) — replaced by `session-dir.ts` + `SessionManager.create()` (#61).
|
|
118
141
|
Subagent transcripts are now written in Pi's official JSONL session format via the SDK's `SessionManager`, nested under the parent session directory.
|
|
119
142
|
|
|
120
|
-
### Estimated impact
|
|
143
|
+
### Estimated impact (realized)
|
|
121
144
|
|
|
122
|
-
| Subsystem
|
|
123
|
-
|
|
|
124
|
-
| Scheduling
|
|
125
|
-
| Ad-hoc RPC
|
|
126
|
-
| Group join
|
|
127
|
-
| Output file
|
|
128
|
-
|
|
|
145
|
+
| Subsystem | Status | LOC impact |
|
|
146
|
+
| ---------------------- | -------------- | ------------------------------------------ |
|
|
147
|
+
| Scheduling | Removed (#52) | −612 |
|
|
148
|
+
| Ad-hoc RPC | Removed (#49) | −080 |
|
|
149
|
+
| Group join | Removed (#49) | −141 |
|
|
150
|
+
| Output file | Replaced (#61) | −83 (replaced by 38-line `session-dir.ts`) |
|
|
151
|
+
| index.ts decomposition | Done (#54) | 1,894 → 274 |
|
|
129
152
|
|
|
130
|
-
|
|
153
|
+
The codebase is now ~6,100 LOC across 35 modules.
|
|
154
|
+
The `index.ts` entry point is 274 lines.
|
|
131
155
|
|
|
132
|
-
##
|
|
156
|
+
## SubagentsService (done — #48)
|
|
133
157
|
|
|
134
|
-
The `
|
|
158
|
+
The `SubagentsService` interface, accessor functions, and serializable types are exported from `@gotgenes/pi-subagents` via the `./service` export map entry.
|
|
135
159
|
No separate API package is needed.
|
|
136
160
|
|
|
137
161
|
Consumers declare this package as an optional peer dependency:
|
|
@@ -139,7 +163,7 @@ Consumers declare this package as an optional peer dependency:
|
|
|
139
163
|
```json
|
|
140
164
|
{
|
|
141
165
|
"peerDependencies": {
|
|
142
|
-
"@gotgenes/pi-subagents": ">=
|
|
166
|
+
"@gotgenes/pi-subagents": ">=5.0.0"
|
|
143
167
|
},
|
|
144
168
|
"peerDependenciesMeta": {
|
|
145
169
|
"@gotgenes/pi-subagents": { "optional": true }
|
|
@@ -150,72 +174,40 @@ Consumers declare this package as an optional peer dependency:
|
|
|
150
174
|
At runtime, consumers use dynamic import for type-safe access to the accessor functions:
|
|
151
175
|
|
|
152
176
|
```typescript
|
|
153
|
-
const {
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
-
|
|
177
|
+
const { getSubagentsService } = await import("@gotgenes/pi-subagents");
|
|
178
|
+
const svc = getSubagentsService();
|
|
179
|
+
if (svc) {
|
|
180
|
+
svc.spawn("Explore", "Check for stale TODOs");
|
|
157
181
|
}
|
|
158
182
|
```
|
|
159
183
|
|
|
160
184
|
Pi's extension loader creates a fresh `jiti` instance per extension with `moduleCache: false`, so module-scoped singletons don't survive across extensions.
|
|
161
|
-
The accessor functions use `Symbol.for()` on `globalThis`, which is process-global by spec, to bridge this gap.
|
|
185
|
+
The accessor functions use `Symbol.for("@gotgenes/pi-subagents:service")` on `globalThis`, which is process-global by spec, to bridge this gap.
|
|
162
186
|
The dynamic import provides compile-time types; the `Symbol.for()` key is the actual runtime channel.
|
|
163
187
|
|
|
164
188
|
### Interface
|
|
165
189
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
export interface SubagentsAPI {
|
|
169
|
-
/**
|
|
170
|
-
* Spawn an agent. Returns the agent ID immediately.
|
|
171
|
-
* The agent runs in the background unless options.foreground is true.
|
|
172
|
-
*/
|
|
173
|
-
spawn(type: string, prompt: string, options?: SpawnOptions): string;
|
|
174
|
-
|
|
175
|
-
/** Get a snapshot of an agent's current state. */
|
|
176
|
-
getRecord(id: string): SubagentRecord | undefined;
|
|
177
|
-
|
|
178
|
-
/** List all tracked agents, most recent first. */
|
|
179
|
-
listAgents(): SubagentRecord[];
|
|
180
|
-
|
|
181
|
-
/** Abort a running or queued agent. Returns false if not found. */
|
|
182
|
-
abort(id: string): boolean;
|
|
183
|
-
|
|
184
|
-
/** Send a steering message to a running agent. */
|
|
185
|
-
steer(id: string, message: string): Promise<boolean>;
|
|
190
|
+
See `src/service.ts` for the canonical definition.
|
|
191
|
+
Key types:
|
|
186
192
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
hasRunning(): boolean;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export interface SpawnOptions {
|
|
195
|
-
description?: string;
|
|
196
|
-
model?: string;
|
|
197
|
-
maxTurns?: number;
|
|
198
|
-
thinkingLevel?: string;
|
|
199
|
-
isolated?: boolean;
|
|
200
|
-
inheritContext?: boolean;
|
|
201
|
-
foreground?: boolean;
|
|
202
|
-
/** Skip the concurrency queue — start immediately. */
|
|
203
|
-
bypassQueue?: boolean;
|
|
204
|
-
isolation?: "worktree";
|
|
205
|
-
}
|
|
206
|
-
```
|
|
193
|
+
- `SubagentsService` — `spawn`, `getRecord`, `listAgents`, `abort`, `steer`, `waitForAll`, `hasRunning`.
|
|
194
|
+
- `SubagentRecord` — serializable agent snapshot (no live session objects).
|
|
195
|
+
- `SpawnOptions` — `description`, `model`, `maxTurns`, `thinkingLevel`, `isolated`, `inheritContext`, `foreground`, `bypassQueue`, `isolation`.
|
|
196
|
+
- `SUBAGENT_EVENTS` — channel constants for `pi.events` subscriptions.
|
|
207
197
|
|
|
208
198
|
### Accessor pattern
|
|
209
199
|
|
|
210
200
|
```typescript
|
|
211
|
-
const
|
|
201
|
+
const SERVICE_KEY = Symbol.for("@gotgenes/pi-subagents:service");
|
|
212
202
|
|
|
213
|
-
export function
|
|
214
|
-
(globalThis as
|
|
203
|
+
export function publishSubagentsService(service: SubagentsService): void {
|
|
204
|
+
(globalThis as Record<symbol, unknown>)[SERVICE_KEY] = service;
|
|
215
205
|
}
|
|
216
206
|
|
|
217
|
-
export function
|
|
218
|
-
return (globalThis as
|
|
207
|
+
export function getSubagentsService(): SubagentsService | undefined {
|
|
208
|
+
return (globalThis as Record<symbol, unknown>)[SERVICE_KEY] as
|
|
209
|
+
| SubagentsService
|
|
210
|
+
| undefined;
|
|
219
211
|
}
|
|
220
212
|
```
|
|
221
213
|
|
|
@@ -237,23 +229,19 @@ They are fire-and-forget broadcast events — no request IDs, no reply channels.
|
|
|
237
229
|
### Consumer example: scheduling extension
|
|
238
230
|
|
|
239
231
|
```typescript
|
|
240
|
-
// package.json:
|
|
241
|
-
// "peerDependencies": { "@gotgenes/pi-subagents": ">=2.0.0" }
|
|
242
|
-
// "peerDependenciesMeta": { "@gotgenes/pi-subagents": { "optional": true } }
|
|
243
|
-
|
|
244
232
|
export default function (pi) {
|
|
245
233
|
pi.on("session_start", async (event, ctx) => {
|
|
246
|
-
let
|
|
234
|
+
let getSubagentsService;
|
|
247
235
|
try {
|
|
248
|
-
({
|
|
236
|
+
({ getSubagentsService } = await import("@gotgenes/pi-subagents"));
|
|
249
237
|
} catch {
|
|
250
238
|
return; // pi-subagents not installed
|
|
251
239
|
}
|
|
252
|
-
const
|
|
253
|
-
if (!
|
|
240
|
+
const svc = getSubagentsService();
|
|
241
|
+
if (!svc) return;
|
|
254
242
|
|
|
255
243
|
setInterval(() => {
|
|
256
|
-
|
|
244
|
+
svc.spawn("Explore", "Check for stale TODOs", {
|
|
257
245
|
bypassQueue: true,
|
|
258
246
|
});
|
|
259
247
|
}, 60 * 60 * 1000);
|
|
@@ -267,13 +255,13 @@ export default function (pi) {
|
|
|
267
255
|
export default function (pi) {
|
|
268
256
|
pi.events.on("subagents:completed", async (data) => {
|
|
269
257
|
const { id } = data as { id: string };
|
|
270
|
-
let
|
|
258
|
+
let getSubagentsService;
|
|
271
259
|
try {
|
|
272
|
-
({
|
|
260
|
+
({ getSubagentsService } = await import("@gotgenes/pi-subagents"));
|
|
273
261
|
} catch {
|
|
274
262
|
return;
|
|
275
263
|
}
|
|
276
|
-
const record =
|
|
264
|
+
const record = getSubagentsService()?.getRecord(id);
|
|
277
265
|
if (record?.result) {
|
|
278
266
|
fs.appendFileSync("agent-log.jsonl", JSON.stringify(record) + "\n");
|
|
279
267
|
}
|
|
@@ -281,32 +269,38 @@ export default function (pi) {
|
|
|
281
269
|
}
|
|
282
270
|
```
|
|
283
271
|
|
|
284
|
-
## index.ts decomposition
|
|
272
|
+
## index.ts decomposition (done — #54, #69, #70)
|
|
285
273
|
|
|
286
|
-
The 1,894-line `index.ts`
|
|
274
|
+
The original 1,894-line `index.ts` has been decomposed into focused modules:
|
|
287
275
|
|
|
288
276
|
```text
|
|
289
277
|
src/
|
|
290
|
-
├── index.ts
|
|
278
|
+
├── index.ts (274) ← slimmed entry point: init, tool registration
|
|
279
|
+
├── runtime.ts (94) ← SubagentRuntime: session-scoped state + methods
|
|
291
280
|
├── tools/
|
|
292
|
-
│ ├── agent-tool.ts
|
|
293
|
-
│ ├── result-tool.ts
|
|
294
|
-
│
|
|
295
|
-
|
|
296
|
-
├──
|
|
297
|
-
├──
|
|
298
|
-
|
|
299
|
-
|
|
281
|
+
│ ├── agent-tool.ts (626) ← Agent tool definition + execute
|
|
282
|
+
│ ├── get-result-tool.ts ← get_subagent_result tool
|
|
283
|
+
│ ├── steer-tool.ts ← steer_subagent tool
|
|
284
|
+
│ └── helpers.ts ← shared tool utilities
|
|
285
|
+
├── handlers/
|
|
286
|
+
│ ├── lifecycle.ts ← session_start, session_before_switch, session_shutdown
|
|
287
|
+
│ └── tool-start.ts ← tool_execution_start handler
|
|
288
|
+
├── notification.ts ← completion nudges, custom renderer
|
|
289
|
+
├── renderer.ts ← notification TUI component
|
|
290
|
+
├── ui/agent-menu.ts (677) ← /agents slash command menu
|
|
291
|
+
├── service-adapter.ts ← SubagentsService implementation wrapping AgentManager
|
|
292
|
+
└── (existing domain modules unchanged)
|
|
300
293
|
```
|
|
301
294
|
|
|
302
295
|
Each extracted module receives narrow constructor-injected dependencies rather than closing over module-level state.
|
|
296
|
+
Handlers call methods on narrow runtime interfaces — no raw field writes, no `widget!` reach-throughs.
|
|
303
297
|
|
|
304
|
-
## Phase plan
|
|
298
|
+
## Phase plan (Phases 1–5 complete)
|
|
305
299
|
|
|
306
|
-
### Phase 1: Export `
|
|
300
|
+
### Phase 1: Export `SubagentsService` from this package ✓ (done — #48)
|
|
307
301
|
|
|
308
|
-
|
|
309
|
-
|
|
302
|
+
Added the `SubagentsService` interface, serializable types, `Symbol.for()` accessor functions, and `SUBAGENT_EVENTS` constants as public exports.
|
|
303
|
+
Wired `service-adapter.ts` to wrap `AgentManager` and call `publishSubagentsService()` at extension init.
|
|
310
304
|
|
|
311
305
|
### Phase 2: Remove scheduling ✓ (done — issue #52)
|
|
312
306
|
|
|
@@ -314,127 +308,225 @@ Deleted `schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`.
|
|
|
314
308
|
Removed the `schedule` parameter from the `Agent` tool schema.
|
|
315
309
|
Removed scheduler setup and lifecycle hooks from `index.ts`.
|
|
316
310
|
|
|
317
|
-
### Phase 3: Remove group-join, ad-hoc RPC; replace output-file
|
|
311
|
+
### Phase 3: Remove group-join, ad-hoc RPC; replace output-file ✓ (done — #49, #61)
|
|
318
312
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
313
|
+
Deleted `group-join.ts`, `cross-extension-rpc.ts` (#49).
|
|
314
|
+
Replaced `output-file.ts` with `SessionManager.create()` + `session-dir.ts` (#61).
|
|
315
|
+
Simplified `index.ts` to use direct individual notifications.
|
|
316
|
+
Lifecycle events emitted on `pi.events` for external consumers.
|
|
323
317
|
|
|
324
|
-
### Phase 4: Implement and publish `
|
|
318
|
+
### Phase 4: Implement and publish `SubagentsService` ✓ (done — #48)
|
|
325
319
|
|
|
326
|
-
|
|
327
|
-
|
|
320
|
+
Wired `service-adapter.ts` to wrap `AgentManager` and call `publishSubagentsService()` at extension init.
|
|
321
|
+
Model strings are resolved inside the adapter.
|
|
328
322
|
|
|
329
|
-
### Phase 5: Decompose `index.ts` ✓ (done —
|
|
323
|
+
### Phase 5: Decompose `index.ts` ✓ (done — #54, #69, #70, #87)
|
|
330
324
|
|
|
331
|
-
Extracted tools, notifications, activity tracking, and the `/agents` command into separate modules.
|
|
332
|
-
`
|
|
325
|
+
Extracted tools, notifications, activity tracking, event handlers, and the `/agents` command into separate modules.
|
|
326
|
+
Created `SubagentRuntime` factory to hold session-scoped state.
|
|
327
|
+
`src/index.ts` shrank from ~1,894 lines to ~274 lines.
|
|
333
328
|
|
|
334
329
|
### Phase 6 (future): Extract UI to `@gotgenes/pi-subagents-ui`
|
|
335
330
|
|
|
336
|
-
Move `ui/agent-widget.ts`, `ui/conversation-viewer.ts`, the `/agents` command, notifications, and activity tracking to a separate extension that consumes `
|
|
331
|
+
Move `ui/agent-widget.ts`, `ui/conversation-viewer.ts`, the `/agents` command, notifications, and activity tracking to a separate extension that consumes `SubagentsService` + lifecycle events.
|
|
337
332
|
This phase is deferred until the API boundary is proven stable in production.
|
|
338
333
|
|
|
339
|
-
## Structural refactoring roadmap (post-#54)
|
|
334
|
+
## Structural refactoring roadmap (post-#54) ✓ complete
|
|
340
335
|
|
|
341
|
-
|
|
342
|
-
|
|
336
|
+
All structural refactoring phases are complete.
|
|
337
|
+
See `git log` for the full history; issue references are preserved below for traceability.
|
|
343
338
|
|
|
344
|
-
|
|
339
|
+
| Phase | Issue | Summary |
|
|
340
|
+
| ------------------ | ------------------ | --------------------------------------------------------------------- |
|
|
341
|
+
| Foundation | #69, #71, #76, #80 | SubagentRuntime, pure assembler, cwd injection, config consolidation |
|
|
342
|
+
| Core decomposition | #84, #72, #87, #70 | WorktreeManager, AgentManager DI, runtime methods, handler extraction |
|
|
343
|
+
| Interface polish | #66, #77 | SDK types, projectAgentsDir |
|
|
344
|
+
| Features | #61 | JSONL session transcripts |
|
|
345
345
|
|
|
346
|
-
|
|
347
|
-
Together they eliminate module-scope mutable state, create a testable functional core, and simplify the agent-types API.
|
|
346
|
+
The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
|
|
348
347
|
|
|
349
|
-
|
|
350
|
-
- Move `defaultMaxTurns`, `graceTurns`, `agentActivity`, `currentCtx`, and widget references out of closure/module scope into a single factory-constructed object.
|
|
351
|
-
- This unblocks handler extraction (Issue #70) by giving handlers a concrete deps bag instead of closure variables.
|
|
348
|
+
---
|
|
352
349
|
|
|
353
|
-
|
|
354
|
-
- Split `runAgent()` into a pure configuration assembler (~200 lines) and an IO shell (~200 lines).
|
|
355
|
-
- The assembler becomes independently testable without mocking the Pi SDK.
|
|
350
|
+
## Next target: AgentManager internal decomposition
|
|
356
351
|
|
|
357
|
-
|
|
358
|
-
|
|
352
|
+
The structural refactoring roadmap decomposed the extension entry point and established clean module boundaries.
|
|
353
|
+
AgentManager itself — the central class — was not touched structurally.
|
|
354
|
+
A design review reveals three tangled responsibilities and two systemic patterns that inflate complexity.
|
|
359
355
|
|
|
360
|
-
|
|
361
|
-
- Replaced the two overlapping lookup functions with a single `resolveAgentConfig(type): AgentConfig` that handles the unknown-type fallback internally.
|
|
362
|
-
- Eliminated the duplicated fallback chain exposed by #71 and simplified test mock setup.
|
|
356
|
+
### Problem statement
|
|
363
357
|
|
|
364
|
-
|
|
358
|
+
AgentManager is a 500-line class that serves as the single mediator between tool callers and the agent runner.
|
|
359
|
+
Every concern passes through it because it owns the `AgentRecord`.
|
|
365
360
|
|
|
366
|
-
|
|
361
|
+
Three responsibilities are tangled:
|
|
367
362
|
|
|
368
|
-
1. **
|
|
369
|
-
|
|
370
|
-
|
|
363
|
+
1. **Record registry** — create, track, query, clean up `AgentRecord` instances.
|
|
364
|
+
2. **Concurrency control** — queue, running count, drain, `bypassQueue`.
|
|
365
|
+
3. **Execution orchestration** — thread options to the runner, intercept callbacks to update records, wire abort signals, manage worktree lifecycle.
|
|
371
366
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
- Converted `AgentManager` constructor from 6 positional parameters to an `AgentManagerOptions` bag with injected `AgentRunner` and `WorktreeManager`.
|
|
375
|
-
- Removed all runtime imports of `agent-runner.ts` and `worktree.ts` from `agent-manager.ts` (only `import type` remains).
|
|
376
|
-
- Migrated all tests from `vi.mock()` module stubs to `vi.fn()` interface stubs.
|
|
367
|
+
`startAgent()` alone is ~130 lines because it handles all three.
|
|
368
|
+
The `.then()` / `.catch()` blocks mix status updates (job 1), worktree cleanup (job 3), notification callbacks (job 1), and queue draining (job 2).
|
|
377
369
|
|
|
378
|
-
|
|
379
|
-
- Added session-context methods (`setSessionContext`, `clearSessionContext`) and widget delegation methods (`setUICtx`, `onTurnStart`, `markFinished`, `updateWidget`, `ensureTimer`).
|
|
380
|
-
- Prerequisite for #70 — without runtime methods, extracted handlers would move LoD violations and output-argument smells into handler classes.
|
|
370
|
+
Two systemic patterns compound the problem:
|
|
381
371
|
|
|
382
|
-
|
|
383
|
-
- Moved the four inline lambdas (`session_start`, `session_before_switch`, `session_shutdown`, `tool_execution_start`) into `SessionLifecycleHandler` and `ToolStartHandler` classes.
|
|
384
|
-
- Handlers call methods on narrow runtime interfaces — no raw field writes, no `widget!` reach-throughs.
|
|
372
|
+
### Problem 1: Callback threading
|
|
385
373
|
|
|
386
|
-
|
|
374
|
+
`SpawnOptions` carries 6 `on*` callback fields.
|
|
375
|
+
They thread through three layers:
|
|
387
376
|
|
|
388
|
-
|
|
377
|
+
```text
|
|
378
|
+
agent-tool.ts (UI tracking state)
|
|
379
|
+
→ AgentManager.startAgent() wraps each to update the record, then forwards
|
|
380
|
+
→ runner.run() subscribes to session events, calls callbacks
|
|
381
|
+
```
|
|
389
382
|
|
|
390
|
-
|
|
391
|
-
- Type-only change in the tool/menu factory dep interfaces.
|
|
392
|
-
- Best done after Issues #69 and #70 when the interfaces are stable.
|
|
383
|
+
The callbacks serve two purposes that are tangled together:
|
|
393
384
|
|
|
394
|
-
|
|
395
|
-
|
|
385
|
+
1. **Record statistics** — `onToolActivity` increments `toolUses`, `onAssistantUsage` accumulates `lifetimeUsage`, `onCompaction` increments `compactionCount`, `onSessionCreated` captures the session and output file.
|
|
386
|
+
This is internal bookkeeping that belongs to the record.
|
|
387
|
+
2. **UI streaming** — the same callbacks update the widget's active-tool display, response text preview, and turn counter.
|
|
388
|
+
This is presentation that belongs to the UI layer.
|
|
396
389
|
|
|
397
|
-
|
|
390
|
+
The session already emits all of these events via `session.subscribe()`.
|
|
391
|
+
The runner subscribes to session events, translates them into callback invocations, AgentManager wraps each callback to update the record, then forwards to the caller's callback.
|
|
392
|
+
Three layers reimplementing what a single event subscription could provide.
|
|
398
393
|
|
|
399
|
-
|
|
400
|
-
- Replaced `output-file.ts` with `SessionManager.create()` + `session-dir.ts`.
|
|
401
|
-
- Subagent sessions are persisted under `<parent-session-dir>/<parent-session-basename>/tasks/` with `parentSession` header linking.
|
|
394
|
+
### Problem 2: Live `ctx` capture
|
|
402
395
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
- Requires coordination on env-var conventions.
|
|
406
|
-
- Not blocked by the structural refactor but logically separate from it.
|
|
396
|
+
`ctx: ExtensionContext` is a mutable reference to the parent session.
|
|
397
|
+
It is captured into `SpawnArgs` and held in the concurrency queue:
|
|
407
398
|
|
|
408
|
-
|
|
399
|
+
```typescript
|
|
400
|
+
const args: SpawnArgs = { pi, ctx, type, prompt, options };
|
|
401
|
+
this.queue.push({ id, args }); // ctx held until dequeue
|
|
402
|
+
```
|
|
409
403
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
404
|
+
When the queued agent dequeues, `runAgent()` reads from the live `ctx`:
|
|
405
|
+
|
|
406
|
+
- `ctx.cwd` — directory that may have changed.
|
|
407
|
+
- `ctx.getSystemPrompt()` — live method call on a potentially stale session.
|
|
408
|
+
- `ctx.model` — model that may have been switched.
|
|
409
|
+
- `ctx.modelRegistry` — registry reference.
|
|
410
|
+
|
|
411
|
+
If the parent session changes between queue and dequeue (model switch, cwd change, session restart), the agent reads invalid state.
|
|
412
|
+
The same live reference persists in `runtime.currentCtx` for the service-adapter.
|
|
413
|
+
|
|
414
|
+
Additionally, `inheritContext` calls `ctx.sessionManager.getBranch()` at run time.
|
|
415
|
+
The user's intent is to fork the conversation as it existed when they asked for the agent — not the conversation at some arbitrary later point when a queue slot opens.
|
|
416
|
+
|
|
417
|
+
### Design: snapshot at spawn time
|
|
418
|
+
|
|
419
|
+
Replace the live `ctx` capture with a plain data snapshot taken once at spawn time:
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
interface ParentSnapshot {
|
|
423
|
+
cwd: string;
|
|
424
|
+
systemPrompt: string;
|
|
425
|
+
model: unknown;
|
|
426
|
+
modelRegistry: { find(...): unknown; getAvailable?(): ... };
|
|
427
|
+
parentContext?: string; // pre-built text if inheritContext
|
|
428
|
+
}
|
|
424
429
|
```
|
|
425
430
|
|
|
426
|
-
|
|
431
|
+
This snapshot is:
|
|
427
432
|
|
|
428
|
-
|
|
433
|
+
- Captured once in `spawn()` (or by the tool before calling `spawn()`).
|
|
434
|
+
- Stored in `SpawnArgs` instead of `ctx`.
|
|
435
|
+
- Passed to `runner.run()` instead of `ctx: ExtensionContext`.
|
|
436
|
+
- Immutable — no staleness risk, no session-lifetime coupling.
|
|
437
|
+
|
|
438
|
+
`runAgent()` already reads exactly these 4 values from `ctx` and never touches it again.
|
|
439
|
+
`buildParentContext()` also reads once and produces a string.
|
|
440
|
+
The snapshot formalizes what is already happening, and makes the "read once" guarantee structural.
|
|
441
|
+
|
|
442
|
+
### Design: session-event observation replaces callback threading
|
|
443
|
+
|
|
444
|
+
The session emits events via `session.subscribe()`.
|
|
445
|
+
Today, `runner.run()` subscribes and translates events into `RunOptions.on*()` callbacks, AgentManager wraps those to update the record, then forwards to the caller.
|
|
446
|
+
|
|
447
|
+
The target replaces this three-layer chain with direct subscription:
|
|
429
448
|
|
|
430
449
|
```text
|
|
431
|
-
|
|
450
|
+
session.subscribe()
|
|
451
|
+
│
|
|
452
|
+
┌─────────────┼─────────────┐
|
|
453
|
+
│ │
|
|
454
|
+
Record observer UI observer
|
|
455
|
+
(accumulates stats on record) (updates widget state)
|
|
456
|
+
managed by AgentManager managed by agent-tool
|
|
457
|
+
subscribes in startAgent() subscribes after spawn
|
|
432
458
|
```
|
|
433
459
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
460
|
+
AgentManager subscribes to the session to update the record (toolUses, lifetimeUsage, compactionCount, outputFile).
|
|
461
|
+
The agent-tool subscribes to the session to stream UI state (active tools, response text, turn count).
|
|
462
|
+
Neither layer wraps or forwards the other's callbacks.
|
|
463
|
+
|
|
464
|
+
`RunOptions` drops all 6 `on*` fields and becomes pure configuration.
|
|
465
|
+
`SpawnOptions` drops all 6 `on*` fields and becomes identity + dispatch mode.
|
|
466
|
+
The session reference reaches callers via `record.session` (already stored) or via an `onSessionCreated` callback that is the one callback that remains (it delivers the session object, enabling the external subscription).
|
|
467
|
+
|
|
468
|
+
### Design: record state machine
|
|
469
|
+
|
|
470
|
+
Status transitions are scattered across 6 locations (`startAgent` `.then()`, `.catch()`, `resume()`, `abort()`, `abortAll()`, `drainQueue()`).
|
|
471
|
+
Each location sets `record.status` plus associated fields (`completedAt`, `result`, `error`) in ad-hoc combinations.
|
|
472
|
+
|
|
473
|
+
Extract a state machine on `AgentRecord` (or a thin wrapper) that owns all transitions:
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
record.markRunning(startedAt)
|
|
477
|
+
record.markCompleted(result, completedAt)
|
|
478
|
+
record.markError(error)
|
|
479
|
+
record.markStopped()
|
|
480
|
+
record.resetForResume()
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Each method sets exactly the fields that belong to that transition.
|
|
484
|
+
Invalid transitions (e.g., `markCompleted` on an already-stopped record) are no-ops.
|
|
485
|
+
The `if (record.status !== "stopped")` guards in `.then()` and `.catch()` become part of the transition logic rather than scattered conditionals.
|
|
486
|
+
|
|
487
|
+
### Phased implementation
|
|
488
|
+
|
|
489
|
+
The three designs are independent and can land in any order.
|
|
490
|
+
The recommended sequence minimizes intermediate churn.
|
|
491
|
+
|
|
492
|
+
#### Step 1: Record state machine
|
|
493
|
+
|
|
494
|
+
Extract status-transition methods onto `AgentRecord` (or a `RecordManager` wrapper).
|
|
495
|
+
Purely mechanical — replace scattered field writes with method calls.
|
|
496
|
+
No interface changes for callers.
|
|
497
|
+
|
|
498
|
+
This is the lowest-risk change and immediately reduces `startAgent()` line count.
|
|
499
|
+
|
|
500
|
+
#### Step 2: Parent snapshot
|
|
501
|
+
|
|
502
|
+
Replace `ctx: ExtensionContext` in `SpawnArgs` with a `ParentSnapshot` data object.
|
|
503
|
+
Capture the snapshot in `spawn()` or at the tool call site.
|
|
504
|
+
Update `runner.run()` signature to accept `ParentSnapshot` instead of `ctx`.
|
|
505
|
+
Remove `pi: ExtensionAPI` from `SpawnArgs` (it is only used to pass to `runner.run()`, which only uses it for `detectEnv()` — that can accept a shell-exec function instead).
|
|
506
|
+
|
|
507
|
+
This change narrows the `AgentRunner` interface and eliminates live-reference capture.
|
|
508
|
+
|
|
509
|
+
#### Step 3: Session-event observation
|
|
510
|
+
|
|
511
|
+
Replace the callback-threading pattern with direct session subscriptions.
|
|
512
|
+
AgentManager subscribes to the session after creation to update the record.
|
|
513
|
+
The agent-tool subscribes to the session after spawn to stream UI state.
|
|
514
|
+
`RunOptions` and `SpawnOptions` drop all `on*` callback fields.
|
|
515
|
+
|
|
516
|
+
This is the largest change but depends on Step 2 (the runner signature is already narrower) and benefits from Step 1 (the record's transition methods encapsulate the stats updates that the subscription drives).
|
|
517
|
+
|
|
518
|
+
### Expected outcome
|
|
519
|
+
|
|
520
|
+
| Metric | Before | After |
|
|
521
|
+
| --------------------------------- | ------ | ------------------------ |
|
|
522
|
+
| `SpawnOptions` fields | 19 | ~8 (identity + dispatch) |
|
|
523
|
+
| `RunOptions` fields | 15 | ~9 (config only) |
|
|
524
|
+
| `startAgent()` lines | ~130 | ~50 |
|
|
525
|
+
| Callback layers | 3 | 0 (direct subscription) |
|
|
526
|
+
| Live `ctx` references in queue | 1 | 0 (snapshot) |
|
|
527
|
+
| Scattered status-transition sites | 6 | 1 (state machine) |
|
|
528
|
+
|
|
529
|
+
---
|
|
438
530
|
|
|
439
531
|
## Relationship with upstream
|
|
440
532
|
|