@gotgenes/pi-subagents 6.17.0 → 6.17.1
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 +12 -0
- package/docs/architecture/architecture.md +588 -535
- package/docs/architecture/history/phase-1-api-boundary.md +8 -0
- package/docs/architecture/history/phase-2-remove-scheduling.md +9 -0
- package/docs/architecture/history/phase-3-remove-rpc-groupjoin.md +11 -0
- package/docs/architecture/history/phase-4-implement-service.md +8 -0
- package/docs/architecture/history/phase-5-decompose-index.md +42 -0
- package/docs/architecture/history/phase-7-encapsulation.md +173 -0
- package/docs/architecture/history/phase-8-testability.md +103 -0
- package/docs/architecture/history/phase-9-observation-ctx.md +122 -0
- package/docs/retro/0147-inject-wrap-text-into-conversation-viewer.md +40 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +11 -11
- package/src/agent-record.ts +6 -6
- package/src/agent-runner.ts +6 -6
- package/src/agent-types.ts +2 -2
- package/src/custom-agents.ts +3 -3
- package/src/default-agents.ts +1 -1
- package/src/env.ts +2 -2
- package/src/handlers/index.ts +2 -2
- package/src/index.ts +26 -26
- package/src/invocation-config.ts +1 -1
- package/src/memory.ts +2 -2
- package/src/notification.ts +4 -4
- package/src/parent-snapshot.ts +1 -1
- package/src/prompts.ts +2 -2
- package/src/record-observer.ts +2 -2
- package/src/renderer.ts +2 -2
- package/src/runtime.ts +2 -2
- package/src/service-adapter.ts +5 -5
- package/src/service.ts +1 -1
- package/src/session-config.ts +5 -5
- package/src/skill-loader.ts +2 -2
- package/src/tools/agent-tool.ts +11 -11
- package/src/tools/background-spawner.ts +8 -8
- package/src/tools/foreground-runner.ts +9 -9
- package/src/tools/get-result-tool.ts +5 -5
- package/src/tools/helpers.ts +4 -4
- package/src/tools/spawn-config.ts +6 -6
- package/src/tools/steer-tool.ts +3 -3
- package/src/types.ts +1 -1
- package/src/ui/agent-activity-tracker.ts +1 -1
- package/src/ui/agent-config-editor.ts +4 -4
- package/src/ui/agent-creation-wizard.ts +5 -5
- package/src/ui/agent-menu.ts +10 -10
- package/src/ui/agent-widget.ts +5 -5
- package/src/ui/conversation-viewer.ts +6 -6
- package/src/ui/display.ts +2 -2
- package/src/ui/ui-observer.ts +1 -1
- package/src/ui/widget-renderer.ts +5 -5
- package/src/worktree-state.ts +1 -1
- package/src/worktree.ts +1 -1
- package/vitest.config.ts +14 -0
|
@@ -4,92 +4,365 @@ This document describes the architecture of the pi-subagents fork: a focused, co
|
|
|
4
4
|
|
|
5
5
|
## Design principles
|
|
6
6
|
|
|
7
|
-
1. **Narrow core**
|
|
7
|
+
1. **Narrow core** — the extension owns agent spawning, execution, and result retrieval.
|
|
8
8
|
Everything else is a consumer.
|
|
9
|
-
2. **Composable by default**
|
|
10
|
-
3. **Typed API boundary**
|
|
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 `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("@gotgenes/pi-subagents:service")` on `globalThis`
|
|
13
|
-
4. **No scheduling**
|
|
12
|
+
The runtime bridge is `Symbol.for("@gotgenes/pi-subagents:service")` on `globalThis` — no separate API package.
|
|
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
|
-
5. **UI extraction is deferred**
|
|
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**
|
|
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**
|
|
20
|
-
8. **Construct complete**
|
|
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
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
|
|
23
|
-
9. **State owns its mutations**
|
|
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
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.
|
|
25
25
|
|
|
26
|
-
##
|
|
26
|
+
## Domain model
|
|
27
27
|
|
|
28
|
-
The extension is organized
|
|
28
|
+
The extension is organized around six domains, each responsible for one aspect of managing agents.
|
|
29
|
+
|
|
30
|
+
```mermaid
|
|
31
|
+
flowchart TB
|
|
32
|
+
subgraph config["Config domain"]
|
|
33
|
+
direction TB
|
|
34
|
+
AgentTypeRegistry["AgentTypeRegistry\n(registry of agent types)"]
|
|
35
|
+
DefaultAgents["default-agents\n(built-in types)"]
|
|
36
|
+
CustomAgents["custom-agents\n(user .md files)"]
|
|
37
|
+
InvocationConfig["invocation-config\n(per-call merge)"]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
subgraph session["Session domain"]
|
|
41
|
+
direction TB
|
|
42
|
+
SessionConfig["assembleSessionConfig\n(pure assembler)"]
|
|
43
|
+
Prompts["prompts\n(system prompt)"]
|
|
44
|
+
Context["context\n(parent history)"]
|
|
45
|
+
Memory["memory\n(MEMORY.md)"]
|
|
46
|
+
SkillLoader["skill-loader\n(preload skills)"]
|
|
47
|
+
Env["env\n(git/platform)"]
|
|
48
|
+
ModelResolver["model-resolver\n(fuzzy match)"]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
subgraph lifecycle["Lifecycle domain"]
|
|
52
|
+
direction TB
|
|
53
|
+
AgentManager["AgentManager\n(spawn, queue, abort)"]
|
|
54
|
+
AgentRunner["agent-runner\n(session, turns, results)"]
|
|
55
|
+
AgentRecord["AgentRecord\n(status state machine)"]
|
|
56
|
+
ParentSnapshot["ParentSnapshot\n(frozen parent state)"]
|
|
57
|
+
Worktree["worktree\n(git isolation)"]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
subgraph observation["Observation domain"]
|
|
61
|
+
direction TB
|
|
62
|
+
RecordObserver["record-observer\n(stats via events)"]
|
|
63
|
+
Notification["notification\n(completion nudges)"]
|
|
64
|
+
UIObserver["ui-observer\n(streaming state)"]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
subgraph tools["Tools domain"]
|
|
68
|
+
direction TB
|
|
69
|
+
AgentTool["Agent tool\n(dispatch)"]
|
|
70
|
+
SpawnConfig["spawn-config\n(resolve params)"]
|
|
71
|
+
FgRunner["foreground-runner"]
|
|
72
|
+
BgSpawner["background-spawner"]
|
|
73
|
+
GetResult["get_subagent_result"]
|
|
74
|
+
Steer["steer_subagent"]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
subgraph ui["UI domain"]
|
|
78
|
+
direction TB
|
|
79
|
+
Widget["agent-widget\n(live status)"]
|
|
80
|
+
ConvViewer["conversation-viewer\n(session overlay)"]
|
|
81
|
+
Menu["agent-menu\n(slash command)"]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
AgentTool --> AgentManager
|
|
85
|
+
AgentManager --> AgentRunner
|
|
86
|
+
AgentRunner --> SessionConfig
|
|
87
|
+
SessionConfig --> AgentTypeRegistry
|
|
88
|
+
SessionConfig --> Prompts & Memory & SkillLoader & Env
|
|
89
|
+
AgentTypeRegistry --> DefaultAgents & CustomAgents
|
|
90
|
+
RecordObserver -.->|subscribes| AgentRunner
|
|
91
|
+
UIObserver -.->|subscribes| AgentRunner
|
|
92
|
+
Widget -.->|polls| AgentManager
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Key domain types
|
|
96
|
+
|
|
97
|
+
```mermaid
|
|
98
|
+
classDiagram
|
|
99
|
+
class AgentRecord {
|
|
100
|
+
+id: string
|
|
101
|
+
+type: SubagentType
|
|
102
|
+
+description: string
|
|
103
|
+
+status: AgentRecordStatus
|
|
104
|
+
+result?: string
|
|
105
|
+
+error?: string
|
|
106
|
+
+toolUses: number
|
|
107
|
+
+lifetimeUsage: LifetimeUsage
|
|
108
|
+
+execution?: ExecutionState
|
|
109
|
+
+worktreeState?: WorktreeState
|
|
110
|
+
+notification?: NotificationState
|
|
111
|
+
+markRunning()
|
|
112
|
+
+markCompleted()
|
|
113
|
+
+markAborted()
|
|
114
|
+
+markSteered()
|
|
115
|
+
+markError()
|
|
116
|
+
+markStopped()
|
|
117
|
+
+resetForResume()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
class AgentManager {
|
|
121
|
+
+spawn(snapshot, type, prompt, config)
|
|
122
|
+
+spawnAndWait(snapshot, type, prompt, config)
|
|
123
|
+
+resume(id, snapshot, exec)
|
|
124
|
+
+getRecord(id): AgentRecord
|
|
125
|
+
+listAgents(): AgentRecord[]
|
|
126
|
+
+abort(id)
|
|
127
|
+
+queueSteer(id, message)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class AgentTypeRegistry {
|
|
131
|
+
+resolveType(type): string
|
|
132
|
+
+resolveAgentConfig(type): AgentConfig
|
|
133
|
+
+reload()
|
|
134
|
+
+getToolNamesForType(type): string[]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
class ParentSnapshot {
|
|
138
|
+
+cwd: string
|
|
139
|
+
+systemPrompt: string
|
|
140
|
+
+model: unknown
|
|
141
|
+
+modelRegistry: unknown
|
|
142
|
+
+parentContext?: string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
class SubagentsService {
|
|
146
|
+
+spawn(type, prompt, options?)
|
|
147
|
+
+getRecord(id): SubagentRecord
|
|
148
|
+
+listAgents(): SubagentRecord[]
|
|
149
|
+
+abort(id)
|
|
150
|
+
+steer(id, message)
|
|
151
|
+
+waitForAll()
|
|
152
|
+
+hasRunning(): boolean
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
AgentManager --> AgentRecord : creates/manages
|
|
156
|
+
AgentManager --> ParentSnapshot : receives at spawn
|
|
157
|
+
SubagentsService --> AgentManager : wraps via adapter
|
|
158
|
+
AgentManager --> AgentTypeRegistry : resolves types
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Agent lifecycle
|
|
162
|
+
|
|
163
|
+
```mermaid
|
|
164
|
+
stateDiagram-v2
|
|
165
|
+
[*] --> queued : spawn (background, at capacity)
|
|
166
|
+
[*] --> running : spawn (foreground or under limit)
|
|
167
|
+
queued --> running : capacity available
|
|
168
|
+
running --> completed : all turns finished
|
|
169
|
+
running --> error : unhandled exception
|
|
170
|
+
running --> aborted : abort() called
|
|
171
|
+
running --> stopped : max turns reached
|
|
172
|
+
running --> steered : steer message injected
|
|
173
|
+
steered --> running : continues with message
|
|
174
|
+
completed --> running : resetForResume
|
|
175
|
+
stopped --> running : resetForResume
|
|
176
|
+
error --> running : resetForResume
|
|
177
|
+
aborted --> running : resetForResume
|
|
178
|
+
completed --> [*]
|
|
179
|
+
error --> [*]
|
|
180
|
+
aborted --> [*]
|
|
181
|
+
stopped --> [*]
|
|
182
|
+
|
|
183
|
+
note right of running
|
|
184
|
+
markCompleted, markAborted,
|
|
185
|
+
markSteered, and markError
|
|
186
|
+
are no-ops when status is stopped
|
|
187
|
+
end note
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Note: `markStopped` always succeeds regardless of current status.
|
|
191
|
+
Other terminal transitions guard against overwriting `stopped` — once an agent is stopped, only `resetForResume` can return it to `running`.
|
|
192
|
+
|
|
193
|
+
## Execution flow
|
|
194
|
+
|
|
195
|
+
```mermaid
|
|
196
|
+
sequenceDiagram
|
|
197
|
+
participant LLM as Parent LLM
|
|
198
|
+
participant Tool as Agent tool
|
|
199
|
+
participant Spawn as spawn-config
|
|
200
|
+
participant Mgr as AgentManager
|
|
201
|
+
participant Runner as agent-runner
|
|
202
|
+
participant Asm as assembleSessionConfig
|
|
203
|
+
participant Child as Child session
|
|
204
|
+
|
|
205
|
+
LLM->>Tool: Agent(type, prompt, ...)
|
|
206
|
+
Tool->>Spawn: resolveSpawnConfig(params)
|
|
207
|
+
Spawn-->>Tool: ResolvedSpawnConfig
|
|
208
|
+
Tool->>Mgr: spawn(snapshot, type, prompt, config)
|
|
209
|
+
Mgr->>Runner: runAgent(record, snapshot, options, io)
|
|
210
|
+
Runner->>Asm: assembleSessionConfig(type, ctx, opts, env, registry, io)
|
|
211
|
+
Asm-->>Runner: SessionConfig
|
|
212
|
+
Runner->>Child: create session + run turn loop
|
|
213
|
+
Child-->>Runner: result text
|
|
214
|
+
Runner-->>Mgr: update AgentRecord
|
|
215
|
+
Note over Mgr: record-observer subscribes to session events for stats
|
|
216
|
+
Note over Mgr: ui-observer subscribes for streaming state
|
|
217
|
+
Mgr-->>Tool: AgentRecord
|
|
218
|
+
Tool-->>LLM: formatted result
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Module organization
|
|
222
|
+
|
|
223
|
+
The extension has 53 source files (7,288 LOC) organized into six domains plus entry-point wiring.
|
|
224
|
+
The three existing subdirectories (`tools/`, `ui/`, `handlers/`) reflect three of these domains.
|
|
225
|
+
The remaining 31 root-level files span four more domains but are not yet grouped in the filesystem.
|
|
226
|
+
|
|
227
|
+
### Current layout
|
|
228
|
+
|
|
229
|
+
```text
|
|
230
|
+
src/
|
|
231
|
+
├── index.ts entry point, tool registration, event wiring
|
|
232
|
+
├── runtime.ts SubagentRuntime factory (session-scoped state)
|
|
233
|
+
├── types.ts shared type definitions
|
|
234
|
+
├── settings.ts SettingsManager (persistent operational settings)
|
|
235
|
+
├── debug.ts debug logging utility
|
|
236
|
+
│
|
|
237
|
+
│ ── Config domain (agent type definitions and resolution) ──
|
|
238
|
+
├── agent-types.ts AgentTypeRegistry class
|
|
239
|
+
├── default-agents.ts built-in agent configs (general-purpose, Explore, Plan)
|
|
240
|
+
├── custom-agents.ts user-defined agent .md file loader
|
|
241
|
+
├── invocation-config.ts per-call config merge
|
|
242
|
+
│
|
|
243
|
+
│ ── Session domain (session assembly and preparation) ──
|
|
244
|
+
├── session-config.ts pure assembler (main entry)
|
|
245
|
+
├── prompts.ts system prompt building
|
|
246
|
+
├── context.ts parent conversation extraction
|
|
247
|
+
├── memory.ts persistent MEMORY.md per agent
|
|
248
|
+
├── skill-loader.ts skill preloading
|
|
249
|
+
├── env.ts git/platform detection
|
|
250
|
+
├── model-resolver.ts fuzzy model name resolution
|
|
251
|
+
├── session-dir.ts session directory derivation
|
|
252
|
+
│
|
|
253
|
+
│ ── Lifecycle domain (agent execution and state) ──
|
|
254
|
+
├── agent-manager.ts spawn, queue, abort, resume, concurrency
|
|
255
|
+
├── agent-runner.ts session creation, turn loop, tool filtering
|
|
256
|
+
├── agent-record.ts status state machine
|
|
257
|
+
├── parent-snapshot.ts immutable spawn-time parent state
|
|
258
|
+
├── execution-state.ts session/output phase state
|
|
259
|
+
├── worktree.ts git worktree isolation
|
|
260
|
+
├── worktree-state.ts worktree phase state
|
|
261
|
+
├── usage.ts token usage tracking
|
|
262
|
+
│
|
|
263
|
+
│ ── Observation domain (progress tracking and notification) ──
|
|
264
|
+
├── record-observer.ts session-event stats observer
|
|
265
|
+
├── notification.ts completion nudges
|
|
266
|
+
├── notification-state.ts per-agent notification tracking
|
|
267
|
+
├── renderer.ts notification TUI component
|
|
268
|
+
│
|
|
269
|
+
│ ── Service domain (cross-extension API) ──
|
|
270
|
+
├── service.ts SubagentsService interface + Symbol.for() accessors
|
|
271
|
+
├── service-adapter.ts SubagentsService wrapper around AgentManager
|
|
272
|
+
│
|
|
273
|
+
│ ── Tools domain (LLM-facing tool implementations) ──
|
|
274
|
+
├── tools/
|
|
275
|
+
│ ├── agent-tool.ts Agent tool definition, validation, dispatch
|
|
276
|
+
│ ├── spawn-config.ts pure config resolution
|
|
277
|
+
│ ├── foreground-runner.ts foreground execution loop
|
|
278
|
+
│ ├── background-spawner.ts background spawn setup
|
|
279
|
+
│ ├── get-result-tool.ts get_subagent_result tool
|
|
280
|
+
│ ├── steer-tool.ts steer_subagent tool
|
|
281
|
+
│ └── helpers.ts shared tool utilities
|
|
282
|
+
│
|
|
283
|
+
│ ── UI domain (user-facing presentation) ──
|
|
284
|
+
├── ui/
|
|
285
|
+
│ ├── agent-widget.ts above-editor live status widget
|
|
286
|
+
│ ├── widget-renderer.ts pure rendering for widget
|
|
287
|
+
│ ├── agent-menu.ts /agents slash command menu
|
|
288
|
+
│ ├── agent-config-editor.ts agent detail/edit view
|
|
289
|
+
│ ├── agent-creation-wizard.ts agent creation (AI + manual)
|
|
290
|
+
│ ├── conversation-viewer.ts scrollable session overlay
|
|
291
|
+
│ ├── agent-activity-tracker.ts live activity state tracker
|
|
292
|
+
│ ├── agent-file-ops.ts filesystem abstraction
|
|
293
|
+
│ ├── ui-observer.ts session-event observer for streaming
|
|
294
|
+
│ └── display.ts pure formatters and shared types
|
|
295
|
+
│
|
|
296
|
+
│ ── Event handlers ──
|
|
297
|
+
└── handlers/
|
|
298
|
+
├── lifecycle.ts session_start, session_before_switch, session_shutdown
|
|
299
|
+
└── tool-start.ts tool_execution_start handler
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Proposed directory restructuring
|
|
303
|
+
|
|
304
|
+
Move the four ungrouped domains into subdirectories so the filesystem mirrors the domain model.
|
|
305
|
+
Root-level files stay: `index.ts` (entry point), `runtime.ts` (wiring), `types.ts` (shared), `settings.ts`, `debug.ts`.
|
|
29
306
|
|
|
30
307
|
```text
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
notification.ts
|
|
68
|
-
renderer.ts
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
ui/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
default-agents.ts - embedded default agent configs (general-purpose, Explore, Plan)
|
|
79
|
-
custom-agents.ts - user-defined agent .md file loader
|
|
80
|
-
debug.ts - debug logging utility
|
|
308
|
+
src/
|
|
309
|
+
├── index.ts
|
|
310
|
+
├── runtime.ts
|
|
311
|
+
├── types.ts
|
|
312
|
+
├── settings.ts
|
|
313
|
+
├── debug.ts
|
|
314
|
+
│
|
|
315
|
+
├── config/ agent type definitions and resolution
|
|
316
|
+
│ ├── agent-types.ts
|
|
317
|
+
│ ├── default-agents.ts
|
|
318
|
+
│ ├── custom-agents.ts
|
|
319
|
+
│ └── invocation-config.ts
|
|
320
|
+
│
|
|
321
|
+
├── session/ session assembly and preparation
|
|
322
|
+
│ ├── session-config.ts
|
|
323
|
+
│ ├── prompts.ts
|
|
324
|
+
│ ├── context.ts
|
|
325
|
+
│ ├── memory.ts
|
|
326
|
+
│ ├── skill-loader.ts
|
|
327
|
+
│ ├── env.ts
|
|
328
|
+
│ ├── model-resolver.ts
|
|
329
|
+
│ └── session-dir.ts
|
|
330
|
+
│
|
|
331
|
+
├── lifecycle/ agent execution and state tracking
|
|
332
|
+
│ ├── agent-manager.ts
|
|
333
|
+
│ ├── agent-runner.ts
|
|
334
|
+
│ ├── agent-record.ts
|
|
335
|
+
│ ├── parent-snapshot.ts
|
|
336
|
+
│ ├── execution-state.ts
|
|
337
|
+
│ ├── worktree.ts
|
|
338
|
+
│ ├── worktree-state.ts
|
|
339
|
+
│ └── usage.ts
|
|
340
|
+
│
|
|
341
|
+
├── observation/ progress tracking and notification
|
|
342
|
+
│ ├── record-observer.ts
|
|
343
|
+
│ ├── notification.ts
|
|
344
|
+
│ ├── notification-state.ts
|
|
345
|
+
│ └── renderer.ts
|
|
346
|
+
│
|
|
347
|
+
├── service/ cross-extension API boundary
|
|
348
|
+
│ ├── service.ts
|
|
349
|
+
│ └── service-adapter.ts
|
|
350
|
+
│
|
|
351
|
+
├── tools/ (existing)
|
|
352
|
+
├── ui/ (existing)
|
|
353
|
+
└── handlers/ (existing)
|
|
81
354
|
```
|
|
82
355
|
|
|
356
|
+
Root goes from 31 files to 5 files + 8 directories — each directory name tells you what domain it belongs to.
|
|
357
|
+
|
|
83
358
|
### Observation model
|
|
84
359
|
|
|
85
360
|
Record statistics (tool uses, token usage, compaction counts) are updated by `record-observer.ts`, which subscribes directly to session events.
|
|
86
361
|
UI streaming (active tools, response text, turn counts) is handled by `ui/ui-observer.ts`, which subscribes to the same session events independently.
|
|
87
|
-
Neither observer wraps or forwards the other
|
|
362
|
+
Neither observer wraps or forwards the other — both subscribe directly to the session.
|
|
88
363
|
|
|
89
364
|
The widget reads agent state by polling a shared `Map<string, AgentActivityTracker>` on `SubagentRuntime` every 80 ms. The conversation viewer subscribes directly to `AgentSession` objects.
|
|
90
365
|
|
|
91
|
-
Cross-extension consumers use the typed `SubagentsService` API published via `Symbol.for("@gotgenes/pi-subagents:service")` on `globalThis`.
|
|
92
|
-
|
|
93
366
|
## Cross-extension architecture
|
|
94
367
|
|
|
95
368
|
```mermaid
|
|
@@ -98,7 +371,7 @@ flowchart TD
|
|
|
98
371
|
direction TB
|
|
99
372
|
exports["SubagentsService interface\npublish / getSubagentsService()\nSubagentRecord, SubagentStatus, LifetimeUsage\nSUBAGENT_EVENTS constants"]
|
|
100
373
|
engine["Agent + get_subagent_result + steer_subagent tools\nAgentManager, agent-runner, agent-types\npublishSubagentsService() called at init"]
|
|
101
|
-
|
|
374
|
+
ui_int["Internal UI: widget, viewer, /agents menu\n(candidate for extraction to pi-subagents-ui)"]
|
|
102
375
|
end
|
|
103
376
|
|
|
104
377
|
core -- "Symbol.for() on globalThis" --> sched["scheduling extension\n(hypothetical)"]
|
|
@@ -112,32 +385,28 @@ They declare this package as an optional peer dependency and use dynamic import
|
|
|
112
385
|
### What the core owns
|
|
113
386
|
|
|
114
387
|
- The three tools: `Agent`, `get_subagent_result`, `steer_subagent`.
|
|
115
|
-
- `AgentManager`
|
|
116
|
-
- `agent-runner`
|
|
117
|
-
- `session-config`
|
|
118
|
-
- `SubagentRuntime`
|
|
119
|
-
- `ParentSnapshot`
|
|
120
|
-
- `record-observer`
|
|
121
|
-
- Agent type registry
|
|
388
|
+
- `AgentManager` — spawn, queue, abort, resume, concurrency control.
|
|
389
|
+
- `agent-runner` — session creation, turn loop, tool filtering, extension binding (Patches 2 and 3).
|
|
390
|
+
- `session-config` — pure configuration assembler (extracted from `agent-runner`).
|
|
391
|
+
- `SubagentRuntime` — session-scoped state bag with methods.
|
|
392
|
+
- `ParentSnapshot` — immutable snapshot of parent session state, captured once at spawn time.
|
|
393
|
+
- `record-observer` — session-event observer that updates record statistics without callback threading.
|
|
394
|
+
- Agent type registry — default agents, custom `.md` file loading.
|
|
122
395
|
- Prompt assembly, context extraction, memory, skills, environment.
|
|
123
396
|
- Worktree isolation.
|
|
124
397
|
- Token usage tracking.
|
|
125
398
|
- Session directory derivation and persisted `SessionManager` for subagent transcripts.
|
|
126
399
|
- Settings persistence.
|
|
127
|
-
- Internal UI (widget, conversation viewer, `/agents` menu)
|
|
400
|
+
- Internal UI (widget, conversation viewer, `/agents` menu) — these stay until the API boundary is proven, then move to a separate extension.
|
|
128
401
|
|
|
129
402
|
### What the core dropped
|
|
130
403
|
|
|
131
|
-
- **Scheduling** (`schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`)
|
|
132
|
-
|
|
133
|
-
- **
|
|
134
|
-
- **
|
|
135
|
-
|
|
136
|
-
- **
|
|
137
|
-
Subagent transcripts are now written in Pi's official JSONL session format.
|
|
138
|
-
- **Callback threading** - the three-layer `on*` callback chain through `SpawnOptions` → `AgentManager` → `RunOptions` was replaced by direct session-event subscriptions (#100).
|
|
139
|
-
- **Live `ctx` capture** - `SpawnArgs` previously held a mutable `ctx: ExtensionContext` reference that could go stale in the concurrency queue.
|
|
140
|
-
Replaced by `ParentSnapshot`, an immutable data object captured once at spawn time (#99).
|
|
404
|
+
- **Scheduling** (`schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`) — removed (#52).
|
|
405
|
+
- **Ad-hoc RPC** (`cross-extension-rpc.ts`) — replaced by the typed `SubagentsService` published via `Symbol.for()` (#49).
|
|
406
|
+
- **Group join** (`group-join.ts`) — removed (#49).
|
|
407
|
+
- **Output file** (`output-file.ts`) — replaced by `session-dir.ts` + `SessionManager.create()` (#61).
|
|
408
|
+
- **Callback threading** — the three-layer `on*` callback chain was replaced by direct session-event subscriptions (#100).
|
|
409
|
+
- **Live `ctx` capture** — replaced by `ParentSnapshot`, an immutable data object captured once at spawn time (#99).
|
|
141
410
|
|
|
142
411
|
## SubagentsService
|
|
143
412
|
|
|
@@ -176,10 +445,10 @@ The dynamic import provides compile-time types; the `Symbol.for()` key is the ac
|
|
|
176
445
|
See `src/service.ts` for the canonical definition.
|
|
177
446
|
Key types:
|
|
178
447
|
|
|
179
|
-
- `SubagentsService`
|
|
180
|
-
- `SubagentRecord`
|
|
181
|
-
- `SpawnOptions`
|
|
182
|
-
- `SUBAGENT_EVENTS`
|
|
448
|
+
- `SubagentsService` — `spawn`, `getRecord`, `listAgents`, `abort`, `steer`, `waitForAll`, `hasRunning`.
|
|
449
|
+
- `SubagentRecord` — serializable agent snapshot (no live session objects).
|
|
450
|
+
- `SpawnOptions` — `description`, `model`, `maxTurns`, `thinkingLevel`, `isolated`, `inheritContext`, `foreground`, `bypassQueue`, `isolation`.
|
|
451
|
+
- `SUBAGENT_EVENTS` — channel constants for `pi.events` subscriptions.
|
|
183
452
|
|
|
184
453
|
### Accessor pattern
|
|
185
454
|
|
|
@@ -209,527 +478,301 @@ The core emits events on `pi.events` that any extension can observe:
|
|
|
209
478
|
| `subagents:completed` | `{ id, type, status, result?, error? }` | Agent finishes |
|
|
210
479
|
| `subagents:activity` | `{ id, toolName?, textDelta?, turnCount? }` | Streaming progress |
|
|
211
480
|
|
|
212
|
-
These are fire-and-forget broadcast events
|
|
213
|
-
|
|
214
|
-
### Consumer example: scheduling extension
|
|
215
|
-
|
|
216
|
-
```typescript
|
|
217
|
-
export default function (pi) {
|
|
218
|
-
pi.on("session_start", async (event, ctx) => {
|
|
219
|
-
let getSubagentsService;
|
|
220
|
-
try {
|
|
221
|
-
({ getSubagentsService } = await import("@gotgenes/pi-subagents"));
|
|
222
|
-
} catch {
|
|
223
|
-
return; // pi-subagents not installed
|
|
224
|
-
}
|
|
225
|
-
const svc = getSubagentsService();
|
|
226
|
-
if (!svc) return;
|
|
227
|
-
|
|
228
|
-
setInterval(() => {
|
|
229
|
-
svc.spawn("Explore", "Check for stale TODOs", {
|
|
230
|
-
bypassQueue: true,
|
|
231
|
-
});
|
|
232
|
-
}, 60 * 60 * 1000);
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
### Consumer example: transcript extension
|
|
238
|
-
|
|
239
|
-
```typescript
|
|
240
|
-
export default function (pi) {
|
|
241
|
-
pi.events.on("subagents:completed", async (data) => {
|
|
242
|
-
const { id } = data as { id: string };
|
|
243
|
-
let getSubagentsService;
|
|
244
|
-
try {
|
|
245
|
-
({ getSubagentsService } = await import("@gotgenes/pi-subagents"));
|
|
246
|
-
} catch {
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
const record = getSubagentsService()?.getRecord(id);
|
|
250
|
-
if (record?.result) {
|
|
251
|
-
fs.appendFileSync("agent-log.jsonl", JSON.stringify(record) + "\n");
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
## index.ts decomposition
|
|
258
|
-
|
|
259
|
-
The original monolithic `index.ts` has been decomposed into focused modules:
|
|
260
|
-
|
|
261
|
-
```text
|
|
262
|
-
src/
|
|
263
|
-
├── index.ts - slimmed entry point: init, tool registration
|
|
264
|
-
├── runtime.ts - SubagentRuntime: session-scoped state + methods
|
|
265
|
-
├── tools/
|
|
266
|
-
│ ├── agent-tool.ts - Agent tool definition, parameter validation, dispatch
|
|
267
|
-
│ ├── foreground-runner.ts - foreground execution loop (spinner, streaming, result)
|
|
268
|
-
│ ├── background-spawner.ts - background spawn (activity setup, notification wiring)
|
|
269
|
-
│ ├── get-result-tool.ts - get_subagent_result tool
|
|
270
|
-
│ ├── steer-tool.ts - steer_subagent tool
|
|
271
|
-
│ └── helpers.ts - shared tool utilities (textResult, buildDetails, getStatusNote, ...)
|
|
272
|
-
├── handlers/
|
|
273
|
-
│ ├── lifecycle.ts - session_start, session_before_switch, session_shutdown
|
|
274
|
-
│ └── tool-start.ts - tool_execution_start handler
|
|
275
|
-
├── notification.ts - completion nudges, custom renderer
|
|
276
|
-
├── renderer.ts - notification TUI component
|
|
277
|
-
├── ui/agent-menu.ts - /agents slash command menu (orchestration, listing, settings)
|
|
278
|
-
├── ui/agent-config-editor.ts - agent detail view (edit/delete/eject/disable/enable)
|
|
279
|
-
├── ui/agent-creation-wizard.ts - agent creation (AI-generation and manual-form)
|
|
280
|
-
├── ui/agent-file-ops.ts - AgentFileOps interface + FsAgentFileOps implementation
|
|
281
|
-
├── service-adapter.ts - SubagentsService implementation wrapping AgentManager
|
|
282
|
-
└── (existing domain modules unchanged)
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
Each extracted module receives narrow constructor-injected dependencies rather than closing over module-level state.
|
|
286
|
-
Handlers call methods on narrow runtime interfaces - no raw field writes, no `widget!` reach-throughs.
|
|
287
|
-
|
|
288
|
-
## Phase plan
|
|
289
|
-
|
|
290
|
-
### Phase 1: Export `SubagentsService` from this package (#48)
|
|
291
|
-
|
|
292
|
-
Added the `SubagentsService` interface, serializable types, `Symbol.for()` accessor functions, and `SUBAGENT_EVENTS` constants as public exports.
|
|
293
|
-
Wired `service-adapter.ts` to wrap `AgentManager` and call `publishSubagentsService()` at extension init.
|
|
294
|
-
|
|
295
|
-
### Phase 2: Remove scheduling (#52)
|
|
296
|
-
|
|
297
|
-
Deleted `schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`.
|
|
298
|
-
Removed the `schedule` parameter from the `Agent` tool schema.
|
|
299
|
-
Removed scheduler setup and lifecycle hooks from `index.ts`.
|
|
300
|
-
|
|
301
|
-
### Phase 3: Remove group-join, ad-hoc RPC; replace output-file (#49, #61)
|
|
302
|
-
|
|
303
|
-
Deleted `group-join.ts`, `cross-extension-rpc.ts` (#49).
|
|
304
|
-
Replaced `output-file.ts` with `SessionManager.create()` + `session-dir.ts` (#61).
|
|
305
|
-
Simplified `index.ts` to use direct individual notifications.
|
|
306
|
-
Lifecycle events emitted on `pi.events` for external consumers.
|
|
307
|
-
|
|
308
|
-
### Phase 4: Implement and publish `SubagentsService` (#48)
|
|
309
|
-
|
|
310
|
-
Wired `service-adapter.ts` to wrap `AgentManager` and call `publishSubagentsService()` at extension init.
|
|
311
|
-
Model strings are resolved inside the adapter.
|
|
312
|
-
|
|
313
|
-
### Phase 5: Decompose `index.ts` (#54, #69, #70, #87)
|
|
314
|
-
|
|
315
|
-
Extracted tools, notifications, activity tracking, event handlers, and the `/agents` command into separate modules.
|
|
316
|
-
Created `SubagentRuntime` factory to hold session-scoped state.
|
|
317
|
-
|
|
318
|
-
### Phase 6 (deferred): Extract UI to `@gotgenes/pi-subagents-ui`
|
|
319
|
-
|
|
320
|
-
The widget, conversation viewer, `/agents` command, notifications, and activity tracking are candidates for extraction to a separate extension that consumes `SubagentsService` + lifecycle events.
|
|
321
|
-
This phase is deferred until the API boundary is proven stable in production.
|
|
481
|
+
These are fire-and-forget broadcast events — no request IDs, no reply channels.
|
|
322
482
|
|
|
323
|
-
|
|
483
|
+
## Current structural analysis
|
|
324
484
|
|
|
325
|
-
|
|
485
|
+
### Health metrics
|
|
326
486
|
|
|
327
|
-
|
|
487
|
+
| Metric | Value |
|
|
488
|
+
| ------------------------- | ---------------------------- |
|
|
489
|
+
| Health score | 75/100 (B) |
|
|
490
|
+
| Total LOC | 7,288 (53 files) |
|
|
491
|
+
| Dead code | 0 files, 0 exports |
|
|
492
|
+
| Maintainability index | 90.7 (good) |
|
|
493
|
+
| Avg cyclomatic complexity | 1.5 |
|
|
494
|
+
| P90 cyclomatic complexity | 2 |
|
|
495
|
+
| Production duplication | 18 lines (1 clone group) |
|
|
496
|
+
| Test duplication | 71 clone groups, 1,424 lines |
|
|
328
497
|
|
|
329
|
-
###
|
|
498
|
+
### Dependency bag inventory
|
|
330
499
|
|
|
331
|
-
|
|
500
|
+
These interfaces carry hidden dependencies that obscure true coupling.
|
|
501
|
+
Bags with 10+ fields are the highest priority for decomposition.
|
|
332
502
|
|
|
333
|
-
|
|
503
|
+
| Interface | Fields | Consumers | Severity |
|
|
504
|
+
| --------------------------- | --------- | ------------------------------------------------- | -------- |
|
|
505
|
+
| `ResolvedSpawnConfig` | 15 | foreground-runner, background-spawner, agent-tool | Critical |
|
|
506
|
+
| `AgentSpawnConfig` | 13 | agent-manager (internal) | Critical |
|
|
507
|
+
| `RunOptions` | 12 | agent-runner | High |
|
|
508
|
+
| `SessionConfig` | 11 | agent-runner (output of assembler) | High |
|
|
509
|
+
| `NotificationDetails` | 10 | notification | Medium |
|
|
510
|
+
| `ResourceLoaderOptions` | 10 | agent-runner (SDK bridge) | Medium |
|
|
511
|
+
| `RunnerIO` | 9 methods | agent-runner | Medium |
|
|
512
|
+
| `CreateSessionOptions` | 9 | agent-runner (SDK bridge) | Medium |
|
|
513
|
+
| `AgentToolDeps` | 8 | agent-tool | Low |
|
|
514
|
+
| `AgentMenuDeps` | 8 | agent-menu | Low |
|
|
515
|
+
| `ConversationViewerOptions` | 8 | conversation-viewer | Low |
|
|
516
|
+
| `AgentRecordInit` | 8 | agent-record | Low |
|
|
334
517
|
|
|
335
|
-
###
|
|
518
|
+
### Complexity hotspots
|
|
336
519
|
|
|
337
|
-
|
|
520
|
+
Functions with cyclomatic complexity ≥ 21 (critical threshold):
|
|
338
521
|
|
|
339
|
-
|
|
340
|
-
|
|
522
|
+
| Function | Cyclomatic | Cognitive | File | Concern |
|
|
523
|
+
| ------------------- | ---------- | --------- | --------------------------- | ---------------------------------- |
|
|
524
|
+
| `buildContentLines` | 30 | 71 | `ui/conversation-viewer.ts` | Formats session events for display |
|
|
525
|
+
| `renderResult` | 26 | 43 | `tools/agent-tool.ts` | Formats agent result for LLM |
|
|
526
|
+
| `showAgentDetail` | 25 | 33 | `ui/agent-config-editor.ts` | Agent detail/edit view |
|
|
527
|
+
| `renderWidgetLines` | 25 | 44 | `ui/widget-renderer.ts` | Renders widget status lines |
|
|
528
|
+
| `ejectAgent` | 21 | 20 | `ui/agent-config-editor.ts` | Eject agent to filesystem |
|
|
529
|
+
| `update` | 21 | 31 | `ui/agent-widget.ts` | Widget lifecycle + polling |
|
|
341
530
|
|
|
342
|
-
|
|
531
|
+
### Churn hotspots
|
|
343
532
|
|
|
344
|
-
|
|
345
|
-
Phase 6 (UI extraction) is deferred.
|
|
346
|
-
See `git log` for the full history; issue references are preserved below for traceability.
|
|
533
|
+
Files with highest commit frequency × complexity (accelerating trend):
|
|
347
534
|
|
|
348
|
-
|
|
|
349
|
-
|
|
|
350
|
-
|
|
|
351
|
-
|
|
|
352
|
-
|
|
|
353
|
-
|
|
|
535
|
+
| Score | File | Commits |
|
|
536
|
+
| ----- | --------------------- | ------- |
|
|
537
|
+
| 85.7 | `index.ts` | 65 |
|
|
538
|
+
| 35.9 | `agent-manager.ts` | 31 |
|
|
539
|
+
| 25.9 | `ui/agent-menu.ts` | 26 |
|
|
540
|
+
| 23.3 | `tools/agent-tool.ts` | 30 |
|
|
354
541
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
## AgentManager decomposition
|
|
358
|
-
|
|
359
|
-
AgentManager was decomposed in three steps to untangle record management, concurrency control, and execution orchestration.
|
|
360
|
-
|
|
361
|
-
### Step 1: Record state machine (#98, #102)
|
|
362
|
-
|
|
363
|
-
Extracted status-transition methods (`markRunning`, `markCompleted`, `markAborted`, `markSteered`, `markError`, `markStopped`, `resetForResume`) onto `AgentRecord`.
|
|
364
|
-
Replaced scattered field writes across 6 sites with encapsulated transition methods.
|
|
365
|
-
Issue #102 consolidated test `AgentRecord` construction into a shared factory.
|
|
366
|
-
|
|
367
|
-
### Step 2: Parent snapshot (#99)
|
|
368
|
-
|
|
369
|
-
Replaced live `ctx: ExtensionContext` capture in `SpawnArgs` with an immutable `ParentSnapshot` data object.
|
|
370
|
-
The snapshot is taken once at spawn time; queued agents execute against frozen state rather than a potentially stale session reference.
|
|
371
|
-
`runAgent()` accepts `ParentSnapshot` instead of `ctx`.
|
|
372
|
-
`pi: ExtensionAPI` was removed from `SpawnArgs` - `runAgent()` accepts a `ShellExec` function instead.
|
|
373
|
-
|
|
374
|
-
### Step 3: Session-event observation (#100)
|
|
375
|
-
|
|
376
|
-
Replaced three-layer callback threading with direct session subscriptions.
|
|
377
|
-
`record-observer.ts` subscribes to the session to update record statistics (tool uses, lifetime usage, compaction count).
|
|
378
|
-
`ui/ui-observer.ts` subscribes to the session to stream UI state (active tools, response text, turn count).
|
|
379
|
-
`SpawnOptions` and `RunOptions` dropped all `on*` callback fields except `onSessionCreated` (which delivers the session object to enable external subscriptions).
|
|
380
|
-
|
|
381
|
-
### Realized impact
|
|
382
|
-
|
|
383
|
-
| Metric | Before | After |
|
|
384
|
-
| --------------------------------- | ------ | ----------------------- |
|
|
385
|
-
| `SpawnOptions` callback fields | 6 | 1 (`onSessionCreated`) |
|
|
386
|
-
| `RunOptions` callback fields | 6 | 1 (`onSessionCreated`) |
|
|
387
|
-
| Callback layers | 3 | 0 (direct subscription) |
|
|
388
|
-
| Live `ctx` references in queue | 1 | 0 (snapshot) |
|
|
389
|
-
| Scattered status-transition sites | 6 | 1 (state machine) |
|
|
390
|
-
|
|
391
|
-
---
|
|
392
|
-
|
|
393
|
-
## Encapsulation roadmap
|
|
394
|
-
|
|
395
|
-
Phase 7 encapsulated mutable state into classes, replaced callbacks with semantic components, and narrowed dependency bags.
|
|
396
|
-
|
|
397
|
-
Each step was sequenced so it made the next step easier.
|
|
398
|
-
|
|
399
|
-
### Resolved smells
|
|
400
|
-
|
|
401
|
-
All nine smells identified at the start of Phase 7 were resolved:
|
|
402
|
-
|
|
403
|
-
| Smell | Resolution |
|
|
404
|
-
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
405
|
-
| Global mutable state | `AgentTypeRegistry` class (#108); `reloadCustomAgents` callback removed from dep bags |
|
|
406
|
-
| Closure bag as class | `NotificationManager` class (#116); `pendingNudges` and timer state are private fields |
|
|
407
|
-
| Mutable state bag | `AgentActivityTracker` class (#110); transition methods replace external writes |
|
|
408
|
-
| Settings relay | `SettingsManager` class (#109); 6 callback fields collapsed to one object |
|
|
409
|
-
| Post-construction mutation | `ExecutionState`, `WorktreeState`, `NotificationState` collaborators (#111); stats behind mutation methods |
|
|
410
|
-
| Fire-and-forget callbacks | `AgentManagerObserver` interface (#112); one observer object replaces 3 closure lambdas |
|
|
411
|
-
| Duplicate `SpawnOptions` | Internal type renamed to `AgentSpawnConfig` (#113); public `SpawnOptions` unchanged |
|
|
412
|
-
| Type dumping ground | `NotificationDetails`, `ParentSnapshot`, `EnvInfo` moved to their natural modules (#116); narrow subsets defined |
|
|
413
|
-
| Wide dependency bags | `AgentToolDeps` 9 → 6, `AgentMenuDeps` 8 → 7 (#114); `emitEvent` removed; description text derived from registry; `agentActivity` narrowed |
|
|
414
|
-
|
|
415
|
-
### Step A: Extract state into classes (foundation, parallel)
|
|
416
|
-
|
|
417
|
-
These three extractions are independent and can proceed in any order.
|
|
418
|
-
Each eliminates a category of global/closure state and gives orphaned callbacks a natural home.
|
|
419
|
-
|
|
420
|
-
#### A1. AgentTypeRegistry class (#108)
|
|
421
|
-
|
|
422
|
-
Wrapped the module-scoped `agents` Map and free functions in `agent-types.ts` into an injectable class.
|
|
423
|
-
`reloadCustomAgents` callback removed from `AgentToolDeps` and `AgentMenuDeps`; replaced by `registry.reload()`.
|
|
424
|
-
`DEFAULT_AGENT_NAMES` moved from `types.ts` to the registry.
|
|
425
|
-
|
|
426
|
-
#### A2. SettingsManager class (#109, #118)
|
|
542
|
+
### Production duplication
|
|
427
543
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
Added `applyMaxConcurrent(n)`, `applyDefaultMaxTurns(n)`, `applyGraceTurns(n)` - each owns the full consequence chain: normalize → set in memory → notify callback → persist → emit event → return toast.
|
|
431
|
-
The 6 settings-related fields in `AgentMenuDeps` collapsed to `settings: AgentMenuSettings`.
|
|
544
|
+
One clone group (18 lines) shared between `agent-runner.ts:456-468` and `conversation-viewer.ts:261-278`.
|
|
545
|
+
Both format turn-event content for display — identical iteration over message content items, extracting tool names and text.
|
|
432
546
|
|
|
433
|
-
|
|
547
|
+
### Proposed bag decompositions
|
|
434
548
|
|
|
435
|
-
|
|
436
|
-
`ui-observer.ts` calls transition methods; consumers use read-only accessors.
|
|
437
|
-
The shared map on `SubagentRuntime` is `Map<string, AgentActivityTracker>`.
|
|
549
|
+
#### ResolvedSpawnConfig (15 fields → 3 value objects)
|
|
438
550
|
|
|
439
|
-
|
|
551
|
+
This bag mixes three concerns: who the agent is, how it should run, and how it should be displayed.
|
|
552
|
+
Each consumer uses a different subset.
|
|
440
553
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
### Step C: Replace AgentManager callbacks with observer (#112)
|
|
451
|
-
|
|
452
|
-
`AgentManagerObserver` interface replaces `onStart`/`onComplete`/`onCompact`.
|
|
453
|
-
`index.ts` constructs one observer object instead of 3 closure lambdas.
|
|
454
|
-
`AgentManagerOptions` drops from 9 → 7 fields.
|
|
455
|
-
|
|
456
|
-
### Step D: Disambiguate SpawnOptions and narrow dependency bags
|
|
457
|
-
|
|
458
|
-
#### D1. Disambiguate SpawnOptions (#113)
|
|
459
|
-
|
|
460
|
-
Internal `SpawnOptions` in `agent-manager.ts` renamed to `AgentSpawnConfig`.
|
|
461
|
-
Public `SpawnOptions` in `service.ts` unchanged.
|
|
462
|
-
|
|
463
|
-
#### D2. Narrow AgentToolDeps and AgentMenuDeps (#114)
|
|
464
|
-
|
|
465
|
-
| Bag | Before | After | How |
|
|
466
|
-
| --------------- | -------- | ----- | ----------------------------------------------------------------------------------------------------------- |
|
|
467
|
-
| `AgentToolDeps` | 9 fields | 6 | `emitEvent` → observer; `typeListText`/`availableTypesText` derived from registry; `agentActivity` narrowed |
|
|
468
|
-
| `AgentMenuDeps` | 8 fields | 7 | Dead `emitEvent` removed; `agentActivity` narrowed to read-only `AgentActivityReader` |
|
|
469
|
-
|
|
470
|
-
### Step E: Decompose large files and relocate types
|
|
471
|
-
|
|
472
|
-
#### E1. Split agent-tool.ts foreground/background (#115)
|
|
473
|
-
|
|
474
|
-
Extracted `foreground-runner.ts` (~175 lines) and `background-spawner.ts` (~116 lines).
|
|
475
|
-
`agent-tool.ts` reduced from 579 → 411 lines.
|
|
476
|
-
|
|
477
|
-
#### E2. Type housekeeping (#116)
|
|
478
|
-
|
|
479
|
-
- Moved `NotificationDetails`, `ParentSnapshot`, `EnvInfo` to their natural modules.
|
|
480
|
-
- Converted `createNotificationSystem` closure to `NotificationManager` class.
|
|
481
|
-
- Converted `ConversationViewer` constructor from 7 positional parameters to `ConversationViewerOptions` bag.
|
|
482
|
-
- Defined `AgentIdentity` and `AgentPromptConfig` narrow subsets; `buildAgentPrompt` narrowed to `AgentPromptConfig`.
|
|
483
|
-
|
|
484
|
-
### Phase 7 results
|
|
485
|
-
|
|
486
|
-
| Metric | Before | After |
|
|
487
|
-
| ------------------------------------------ | ------ | ----- |
|
|
488
|
-
| Module-scoped mutable state | 1 | 0 |
|
|
489
|
-
| Closure-bag "classes" | 2 | 0 |
|
|
490
|
-
| Externally-mutated state bags | 2 | 0 |
|
|
491
|
-
| `AgentManagerOptions` fields | 9 | 7 |
|
|
492
|
-
| `AgentToolDeps` fields | 9 | 6 |
|
|
493
|
-
| `AgentMenuDeps` fields | 13 | 7 |
|
|
494
|
-
| `SpawnOptions` callback fields | 6 | 1 |
|
|
495
|
-
| `RunOptions` callback fields | 6 | 1 |
|
|
496
|
-
| Callbacks threaded through deps | 8 | 0 |
|
|
497
|
-
| Types in `types.ts` without a natural home | 4 | 0 |
|
|
554
|
+
```typescript
|
|
555
|
+
/** Who this agent is — type resolution result. */
|
|
556
|
+
interface SpawnIdentity {
|
|
557
|
+
subagentType: string;
|
|
558
|
+
rawType: SubagentType;
|
|
559
|
+
fellBack: boolean;
|
|
560
|
+
displayName: string;
|
|
561
|
+
}
|
|
498
562
|
|
|
499
|
-
|
|
563
|
+
/** How the agent should run — execution parameters. */
|
|
564
|
+
interface SpawnExecution {
|
|
565
|
+
prompt: string;
|
|
566
|
+
description: string;
|
|
567
|
+
model: Model<any> | undefined;
|
|
568
|
+
effectiveMaxTurns: number | undefined;
|
|
569
|
+
thinking: ThinkingLevel | undefined;
|
|
570
|
+
inheritContext: boolean;
|
|
571
|
+
runInBackground: boolean;
|
|
572
|
+
isolated: boolean;
|
|
573
|
+
isolation: IsolationMode | undefined;
|
|
574
|
+
agentInvocation: AgentInvocation;
|
|
575
|
+
}
|
|
500
576
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
B --> C["C: Observer"] --> D1["D1: SpawnOptions"] --> D2
|
|
508
|
-
D2 --> E1["E1: agent-tool split"]
|
|
509
|
-
A1 --> E2["E2: Type housekeeping"]
|
|
577
|
+
/** How the agent is presented — display metadata. */
|
|
578
|
+
interface SpawnPresentation {
|
|
579
|
+
modelName: string | undefined;
|
|
580
|
+
agentTags: string[];
|
|
581
|
+
detailBase: Pick<AgentDetails, ...>;
|
|
582
|
+
}
|
|
510
583
|
```
|
|
511
584
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
Phase 7 eliminated all structural smells (mutable state, closure bags, callback threading, wide dependency bags).
|
|
517
|
-
Phase 8 targeted the next layer: testability friction, display module cohesion, and menu decomposition.
|
|
585
|
+
`foreground-runner` and `background-spawner` primarily consume `SpawnExecution` + `SpawnIdentity`.
|
|
586
|
+
`agent-tool` uses all three to build the `AgentSpawnConfig` and the result text.
|
|
587
|
+
After decomposition, each consumer declares its real dependencies explicitly.
|
|
518
588
|
|
|
519
|
-
|
|
520
|
-
Step G resolved `session-config.test.ts`; Step H resolved both `agent-runner.test.ts` and `agent-runner-extension-tools.test.ts`.
|
|
589
|
+
#### AgentSpawnConfig (13 fields → extract ParentSessionInfo)
|
|
521
590
|
|
|
522
|
-
|
|
523
|
-
The display extraction unblocked menu decomposition.
|
|
591
|
+
Several fields form a natural cluster around parent session identity:
|
|
524
592
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
| 3× duplicated `makeDeps()` | Step F (#131): shared `createToolDeps()` in `test/helpers/` |
|
|
534
|
-
|
|
535
|
-
The well-designed test suites - `agent-manager.test.ts` (1 mock, DI via `AgentRunner` interface), `notification.test.ts` (0 mocks, pure functions + DI), and `agent-tool.test.ts` (0 mocks, tests via deps bag) - confirmed the pattern: modules that accept collaborators through injection produce resilient tests; modules that import collaborators directly produce fragile mock-heavy tests.
|
|
536
|
-
|
|
537
|
-
### Step F: Shared test fixtures (#131)
|
|
538
|
-
|
|
539
|
-
Consolidated duplicated mock factories into `test/helpers/`.
|
|
540
|
-
|
|
541
|
-
1. `createMockSession()` - subscribable event bus with `emit()` helper; replaced 3 hand-rolled copies.
|
|
542
|
-
2. `createToolDeps()` - builds `AgentToolDeps` with sensible defaults and override support; replaced 3 `makeDeps()` copies.
|
|
543
|
-
3. `makeRecord()` - `AgentRecord` factory with sensible defaults; replaced scattered inline construction.
|
|
544
|
-
4. `STUB_CTX` - shared stub `ExtensionContext` constant; centralised unavoidable bridge casts.
|
|
545
|
-
|
|
546
|
-
Impact: reduced test boilerplate; single source of truth for mock shapes; changes to dep interfaces propagate automatically.
|
|
547
|
-
|
|
548
|
-
### Step G: Inject IO collaborators into session-config (#132)
|
|
549
|
-
|
|
550
|
-
`assembleSessionConfig` now accepts `io: AssemblerIO` as a required parameter.
|
|
551
|
-
`index.ts` constructs the real `AssemblerIO` from direct imports via the `RunnerIO.assemblerIO` field (wired in Step H).
|
|
552
|
-
`session-config.test.ts` injects stubs - all 4 `vi.mock()` calls eliminated, assertions shifted to `SessionConfig` output properties.
|
|
553
|
-
|
|
554
|
-
### Step H: Inject SDK boundary into agent-runner (#133)
|
|
555
|
-
|
|
556
|
-
`runAgent()` now accepts `io: RunnerIO` as a required parameter bundling all IO collaborators: `detectEnv`, `getAgentDir`, `createResourceLoader`, `deriveSessionDir`, `createSessionManager`, `createSettingsManager`, `createSession`, and `assemblerIO`.
|
|
557
|
-
|
|
558
|
-
`createAgentRunner(io: RunnerIO): AgentRunner` factory captures the boundary at construction time so `AgentManager` and the `AgentRunner` interface remain unchanged.
|
|
559
|
-
`index.ts` constructs the real `RunnerIO` from Pi SDK imports and sibling modules.
|
|
560
|
-
|
|
561
|
-
Impact: all 7 `vi.mock()` calls eliminated from both `agent-runner.test.ts` and `agent-runner-extension-tools.test.ts`; tests verify behavior (turn limits, tool filtering, response collection) through injected stubs; SDK imports moved to the extension entry point.
|
|
562
|
-
|
|
563
|
-
### Step I: Reduce `as any` casts in tests (#134)
|
|
564
|
-
|
|
565
|
-
Reduced `as any` count from 93 to 15 (plus 13 explicit `as unknown as T` bridge casts).
|
|
566
|
-
|
|
567
|
-
Production changes:
|
|
568
|
-
|
|
569
|
-
- `ResourceLoaderOptions.appendSystemPromptOverride` typed to match `DefaultResourceLoaderOptions`; `createResourceLoader` factory cast removed from `index.ts`.
|
|
570
|
-
- `CreateSessionOptions.settingsManager` / `RunnerIO.createSettingsManager` typed as `SettingsManager`.
|
|
571
|
-
- `WidgetLike` interface in `runtime.ts` narrows the widget field.
|
|
572
|
-
- Local `ToolCallContent` / `BashExecutionMessage` type guards replace `as any` duck-typing in `conversation-viewer.ts` and `agent-runner.ts`.
|
|
573
|
-
- `textResult()` return no longer casts `details as any`.
|
|
574
|
-
- `toAgentSession()` helper and `STUB_CTX` constant centralise unavoidable bridge casts.
|
|
575
|
-
|
|
576
|
-
Remaining 15 `as any` casts are: 8 menu-handler `ctx as any` (deferred - requires `AgentManager.spawn` to accept `ParentSnapshot` directly), 2 `print-mode.test.ts` (same ExtensionContext/API pattern), 2 private-field test access, 1 `createSession` SDK bridge in `index.ts`, 1 `foreground-runner.ts` `AgentToolResult<any>` detail, 1 `stub-ctx.ts` comment.
|
|
577
|
-
|
|
578
|
-
### Step J: Extract display helpers (#135)
|
|
579
|
-
|
|
580
|
-
`ui/display.ts` now contains all pure formatters, display helpers, constants, and shared types (`Theme`, `AgentDetails`).
|
|
581
|
-
`agent-widget.ts` dropped from 522 → ~340 lines.
|
|
582
|
-
All consumer modules (menu, tools, renderer, conversation viewer) import from `ui/display.ts` directly.
|
|
583
|
-
`test/agent-widget.test.ts` renamed to `test/display.test.ts`.
|
|
584
|
-
|
|
585
|
-
### Step K: Decompose agent-menu.ts (#136)
|
|
586
|
-
|
|
587
|
-
`agent-menu.ts` (668 lines) decomposed into four modules:
|
|
593
|
+
```typescript
|
|
594
|
+
/** Parent session identity — always travel together. */
|
|
595
|
+
interface ParentSessionInfo {
|
|
596
|
+
parentSessionFile?: string;
|
|
597
|
+
parentSessionId?: string;
|
|
598
|
+
toolCallId?: string;
|
|
599
|
+
}
|
|
600
|
+
```
|
|
588
601
|
|
|
589
|
-
|
|
590
|
-
2. `ui/agent-config-editor.ts` - `showAgentDetail` with edit/delete/reset/eject/disable/enable transitions (~200 lines).
|
|
591
|
-
3. `ui/agent-creation-wizard.ts` - AI-generation and manual-form creation paths (~250 lines).
|
|
592
|
-
4. `ui/agent-menu.ts` - menu orchestration, agent listing, running-agent viewer, settings form (~300 lines).
|
|
602
|
+
Extracting this from `AgentSpawnConfig` reduces it from 13 to 10 fields and introduces a named concept that currently exists only as scattered optional fields.
|
|
593
603
|
|
|
594
|
-
|
|
604
|
+
#### RunOptions (12 fields → extract RunContext)
|
|
595
605
|
|
|
596
|
-
|
|
606
|
+
The `RunOptions` bag mixes execution parameters with context information:
|
|
597
607
|
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
608
|
+
```typescript
|
|
609
|
+
/** Parent context needed to configure the child session. */
|
|
610
|
+
interface RunContext {
|
|
611
|
+
cwd?: string;
|
|
612
|
+
parentSessionFile?: string;
|
|
613
|
+
parentSessionId?: string;
|
|
614
|
+
exec: ShellExec;
|
|
615
|
+
registry: AgentConfigLookup;
|
|
616
|
+
}
|
|
606
617
|
```
|
|
607
618
|
|
|
608
|
-
The
|
|
609
|
-
|
|
610
|
-
---
|
|
619
|
+
The remaining `RunOptions` fields (`model`, `maxTurns`, `signal`, `isolated`, `thinkingLevel`, `defaultMaxTurns`, `graceTurns`, `onSessionCreated`) are genuine execution parameters.
|
|
611
620
|
|
|
612
|
-
|
|
621
|
+
#### SessionConfig (11 fields → extract ToolFilterConfig)
|
|
613
622
|
|
|
614
|
-
|
|
615
|
-
Phase 9 targets the next layer: observation model consolidation, `ExtensionContext` elimination from internal APIs, remaining `vi.mock()` / `as any` casts, and dependency bag cleanup.
|
|
623
|
+
Three fields form a cohesive tool-filtering cluster:
|
|
616
624
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
| `deps.` prefix noise in function bodies | remaining modules across tools, UI, service-adapter | Functions accept a `deps` bag and access every field as `deps.foo`; hides real dependencies and lengthens every call line | Low |
|
|
625
|
+
```typescript
|
|
626
|
+
/** Tool filtering configuration — used by filterActiveTools. */
|
|
627
|
+
interface ToolFilterConfig {
|
|
628
|
+
toolNames: string[];
|
|
629
|
+
disallowedSet: Set<string> | undefined;
|
|
630
|
+
extensions: boolean | string[];
|
|
631
|
+
}
|
|
632
|
+
```
|
|
626
633
|
|
|
627
|
-
|
|
634
|
+
Extracting this reduces `SessionConfig` from 11 to 8 fields and gives `filterActiveTools` a named input type instead of three positional parameters.
|
|
628
635
|
|
|
629
|
-
|
|
636
|
+
#### RunnerIO (9 methods → 2 focused interfaces)
|
|
630
637
|
|
|
631
|
-
|
|
632
|
-
- **≥5 fields** - keep a named interface but destructure in the function signature (`{ manager, widget }: ForegroundDeps`) so the function body uses bare names, not `deps.foo`.
|
|
638
|
+
The IO boundary mixes environment discovery with session factory operations:
|
|
633
639
|
|
|
634
|
-
|
|
640
|
+
```typescript
|
|
641
|
+
/** Environment discovery — detecting paths and platform info. */
|
|
642
|
+
interface EnvironmentIO {
|
|
643
|
+
detectEnv: (exec: ShellExec, cwd: string) => Promise<EnvInfo>;
|
|
644
|
+
getAgentDir: () => string;
|
|
645
|
+
deriveSessionDir: (parentSessionFile: string | undefined, effectiveCwd: string) => string;
|
|
646
|
+
}
|
|
635
647
|
|
|
636
|
-
|
|
648
|
+
/** Session factory — creating SDK objects. */
|
|
649
|
+
interface SessionFactoryIO {
|
|
650
|
+
createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoaderLike;
|
|
651
|
+
createSessionManager: (cwd: string, sessionDir: string) => SessionManagerLike;
|
|
652
|
+
createSettingsManager: (cwd: string, agentDir: string) => SettingsManager;
|
|
653
|
+
createSession: (opts: CreateSessionOptions) => Promise<{ session: AgentSession }>;
|
|
654
|
+
assemblerIO: AssemblerIO;
|
|
655
|
+
}
|
|
656
|
+
```
|
|
637
657
|
|
|
638
|
-
|
|
639
|
-
UI consumers read stats from `AgentRecord` instead of the tracker.
|
|
640
|
-
The UI observer retains event subscriptions for re-render triggers but no longer accumulates stats independently.
|
|
658
|
+
The runner would accept `EnvironmentIO & SessionFactoryIO` (keeping backward compatibility) while each piece can be tested independently.
|
|
641
659
|
|
|
642
|
-
|
|
643
|
-
The 15+ callsites that navigate `record.execution?.session` simplify to `record.session`.
|
|
660
|
+
## Improvement roadmap (Phase 10)
|
|
644
661
|
|
|
645
|
-
|
|
662
|
+
Phase 10 addresses the structural gaps identified in this analysis: flat code organization, oversized dependency bags, and complexity hotspots.
|
|
646
663
|
|
|
647
|
-
|
|
664
|
+
### Step 1: Reorganize source into domain directories ([#164][164])
|
|
648
665
|
|
|
649
|
-
|
|
666
|
+
Move files into `config/`, `session/`, `lifecycle/`, `observation/`, and `service/` subdirectories as described in the [proposed directory restructuring](#proposed-directory-restructuring).
|
|
650
667
|
|
|
651
|
-
|
|
652
|
-
Injected three collaborators (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) into `createAgentTool` so `execute` no longer reads `ctx` beyond `ctx.ui` (already delegated to `widget.setUICtx`).
|
|
653
|
-
`AgentManager.spawn()` and `spawnAndWait()` accept `ParentSnapshot` instead of `ExtensionContext`.
|
|
654
|
-
`service-adapter.ts` calls `buildParentSnapshot(session.ctx)` at its boundary.
|
|
655
|
-
`foreground-runner` and `background-spawner` receive `ResolvedSpawnConfig` + domain values (`snapshot`, `parentSessionFile`, `parentSessionId`) instead of `ctx`.
|
|
668
|
+
This is a mechanical change (file moves + import path updates) that:
|
|
656
669
|
|
|
657
|
-
|
|
658
|
-
|
|
670
|
+
- Makes the domain model visible in the filesystem.
|
|
671
|
+
- Reduces cognitive load when navigating the codebase (5 root files + 8 directories vs. 31 root files + 3 directories).
|
|
672
|
+
- Co-locates related files, making subsequent refactoring easier.
|
|
659
673
|
|
|
660
|
-
|
|
674
|
+
### Step 2: Decompose ResolvedSpawnConfig ([#165][165])
|
|
661
675
|
|
|
662
|
-
-
|
|
663
|
-
|
|
664
|
-
|
|
676
|
+
Split the 15-field bag into `SpawnIdentity`, `SpawnExecution`, and `SpawnPresentation`.
|
|
677
|
+
Each consumer declares its real dependencies.
|
|
678
|
+
Enables Step 3 (narrowing AgentSpawnConfig, [#166][166]).
|
|
665
679
|
|
|
666
|
-
|
|
680
|
+
### Step 3: Extract ParentSessionInfo from AgentSpawnConfig ([#166][166])
|
|
667
681
|
|
|
668
|
-
|
|
682
|
+
Extract `parentSessionFile`, `parentSessionId`, `toolCallId` into a `ParentSessionInfo` value object.
|
|
683
|
+
Reduces AgentSpawnConfig from 13 to 10 fields.
|
|
669
684
|
|
|
670
|
-
|
|
671
|
-
All inner functions in `agent-menu.ts`, `agent-config-editor.ts`, and `agent-creation-wizard.ts` now accept `(ui: MenuUI)` instead of `(ctx: ExtensionContext)`.
|
|
672
|
-
`index.ts` passes `ctx.ui`, `ctx.modelRegistry`, and `buildParentSnapshot(ctx)` to the handler.
|
|
685
|
+
### Step 4: Narrow RunnerIO ([#167][167])
|
|
673
686
|
|
|
674
|
-
|
|
675
|
-
|
|
687
|
+
Split into `EnvironmentIO` and `SessionFactoryIO`.
|
|
688
|
+
Each half can be tested independently.
|
|
676
689
|
|
|
677
|
-
|
|
690
|
+
### Step 5: Extract ToolFilterConfig from SessionConfig ([#168][168])
|
|
678
691
|
|
|
679
|
-
|
|
680
|
-
|
|
692
|
+
Extract the tool-filtering cluster into `ToolFilterConfig`.
|
|
693
|
+
Give `filterActiveTools` a named input type.
|
|
681
694
|
|
|
682
|
-
|
|
695
|
+
### Step 6: Extract RunContext from RunOptions ([#169][169])
|
|
683
696
|
|
|
684
|
-
|
|
697
|
+
Extract context fields into `RunContext`.
|
|
698
|
+
Reduces RunOptions from 12 to 7 fields.
|
|
685
699
|
|
|
686
|
-
### Step
|
|
700
|
+
### Step 7: Reduce buildContentLines complexity ([#170][170])
|
|
687
701
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
Tests inject a stub or the real function directly via options — no module-level mock needed.
|
|
702
|
+
`buildContentLines` in `conversation-viewer.ts` has cognitive complexity 71.
|
|
703
|
+
Extract formatting sub-functions for each content type (tool calls, text, bash output).
|
|
691
704
|
|
|
692
|
-
|
|
705
|
+
### Step 8: Reduce renderResult complexity ([#171][171])
|
|
693
706
|
|
|
694
|
-
|
|
707
|
+
`renderResult` in `agent-tool.ts` has cognitive complexity 43.
|
|
708
|
+
Extract result formatting by status (completed, error, aborted, stopped).
|
|
695
709
|
|
|
696
|
-
### Step
|
|
710
|
+
### Step 9: Extract shared turn-formatting logic ([#172][172])
|
|
697
711
|
|
|
698
|
-
|
|
699
|
-
The widget is now a thin lifecycle/polling wrapper (198 lines, down from 374) that delegates to pure render functions.
|
|
700
|
-
Rendering functions receive data (agent list, activity map, registry) and return formatted strings - testable without widget lifecycle. 23 new unit tests cover all status variants, overflow, tree connectors, and empty states.
|
|
712
|
+
The 18-line production clone between `agent-runner.ts` and `conversation-viewer.ts` extracts into a shared function in the session domain.
|
|
701
713
|
|
|
702
714
|
### Step dependencies
|
|
703
715
|
|
|
704
716
|
```mermaid
|
|
705
717
|
flowchart LR
|
|
706
|
-
subgraph
|
|
707
|
-
|
|
718
|
+
subgraph organization["Code organization"]
|
|
719
|
+
S1["#164: Domain directories"]
|
|
708
720
|
end
|
|
709
|
-
subgraph
|
|
710
|
-
|
|
721
|
+
subgraph bags["Dependency bags"]
|
|
722
|
+
S2["#165: ResolvedSpawnConfig"] --> S3["#166: AgentSpawnConfig"]
|
|
723
|
+
S4["#167: RunnerIO"]
|
|
724
|
+
S5["#168: SessionConfig"]
|
|
725
|
+
S6["#169: RunOptions"]
|
|
711
726
|
end
|
|
712
|
-
|
|
727
|
+
subgraph complexity["Complexity reduction"]
|
|
728
|
+
S7["#170: buildContentLines"]
|
|
729
|
+
S8["#171: renderResult"]
|
|
730
|
+
S9["#172: Shared turn-formatting"]
|
|
731
|
+
end
|
|
732
|
+
S1 --> S2 & S4 & S5 & S6
|
|
733
|
+
S1 --> S7 & S8 & S9
|
|
713
734
|
```
|
|
714
735
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
|
726
|
-
|
|
|
736
|
+
Step 1 ([#164][164], directory restructuring) unblocks all other steps by co-locating related files.
|
|
737
|
+
Steps 2–6 (bag decomposition) and Steps 7–9 (complexity reduction) are independent tracks that can proceed in parallel.
|
|
738
|
+
Within the bag track, Step 2 ([#165][165], ResolvedSpawnConfig) enables Step 3 ([#166][166], AgentSpawnConfig).
|
|
739
|
+
|
|
740
|
+
## Refactoring history
|
|
741
|
+
|
|
742
|
+
Phases 1–5 and 7–9 are complete.
|
|
743
|
+
Phase 6 (UI extraction to a separate package) is deferred.
|
|
744
|
+
Detailed records are preserved in per-phase history files:
|
|
745
|
+
|
|
746
|
+
| Phase | Title | Status | History |
|
|
747
|
+
| ----- | --------------------------------------------------- | -------- | -------------------------------------------------------------------------- |
|
|
748
|
+
| 1 | Export SubagentsService API boundary | Complete | [phase-1-api-boundary.md](history/phase-1-api-boundary.md) |
|
|
749
|
+
| 2 | Remove scheduling subsystem | Complete | [phase-2-remove-scheduling.md](history/phase-2-remove-scheduling.md) |
|
|
750
|
+
| 3 | Remove group-join, RPC; replace output-file | Complete | [phase-3-remove-rpc-groupjoin.md](history/phase-3-remove-rpc-groupjoin.md) |
|
|
751
|
+
| 4 | Implement and publish SubagentsService | Complete | [phase-4-implement-service.md](history/phase-4-implement-service.md) |
|
|
752
|
+
| 5 | Decompose index.ts | Complete | [phase-5-decompose-index.md](history/phase-5-decompose-index.md) |
|
|
753
|
+
| 6 | Extract UI to separate package | Deferred | — |
|
|
754
|
+
| 7 | Encapsulation and dependency narrowing | Complete | [phase-7-encapsulation.md](history/phase-7-encapsulation.md) |
|
|
755
|
+
| 8 | Testability, display extraction, menu decomposition | Complete | [phase-8-testability.md](history/phase-8-testability.md) |
|
|
756
|
+
| 9 | Observation consolidation, ctx elimination | Complete | [phase-9-observation-ctx.md](history/phase-9-observation-ctx.md) |
|
|
757
|
+
|
|
758
|
+
### Structural refactoring issues
|
|
759
|
+
|
|
760
|
+
| Phase | Issue | Summary |
|
|
761
|
+
| ------------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
762
|
+
| Foundation | #69, #71, #76, #80 | SubagentRuntime, pure assembler, cwd injection, config consolidation |
|
|
763
|
+
| Core decomposition | #84, #72, #87, #70 | WorktreeManager, AgentManager DI, runtime methods, handler extraction |
|
|
764
|
+
| Interface polish | #66, #77 | SDK types, projectAgentsDir |
|
|
765
|
+
| Features | #61 | JSONL session transcripts |
|
|
766
|
+
| AgentManager | #98, #99, #100, #102 | Record state machine, ParentSnapshot, session-event observation, test factory |
|
|
767
|
+
| Encapsulation | #108, #109, #110, #111, #112, #113, #114, #115, #116, #118 | Registry, settings, activity tracker, record lifecycle, observer, spawn options, deps narrowing, tool split, type housekeeping |
|
|
768
|
+
| Testability | #131, #132, #133, #134, #135, #136 | Shared fixtures, session-config IO, runner SDK boundary, as-any reduction, display extraction, menu decomposition |
|
|
769
|
+
| Observation/ctx | #144, #145, #146, #147, #148 | Observation consolidation, execute decomposition, UI context, text wrapping injection, widget rendering split |
|
|
727
770
|
|
|
728
|
-
|
|
771
|
+
The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
|
|
729
772
|
|
|
730
773
|
## Relationship with upstream
|
|
731
774
|
|
|
732
|
-
This fork (`@gotgenes/pi-subagents` in the [gotgenes/pi-packages] monorepo) is
|
|
775
|
+
This fork (`@gotgenes/pi-subagents` in the [gotgenes/pi-packages] monorepo) is a hard fork of [tintinweb/pi-subagents].
|
|
733
776
|
The decomposition diverges materially from upstream's direction.
|
|
734
777
|
|
|
735
778
|
The three upstream PRs (#71, #72, #73) remain open.
|
|
@@ -742,3 +785,13 @@ The upstream test suite is run periodically as a regression canary for the agent
|
|
|
742
785
|
[earendil-works/pi#4207]: https://github.com/earendil-works/pi/issues/4207
|
|
743
786
|
[gotgenes/pi-packages]: https://github.com/gotgenes/pi-packages
|
|
744
787
|
[tintinweb/pi-subagents]: https://github.com/tintinweb/pi-subagents
|
|
788
|
+
|
|
789
|
+
[164]: https://github.com/gotgenes/pi-packages/issues/164
|
|
790
|
+
[165]: https://github.com/gotgenes/pi-packages/issues/165
|
|
791
|
+
[166]: https://github.com/gotgenes/pi-packages/issues/166
|
|
792
|
+
[167]: https://github.com/gotgenes/pi-packages/issues/167
|
|
793
|
+
[168]: https://github.com/gotgenes/pi-packages/issues/168
|
|
794
|
+
[169]: https://github.com/gotgenes/pi-packages/issues/169
|
|
795
|
+
[170]: https://github.com/gotgenes/pi-packages/issues/170
|
|
796
|
+
[171]: https://github.com/gotgenes/pi-packages/issues/171
|
|
797
|
+
[172]: https://github.com/gotgenes/pi-packages/issues/172
|