@gotgenes/pi-subagents 6.3.1 → 6.5.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 +32 -0
- package/docs/architecture/architecture.md +244 -260
- package/docs/plans/0108-extract-agent-type-registry.md +322 -0
- package/docs/plans/0109-extract-settings-manager.md +276 -0
- package/docs/retro/0108-extract-agent-type-registry.md +41 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +16 -13
- package/src/agent-runner.ts +4 -0
- package/src/agent-types.ts +108 -91
- package/src/index.ts +31 -58
- package/src/runtime.ts +0 -6
- package/src/session-config.ts +5 -4
- package/src/settings.ts +94 -46
- package/src/tools/agent-tool.ts +11 -11
- package/src/tools/get-result-tool.ts +3 -1
- package/src/types.ts +0 -3
- package/src/ui/agent-menu.ts +47 -53
- package/src/ui/agent-widget.ts +10 -9
- package/src/ui/conversation-viewer.ts +4 -2
|
@@ -14,104 +14,97 @@ This document describes the architecture of the pi-subagents fork: a focused, co
|
|
|
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
|
|
17
|
+
6. **Snapshot, don't capture** — mutable parent state (ctx, session, model) is read once at spawn time and frozen into a `ParentSnapshot` data object.
|
|
18
18
|
No live references survive past the spawn call.
|
|
19
|
-
7. **Subscribe, don't thread** — observation of agent progress uses event subscription
|
|
19
|
+
7. **Subscribe, don't thread** — observation of agent progress uses direct session-event subscription, not callback parameters threaded through multiple layers.
|
|
20
|
+
8. **Construct complete** — objects are born with all their dependencies.
|
|
21
|
+
If state isn't available yet, the object that needs it doesn't exist yet.
|
|
22
|
+
No post-construction field writes from external code — if an object can't be instantiated ready-to-go, the prep work hasn't been done and the right dependencies haven't been identified.
|
|
23
|
+
9. **State owns its mutations** — mutable state lives in a class whose methods enforce valid transitions and invariants.
|
|
24
|
+
Free functions that mutate module-scoped variables, closure-captured bags-of-functions, and external writes to shared interfaces are replaced by classes that encapsulate the state they manage.
|
|
20
25
|
|
|
21
26
|
## Current state
|
|
22
27
|
|
|
23
|
-
The extension is
|
|
24
|
-
The `index.ts` entry point is ~270 lines; the rest is decomposed into domain modules.
|
|
28
|
+
The extension is organized into 39 focused modules with a typed `SubagentsService` API boundary.
|
|
25
29
|
|
|
26
30
|
```text
|
|
27
|
-
index.ts
|
|
28
|
-
agent-manager.ts
|
|
29
|
-
agent-runner.ts
|
|
30
|
-
session-config.ts
|
|
31
|
-
agent-types.ts
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
tools/
|
|
54
|
-
tools/
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
31
|
+
index.ts — entry point, tool registration, event wiring
|
|
32
|
+
agent-manager.ts — lifecycle, concurrency, queue
|
|
33
|
+
agent-runner.ts — session creation, turn loop, tool filtering
|
|
34
|
+
session-config.ts — pure session-config assembler
|
|
35
|
+
agent-types.ts — type registry (defaults + custom .md files)
|
|
36
|
+
agent-record.ts — agent record with encapsulated status transitions
|
|
37
|
+
types.ts — shared type definitions
|
|
38
|
+
runtime.ts — SubagentRuntime factory (session-scoped state)
|
|
39
|
+
parent-snapshot.ts — immutable snapshot of parent session state
|
|
40
|
+
|
|
41
|
+
prompts.ts — system prompt assembly
|
|
42
|
+
context.ts — parent conversation extraction
|
|
43
|
+
memory.ts — persistent MEMORY.md per agent
|
|
44
|
+
skill-loader.ts — preload .pi/skills into prompts
|
|
45
|
+
env.ts — git/platform detection
|
|
46
|
+
|
|
47
|
+
worktree.ts — git worktree isolation
|
|
48
|
+
usage.ts — token usage tracking
|
|
49
|
+
model-resolver.ts — fuzzy model name resolution
|
|
50
|
+
invocation-config.ts — merge tool params with agent config
|
|
51
|
+
session-dir.ts — subagent session directory derivation
|
|
52
|
+
settings.ts — persistent operational settings; `SettingsManager` class owns all three in-memory values
|
|
53
|
+
|
|
54
|
+
service.ts — SubagentsService interface + Symbol.for() accessors
|
|
55
|
+
service-adapter.ts — SubagentsService implementation wrapping AgentManager
|
|
56
|
+
|
|
57
|
+
tools/agent-tool.ts — Agent tool definition + execute
|
|
58
|
+
tools/get-result-tool.ts — get_subagent_result tool
|
|
59
|
+
tools/steer-tool.ts — steer_subagent tool
|
|
60
|
+
tools/helpers.ts — shared tool utilities
|
|
61
|
+
|
|
62
|
+
handlers/lifecycle.ts — session_start, session_before_switch, session_shutdown
|
|
63
|
+
handlers/tool-start.ts — tool_execution_start handler
|
|
64
|
+
|
|
65
|
+
notification.ts — completion nudges, custom message renderer
|
|
66
|
+
renderer.ts — notification TUI component
|
|
67
|
+
record-observer.ts — session-event observer for record statistics
|
|
68
|
+
|
|
69
|
+
ui/agent-widget.ts — above-editor live status widget
|
|
70
|
+
ui/agent-menu.ts — /agents slash command menu
|
|
64
71
|
ui/conversation-viewer.ts — scrollable session overlay
|
|
72
|
+
ui/ui-observer.ts — session-event observer for UI streaming
|
|
65
73
|
|
|
66
|
-
default-agents.ts
|
|
67
|
-
custom-agents.ts
|
|
68
|
-
debug.ts
|
|
74
|
+
default-agents.ts — embedded default agent configs (general-purpose, Explore, Plan)
|
|
75
|
+
custom-agents.ts — user-defined agent .md file loader
|
|
76
|
+
debug.ts — debug logging utility
|
|
69
77
|
```
|
|
70
78
|
|
|
71
|
-
###
|
|
79
|
+
### Observation model
|
|
72
80
|
|
|
73
|
-
|
|
81
|
+
Record statistics (tool uses, token usage, compaction counts) are updated by `record-observer.ts`, which subscribes directly to session events.
|
|
82
|
+
UI streaming (active tools, response text, turn counts) is handled by `ui/ui-observer.ts`, which subscribes to the same session events independently.
|
|
83
|
+
Neither observer wraps or forwards the other — both subscribe directly to the session.
|
|
84
|
+
|
|
85
|
+
The widget reads agent state by polling a shared `Map<string, AgentActivity>` on `SubagentRuntime` every 80 ms. The conversation viewer subscribes directly to `AgentSession` objects.
|
|
74
86
|
|
|
75
87
|
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.
|
|
77
88
|
|
|
78
|
-
##
|
|
89
|
+
## Cross-extension architecture
|
|
79
90
|
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
│ AgentManager, agent-runner, agent-types │
|
|
93
|
-
│ publishSubagentsService(impl) ← called at init │
|
|
94
|
-
│ │
|
|
95
|
-
│ Internal UI (widget, viewer, /agents menu) │
|
|
96
|
-
│ ← moves to pi-subagents-ui later │
|
|
97
|
-
└──────────────────────┬─────────────────────────────────┘
|
|
98
|
-
│ Symbol.for("@gotgenes/pi-subagents:service")
|
|
99
|
-
│
|
|
100
|
-
┌─────────────────┼──────────────────┐
|
|
101
|
-
│ │ │
|
|
102
|
-
▼ ▼ ▼
|
|
103
|
-
┌─────────┐ ┌──────────────┐ ┌──────────────┐
|
|
104
|
-
│ pi- │ │ pi-subagents │ │ any future │
|
|
105
|
-
│ schedule│ │ -ui │ │ extension │
|
|
106
|
-
│ (other │ │ (deferred) │ │ │
|
|
107
|
-
│ ext) │ └──────────────┘ └──────────────┘
|
|
108
|
-
└─────────┘
|
|
109
|
-
│
|
|
110
|
-
│ getSubagentsService()?.spawn(...)
|
|
111
|
-
│ (optional peer dep + dynamic import for types)
|
|
112
|
-
▼
|
|
91
|
+
```mermaid
|
|
92
|
+
flowchart TD
|
|
93
|
+
subgraph core["@gotgenes/pi-subagents (this package)"]
|
|
94
|
+
direction TB
|
|
95
|
+
exports["SubagentsService interface\npublish / getSubagentsService()\nSubagentRecord, SubagentStatus, LifetimeUsage\nSUBAGENT_EVENTS constants"]
|
|
96
|
+
engine["Agent + get_subagent_result + steer_subagent tools\nAgentManager, agent-runner, agent-types\npublishSubagentsService() called at init"]
|
|
97
|
+
ui["Internal UI: widget, viewer, /agents menu\n(candidate for extraction to pi-subagents-ui)"]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
core -- "Symbol.for() on globalThis" --> sched["scheduling extension\n(hypothetical)"]
|
|
101
|
+
core -- "Symbol.for() on globalThis" --> subui["pi-subagents-ui\n(deferred)"]
|
|
102
|
+
core -- "Symbol.for() on globalThis" --> future["any future extension"]
|
|
113
103
|
```
|
|
114
104
|
|
|
105
|
+
Consumers call `getSubagentsService()?.spawn(...)` at runtime.
|
|
106
|
+
They declare this package as an optional peer dependency and use dynamic import for compile-time types.
|
|
107
|
+
|
|
115
108
|
### What the core owns
|
|
116
109
|
|
|
117
110
|
- The three tools: `Agent`, `get_subagent_result`, `steer_subagent`.
|
|
@@ -119,6 +112,8 @@ The ad-hoc RPC layer and untyped `Symbol.for("pi-subagents:manager")` have been
|
|
|
119
112
|
- `agent-runner` — session creation, turn loop, tool filtering, extension binding (Patches 2 and 3).
|
|
120
113
|
- `session-config` — pure configuration assembler (extracted from `agent-runner`).
|
|
121
114
|
- `SubagentRuntime` — session-scoped state bag with methods.
|
|
115
|
+
- `ParentSnapshot` — immutable snapshot of parent session state, captured once at spawn time.
|
|
116
|
+
- `record-observer` — session-event observer that updates record statistics without callback threading.
|
|
122
117
|
- Agent type registry — default agents, custom `.md` file loading.
|
|
123
118
|
- Prompt assembly, context extraction, memory, skills, environment.
|
|
124
119
|
- Worktree isolation.
|
|
@@ -127,33 +122,20 @@ The ad-hoc RPC layer and untyped `Symbol.for("pi-subagents:manager")` have been
|
|
|
127
122
|
- Settings persistence.
|
|
128
123
|
- Internal UI (widget, conversation viewer, `/agents` menu) — these stay until the API boundary is proven, then move to a separate extension.
|
|
129
124
|
|
|
130
|
-
### What the core
|
|
125
|
+
### What the core dropped
|
|
131
126
|
|
|
132
|
-
- **Scheduling** (`schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`) —
|
|
133
|
-
The `schedule` parameter is removed from the `Agent` tool schema.
|
|
127
|
+
- **Scheduling** (`schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`) — removed (#52).
|
|
134
128
|
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()
|
|
136
|
-
|
|
137
|
-
- **Group join** (`group-join.ts`) — 141 LOC removed.
|
|
138
|
-
The grouped notification batching adds complexity for a marginal UX improvement.
|
|
129
|
+
- **Ad-hoc RPC** (`cross-extension-rpc.ts`) — replaced by the typed `SubagentsService` published via `Symbol.for()` (#49).
|
|
130
|
+
- **Group join** (`group-join.ts`) — removed (#49).
|
|
139
131
|
Individual completion notifications are sufficient.
|
|
140
132
|
- **Output file** (`output-file.ts`) — replaced by `session-dir.ts` + `SessionManager.create()` (#61).
|
|
141
|
-
Subagent transcripts are now written in Pi's official JSONL session format
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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 |
|
|
152
|
-
|
|
153
|
-
The codebase is now ~6,100 LOC across 35 modules.
|
|
154
|
-
The `index.ts` entry point is 274 lines.
|
|
133
|
+
Subagent transcripts are now written in Pi's official JSONL session format.
|
|
134
|
+
- **Callback threading** — the three-layer `on*` callback chain through `SpawnOptions` → `AgentManager` → `RunOptions` was replaced by direct session-event subscriptions (#100).
|
|
135
|
+
- **Live `ctx` capture** — `SpawnArgs` previously held a mutable `ctx: ExtensionContext` reference that could go stale in the concurrency queue.
|
|
136
|
+
Replaced by `ParentSnapshot`, an immutable data object captured once at spawn time (#99).
|
|
155
137
|
|
|
156
|
-
## SubagentsService
|
|
138
|
+
## SubagentsService
|
|
157
139
|
|
|
158
140
|
The `SubagentsService` interface, accessor functions, and serializable types are exported from `@gotgenes/pi-subagents` via the `./service` export map entry.
|
|
159
141
|
No separate API package is needed.
|
|
@@ -223,8 +205,7 @@ The core emits events on `pi.events` that any extension can observe:
|
|
|
223
205
|
| `subagents:completed` | `{ id, type, status, result?, error? }` | Agent finishes |
|
|
224
206
|
| `subagents:activity` | `{ id, toolName?, textDelta?, turnCount? }` | Streaming progress |
|
|
225
207
|
|
|
226
|
-
These
|
|
227
|
-
They are fire-and-forget broadcast events — no request IDs, no reply channels.
|
|
208
|
+
These are fire-and-forget broadcast events — no request IDs, no reply channels.
|
|
228
209
|
|
|
229
210
|
### Consumer example: scheduling extension
|
|
230
211
|
|
|
@@ -269,71 +250,78 @@ export default function (pi) {
|
|
|
269
250
|
}
|
|
270
251
|
```
|
|
271
252
|
|
|
272
|
-
## index.ts decomposition
|
|
253
|
+
## index.ts decomposition
|
|
273
254
|
|
|
274
|
-
The original
|
|
255
|
+
The original monolithic `index.ts` has been decomposed into focused modules:
|
|
275
256
|
|
|
276
257
|
```text
|
|
277
258
|
src/
|
|
278
|
-
├── index.ts
|
|
279
|
-
├── runtime.ts
|
|
259
|
+
├── index.ts — slimmed entry point: init, tool registration
|
|
260
|
+
├── runtime.ts — SubagentRuntime: session-scoped state + methods
|
|
280
261
|
├── tools/
|
|
281
|
-
│ ├── agent-tool.ts
|
|
282
|
-
│ ├── get-result-tool.ts
|
|
283
|
-
│ ├── steer-tool.ts
|
|
284
|
-
│ └── helpers.ts
|
|
262
|
+
│ ├── agent-tool.ts — Agent tool definition + execute
|
|
263
|
+
│ ├── get-result-tool.ts — get_subagent_result tool
|
|
264
|
+
│ ├── steer-tool.ts — steer_subagent tool
|
|
265
|
+
│ └── helpers.ts — shared tool utilities
|
|
285
266
|
├── handlers/
|
|
286
|
-
│ ├── lifecycle.ts
|
|
287
|
-
│ └── tool-start.ts
|
|
288
|
-
├── notification.ts
|
|
289
|
-
├── renderer.ts
|
|
290
|
-
├── ui/agent-menu.ts
|
|
291
|
-
├── service-adapter.ts
|
|
267
|
+
│ ├── lifecycle.ts — session_start, session_before_switch, session_shutdown
|
|
268
|
+
│ └── tool-start.ts — tool_execution_start handler
|
|
269
|
+
├── notification.ts — completion nudges, custom renderer
|
|
270
|
+
├── renderer.ts — notification TUI component
|
|
271
|
+
├── ui/agent-menu.ts — /agents slash command menu
|
|
272
|
+
├── service-adapter.ts — SubagentsService implementation wrapping AgentManager
|
|
292
273
|
└── (existing domain modules unchanged)
|
|
293
274
|
```
|
|
294
275
|
|
|
295
276
|
Each extracted module receives narrow constructor-injected dependencies rather than closing over module-level state.
|
|
296
277
|
Handlers call methods on narrow runtime interfaces — no raw field writes, no `widget!` reach-throughs.
|
|
297
278
|
|
|
298
|
-
## Phase plan
|
|
279
|
+
## Phase plan
|
|
299
280
|
|
|
300
|
-
### Phase 1: Export `SubagentsService` from this package
|
|
281
|
+
### Phase 1: Export `SubagentsService` from this package (#48)
|
|
301
282
|
|
|
302
283
|
Added the `SubagentsService` interface, serializable types, `Symbol.for()` accessor functions, and `SUBAGENT_EVENTS` constants as public exports.
|
|
303
284
|
Wired `service-adapter.ts` to wrap `AgentManager` and call `publishSubagentsService()` at extension init.
|
|
304
285
|
|
|
305
|
-
### Phase 2: Remove scheduling
|
|
286
|
+
### Phase 2: Remove scheduling (#52)
|
|
306
287
|
|
|
307
288
|
Deleted `schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`.
|
|
308
289
|
Removed the `schedule` parameter from the `Agent` tool schema.
|
|
309
290
|
Removed scheduler setup and lifecycle hooks from `index.ts`.
|
|
310
291
|
|
|
311
|
-
### Phase 3: Remove group-join, ad-hoc RPC; replace output-file
|
|
292
|
+
### Phase 3: Remove group-join, ad-hoc RPC; replace output-file (#49, #61)
|
|
312
293
|
|
|
313
294
|
Deleted `group-join.ts`, `cross-extension-rpc.ts` (#49).
|
|
314
295
|
Replaced `output-file.ts` with `SessionManager.create()` + `session-dir.ts` (#61).
|
|
315
296
|
Simplified `index.ts` to use direct individual notifications.
|
|
316
297
|
Lifecycle events emitted on `pi.events` for external consumers.
|
|
317
298
|
|
|
318
|
-
### Phase 4: Implement and publish `SubagentsService`
|
|
299
|
+
### Phase 4: Implement and publish `SubagentsService` (#48)
|
|
319
300
|
|
|
320
301
|
Wired `service-adapter.ts` to wrap `AgentManager` and call `publishSubagentsService()` at extension init.
|
|
321
302
|
Model strings are resolved inside the adapter.
|
|
322
303
|
|
|
323
|
-
### Phase 5: Decompose `index.ts`
|
|
304
|
+
### Phase 5: Decompose `index.ts` (#54, #69, #70, #87)
|
|
324
305
|
|
|
325
306
|
Extracted tools, notifications, activity tracking, event handlers, and the `/agents` command into separate modules.
|
|
326
307
|
Created `SubagentRuntime` factory to hold session-scoped state.
|
|
327
|
-
`src/index.ts` shrank from ~1,894 lines to ~274 lines.
|
|
328
308
|
|
|
329
309
|
### Phase 6 (future): Extract UI to `@gotgenes/pi-subagents-ui`
|
|
330
310
|
|
|
331
311
|
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.
|
|
332
312
|
This phase is deferred until the API boundary is proven stable in production.
|
|
333
313
|
|
|
334
|
-
|
|
314
|
+
### Phase 7: Encapsulation and dependency narrowing
|
|
335
315
|
|
|
336
|
-
|
|
316
|
+
Target: every mutable state bag becomes a class, every dependency bag narrows to what its consumer uses, every callback becomes either a method on a collaborator or an event on an observable.
|
|
317
|
+
|
|
318
|
+
The work is sequenced so each change makes the next change easy.
|
|
319
|
+
See the [Encapsulation roadmap](#encapsulation-roadmap) section for the full breakdown.
|
|
320
|
+
|
|
321
|
+
## Structural refactoring roadmap
|
|
322
|
+
|
|
323
|
+
Phases 1–5 are complete.
|
|
324
|
+
Phase 7 (encapsulation and dependency narrowing) is the active structural track.
|
|
337
325
|
See `git log` for the full history; issue references are preserved below for traceability.
|
|
338
326
|
|
|
339
327
|
| Phase | Issue | Summary |
|
|
@@ -345,188 +333,184 @@ See `git log` for the full history; issue references are preserved below for tra
|
|
|
345
333
|
|
|
346
334
|
The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
|
|
347
335
|
|
|
348
|
-
|
|
336
|
+
## AgentManager decomposition
|
|
349
337
|
|
|
350
|
-
|
|
338
|
+
AgentManager was decomposed in three steps to untangle record management, concurrency control, and execution orchestration.
|
|
351
339
|
|
|
352
|
-
|
|
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.
|
|
340
|
+
### Step 1: Record state machine (#98, #102)
|
|
355
341
|
|
|
356
|
-
|
|
342
|
+
Extracted status-transition methods (`markRunning`, `markCompleted`, `markAborted`, `markSteered`, `markError`, `markStopped`, `resetForResume`) onto `AgentRecord`.
|
|
343
|
+
Replaced scattered field writes across 6 sites with encapsulated transition methods.
|
|
344
|
+
Issue #102 consolidated test `AgentRecord` construction into a shared factory.
|
|
357
345
|
|
|
358
|
-
|
|
359
|
-
Every concern passes through it because it owns the `AgentRecord`.
|
|
346
|
+
### Step 2: Parent snapshot (#99)
|
|
360
347
|
|
|
361
|
-
|
|
348
|
+
Replaced live `ctx: ExtensionContext` capture in `SpawnArgs` with an immutable `ParentSnapshot` data object.
|
|
349
|
+
The snapshot is taken once at spawn time; queued agents execute against frozen state rather than a potentially stale session reference.
|
|
350
|
+
`runAgent()` accepts `ParentSnapshot` instead of `ctx`.
|
|
351
|
+
`pi: ExtensionAPI` was removed from `SpawnArgs` — `runAgent()` accepts a `ShellExec` function instead.
|
|
362
352
|
|
|
363
|
-
|
|
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.
|
|
353
|
+
### Step 3: Session-event observation (#100)
|
|
366
354
|
|
|
367
|
-
|
|
368
|
-
|
|
355
|
+
Replaced three-layer callback threading with direct session subscriptions.
|
|
356
|
+
`record-observer.ts` subscribes to the session to update record statistics (tool uses, lifetime usage, compaction count).
|
|
357
|
+
`ui/ui-observer.ts` subscribes to the session to stream UI state (active tools, response text, turn count).
|
|
358
|
+
`SpawnOptions` and `RunOptions` dropped all `on*` callback fields except `onSessionCreated` (which delivers the session object to enable external subscriptions).
|
|
369
359
|
|
|
370
|
-
|
|
360
|
+
### Realized impact
|
|
371
361
|
|
|
372
|
-
|
|
362
|
+
| Metric | Before | After |
|
|
363
|
+
| --------------------------------- | ------ | ----------------------- |
|
|
364
|
+
| `SpawnOptions` callback fields | 6 | 1 (`onSessionCreated`) |
|
|
365
|
+
| `RunOptions` callback fields | 6 | 1 (`onSessionCreated`) |
|
|
366
|
+
| Callback layers | 3 | 0 (direct subscription) |
|
|
367
|
+
| Live `ctx` references in queue | 1 | 0 (snapshot) |
|
|
368
|
+
| Scattered status-transition sites | 6 | 1 (state machine) |
|
|
373
369
|
|
|
374
|
-
|
|
375
|
-
They thread through three layers:
|
|
376
|
-
|
|
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
|
-
```
|
|
370
|
+
---
|
|
382
371
|
|
|
383
|
-
|
|
372
|
+
## Encapsulation roadmap
|
|
384
373
|
|
|
385
|
-
|
|
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.
|
|
374
|
+
This section describes the Phase 7 targets: encapsulating mutable state into classes, replacing callbacks with semantic components, and narrowing dependency bags.
|
|
389
375
|
|
|
390
|
-
|
|
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.
|
|
376
|
+
Each step is sequenced so it makes the next step easier.
|
|
393
377
|
|
|
394
|
-
###
|
|
378
|
+
### Current smells
|
|
395
379
|
|
|
396
|
-
|
|
397
|
-
|
|
380
|
+
| Smell | Location | Evidence |
|
|
381
|
+
| -------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
382
|
+
| ~~Global mutable state~~ | ~~`agent-types.ts`~~ | **Fixed #108**: `AgentTypeRegistry` class; `reloadCustomAgents` callback removed from `AgentToolDeps` and `AgentMenuDeps` |
|
|
383
|
+
| Closure bag as class | `createNotificationSystem()` | Returns 4 functions sharing closure state (`pendingNudges`, timers) |
|
|
384
|
+
| Mutable state bag | `AgentActivity` (7 fields) | Written by `ui-observer.ts`, read by widget, notification, agent-tool |
|
|
385
|
+
| ~~Settings relay~~ | ~~`AgentMenuDeps` (13 fields)~~ | **Fixed #109**: `SettingsManager` class; 6 callback fields collapsed to `settings: SettingsManager`; `AgentMenuDeps` now 8 fields |
|
|
386
|
+
| Post-construction mutation | `AgentRecord` non-transition state | `session`, `outputFile`, `worktree`, `promise` written by external code after construction |
|
|
387
|
+
| Fire-and-forget callbacks | `AgentManagerOptions` | `onStart`, `onComplete`, `onCompact` wired as closures in `index.ts` |
|
|
388
|
+
| Duplicate `SpawnOptions` | `service.ts` + `agent-manager.ts` | Two incompatible shapes (JSON-friendly vs runtime types) with the same name |
|
|
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
|
+
| Wide dependency bags | `AgentToolDeps` (7), `AgentMenuDeps` (8) | Settings narrowed (#109); registry narrowed (#108); more narrowing planned in D steps |
|
|
398
391
|
|
|
399
|
-
|
|
400
|
-
const args: SpawnArgs = { pi, ctx, type, prompt, options };
|
|
401
|
-
this.queue.push({ id, args }); // ctx held until dequeue
|
|
402
|
-
```
|
|
392
|
+
### Step A: Extract state into classes (foundation, parallel)
|
|
403
393
|
|
|
404
|
-
|
|
394
|
+
These three extractions are independent and can proceed in any order.
|
|
395
|
+
Each eliminates a category of global/closure state and gives orphaned callbacks a natural home.
|
|
405
396
|
|
|
406
|
-
|
|
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.
|
|
397
|
+
#### A1. `AgentTypeRegistry` class (#108)
|
|
410
398
|
|
|
411
|
-
|
|
412
|
-
|
|
399
|
+
Wrap the module-scoped `agents` Map and free functions in `agent-types.ts` into an injectable class.
|
|
400
|
+
`reloadCustomAgents` (currently a callback threaded through `AgentToolDeps` and `AgentMenuDeps`) becomes `registry.reload()`.
|
|
401
|
+
`DEFAULT_AGENT_NAMES` moves from `types.ts` to the registry.
|
|
413
402
|
|
|
414
|
-
|
|
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.
|
|
403
|
+
Impact: eliminates global mutable state, enables test isolation without module resets, removes `reloadCustomAgents` callback from 2 dependency bags.
|
|
416
404
|
|
|
417
|
-
|
|
405
|
+
#### ~~A2. `SettingsManager` class (#109)~~ — **Done**
|
|
418
406
|
|
|
419
|
-
|
|
407
|
+
Encapsulated settings load/save/apply cycle into `SettingsManager` (in `settings.ts`).
|
|
408
|
+
Owns `defaultMaxTurns`, `graceTurns`, `maxConcurrent` with normalizing property accessors.
|
|
409
|
+
Absorbed `SettingsAppliers`, `applyAndEmitLoaded`, `saveAndEmitChanged`.
|
|
410
|
+
The 6 settings-related fields in `AgentMenuDeps` collapsed to `settings: AgentMenuSettings`.
|
|
411
|
+
`AgentManager` reads `maxConcurrent` via injected `getMaxConcurrent` function.
|
|
412
|
+
`SubagentRuntime.defaultMaxTurns` and `.graceTurns` removed.
|
|
420
413
|
|
|
421
|
-
|
|
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
|
-
}
|
|
429
|
-
```
|
|
414
|
+
Impact: reduced `AgentMenuDeps` from 13 → 8 fields; `AgentToolDeps` from 8 → 7 fields.
|
|
430
415
|
|
|
431
|
-
|
|
416
|
+
#### A2b. `SettingsManager` apply methods (#118)
|
|
432
417
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
418
|
+
Eliminate the cross-collaborator orchestration in `showSettings`.
|
|
419
|
+
The menu currently mutates `settings`, pokes `manager.notifyConcurrencyChanged()`, then calls `settings.saveAndNotify()` — it knows too much about the consequence chain.
|
|
420
|
+
Add `applyMaxConcurrent(n)`, `applyDefaultMaxTurns(n)`, `applyGraceTurns(n)` methods that own the full lifecycle: normalize → apply → notify interested parties (via callback) → persist → emit event → return toast.
|
|
421
|
+
`SettingsManager` accepts an `onMaxConcurrentChanged` callback wired to `manager.notifyConcurrencyChanged()` at init.
|
|
422
|
+
`notifyConcurrencyChanged` disappears from `AgentMenuManager`.
|
|
437
423
|
|
|
438
|
-
|
|
439
|
-
`buildParentContext()` also reads once and produces a string.
|
|
440
|
-
The snapshot formalizes what is already happening, and makes the "read once" guarantee structural.
|
|
424
|
+
Impact: eliminates LoD / Tell-Don't-Ask violation; menu no longer coordinates between settings and manager.
|
|
441
425
|
|
|
442
|
-
|
|
426
|
+
#### A3. `AgentActivityTracker` class (#110)
|
|
443
427
|
|
|
444
|
-
|
|
445
|
-
|
|
428
|
+
Wrap the 7-field mutable `AgentActivity` interface with transition methods (`onToolStart()`, `onToolEnd()`, `onMessageUpdate()`, `onTurnEnd()`).
|
|
429
|
+
`ui-observer.ts` calls tracker methods instead of writing raw fields.
|
|
430
|
+
The notification system, widget, and agent-tool receive a proper collaborator instead of reaching into a shared `Map<string, AgentActivity>`.
|
|
446
431
|
|
|
447
|
-
|
|
432
|
+
Impact: eliminates output-argument writes in `ui-observer.ts`, makes the mutation contract explicit.
|
|
448
433
|
|
|
449
|
-
|
|
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
|
|
458
|
-
```
|
|
459
|
-
|
|
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.
|
|
434
|
+
### Step B: Split `AgentRecord` lifecycle state (#111)
|
|
463
435
|
|
|
464
|
-
`
|
|
465
|
-
|
|
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).
|
|
436
|
+
`AgentRecord` is currently constructed in `spawn()` before most of its state exists, then mutated across 4 files as information trickles in.
|
|
437
|
+
The fix is not setter methods — it's splitting along lifecycle boundaries so each object is born complete.
|
|
467
438
|
|
|
468
|
-
|
|
439
|
+
- **`AgentRecord`** stays as identity + status state machine (what we know at spawn time).
|
|
440
|
+
- **Execution state** (`session`, `promise`, `outputFile`) → a new object constructed when the runner creates the session, injected as a complete collaborator.
|
|
441
|
+
- **Worktree state** (`worktree` info, cleanup result) → a new object constructed when isolation is set up, only exists for worktree agents.
|
|
442
|
+
- **`pendingSteers`** moves to a queue on the manager (where they're buffered), not a field on the record.
|
|
469
443
|
|
|
470
|
-
|
|
471
|
-
|
|
444
|
+
Each piece is born complete at the moment its information is available.
|
|
445
|
+
The record doesn't accumulate half-baked state — it receives fully constructed collaborators.
|
|
472
446
|
|
|
473
|
-
|
|
447
|
+
### Step C: Replace `AgentManager` callbacks with observer (#112)
|
|
474
448
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
record.markStopped()
|
|
480
|
-
record.resetForResume()
|
|
481
|
-
```
|
|
449
|
+
Replace the `onStart`/`onComplete`/`onCompact` callback parameters with an `AgentManagerObserver` interface (or typed event emitter).
|
|
450
|
+
The observer methods receive the same data the callbacks receive today.
|
|
451
|
+
`index.ts` constructs the observer once instead of building 3 closure callbacks that capture `runtime`, `pi`, `notifications`, etc.
|
|
452
|
+
`AgentManagerOptions` drops from 8 → 5 fields.
|
|
482
453
|
|
|
483
|
-
|
|
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.
|
|
454
|
+
### Step D: Disambiguate `SpawnOptions` and narrow dependency bags
|
|
486
455
|
|
|
487
|
-
|
|
456
|
+
With the registry class, settings manager, and observer in place, the dependency bags shrink naturally.
|
|
488
457
|
|
|
489
|
-
|
|
490
|
-
The recommended sequence minimizes intermediate churn.
|
|
458
|
+
#### D1. Disambiguate `SpawnOptions` (#113)
|
|
491
459
|
|
|
492
|
-
|
|
460
|
+
Rename the internal `SpawnOptions` in `agent-manager.ts` to `AgentSpawnConfig` (or similar) to distinguish it from the JSON-friendly public `SpawnOptions` in `service.ts`.
|
|
461
|
+
The two types serve different consumers and should not share a name.
|
|
493
462
|
|
|
494
|
-
|
|
495
|
-
Purely mechanical — replace scattered field writes with method calls.
|
|
496
|
-
No interface changes for callers.
|
|
463
|
+
#### D2. Narrow `AgentToolDeps` and `AgentMenuDeps` (#114)
|
|
497
464
|
|
|
498
|
-
|
|
465
|
+
| Bag | Before | After | How |
|
|
466
|
+
| --------------- | --------- | ----- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
467
|
+
| `AgentToolDeps` | 9 fields | ~5 | Registry owns reload; activity tracker is a collaborator; `emitEvent` moves to observer |
|
|
468
|
+
| `AgentMenuDeps` | 13 fields | ~6 | Settings manager absorbs 6 fields (#109); apply methods remove `notifyConcurrencyChanged` (#118); registry owns reload |
|
|
499
469
|
|
|
500
|
-
|
|
470
|
+
### Step E: Decompose large files and relocate types (parallel)
|
|
501
471
|
|
|
502
|
-
####
|
|
472
|
+
#### E1. Split `agent-tool.ts` foreground/background (#115)
|
|
503
473
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
Update `runner.run()` signature to accept `ParentSnapshot` instead of `ctx`.
|
|
507
|
-
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).
|
|
474
|
+
Extract the foreground execution loop (spinner, streaming, result rendering) and background spawn path into separate modules.
|
|
475
|
+
The 654-line file splits along a natural seam.
|
|
508
476
|
|
|
509
|
-
|
|
477
|
+
#### E2. Type housekeeping (#116)
|
|
510
478
|
|
|
511
|
-
|
|
479
|
+
- Move `NotificationDetails` from `types.ts` to `notification.ts`.
|
|
480
|
+
- Move `DEFAULT_AGENT_NAMES` from `types.ts` to the registry.
|
|
481
|
+
- Move `ParentSnapshot` from `types.ts` to `parent-snapshot.ts`.
|
|
482
|
+
- Move `EnvInfo` from `types.ts` to `env.ts`.
|
|
483
|
+
- Convert `createNotificationSystem` closure to `NotificationManager` class.
|
|
484
|
+
- Convert `ConversationViewer` constructor from 6 positional parameters to an options bag.
|
|
485
|
+
- Define narrow `AgentConfig` subset interfaces for consumers that use 2–4 fields of the 21-field type.
|
|
512
486
|
|
|
513
|
-
|
|
514
|
-
AgentManager subscribes to the session after creation to update the record.
|
|
515
|
-
The agent-tool subscribes to the session after spawn to stream UI state.
|
|
516
|
-
`RunOptions` and `SpawnOptions` drop all `on*` callback fields.
|
|
487
|
+
### Expected impact
|
|
517
488
|
|
|
518
|
-
|
|
489
|
+
| Metric | Before | After |
|
|
490
|
+
| ------------------------------------------ | ---------------------------------------------------------------------------- | ------- |
|
|
491
|
+
| Module-scoped mutable state | ~~1 (`agent-types.ts` Map)~~ | **0** ✓ |
|
|
492
|
+
| Closure-bag "classes" | ~~2~~ 1 (`createNotificationSystem`; settings free functions **fixed #109**) | 0 |
|
|
493
|
+
| Externally-mutated state bags | 2 (`AgentActivity`, `AgentRecord` non-transition fields) | 0 |
|
|
494
|
+
| `AgentManagerOptions` fields | 8 | 5 |
|
|
495
|
+
| `AgentToolDeps` fields | ~~9~~ **7** (−6 registry #108, −1 settings #109 → +1 settings obj) | ~5 |
|
|
496
|
+
| `AgentMenuDeps` fields | ~~13~~ **8** (−6 settings #109 collapsed to 1; −1 registry #108) | ~6 ✓ |
|
|
497
|
+
| `SpawnOptions` callback fields | 1 (`onSessionCreated`) | 0 |
|
|
498
|
+
| Callbacks threaded through deps | ~~8~~ 0 remaining settings callbacks (**fixed #109**); `emitEvent` ×3 remain | 0 |
|
|
499
|
+
| Types in `types.ts` without a natural home | 4 | 0 |
|
|
519
500
|
|
|
520
|
-
###
|
|
501
|
+
### Dependency graph
|
|
521
502
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
503
|
+
```text
|
|
504
|
+
A1 (Registry) ──────────────────┐
|
|
505
|
+
A2 (Settings) ── A2b (Apply) ──┤
|
|
506
|
+
A3 (Activity Tracker) ───────────┤
|
|
507
|
+
├── D2 (Narrow deps) ── E1 (agent-tool split)
|
|
508
|
+
B (Record lifecycle) ───────────┤
|
|
509
|
+
└── C (Observer) ────────────┤
|
|
510
|
+
└── D1 (SpawnOptions) ──┘
|
|
511
|
+
|
|
512
|
+
E2 (Type housekeeping) ── can start after A1, runs parallel to later steps
|
|
513
|
+
```
|
|
530
514
|
|
|
531
515
|
---
|
|
532
516
|
|