@gotgenes/pi-subagents 6.3.1 → 6.4.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 +16 -0
- package/docs/architecture/architecture.md +234 -263
- package/docs/plans/0108-extract-agent-type-registry.md +322 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +5 -0
- package/src/agent-runner.ts +4 -0
- package/src/agent-types.ts +108 -91
- package/src/index.ts +16 -21
- package/src/session-config.ts +5 -4
- package/src/tools/agent-tool.ts +8 -8
- package/src/tools/get-result-tool.ts +3 -1
- package/src/types.ts +0 -3
- package/src/ui/agent-menu.ts +23 -25
- package/src/ui/agent-widget.ts +10 -9
- package/src/ui/conversation-viewer.ts +4 -2
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 108
|
|
3
|
+
issue_title: "refactor(pi-subagents): extract AgentTypeRegistry class from module-scoped state"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Extract AgentTypeRegistry class
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`agent-types.ts` manages a module-scoped `Map<string, AgentConfig>` mutated by `registerAgents()` and read by 12+ call sites across 6 files.
|
|
11
|
+
This is global mutable state hidden behind free functions — tests must call `registerAgents(new Map())` in `beforeEach` to reset it, and `reloadCustomAgents` is threaded as a callback through `AgentToolDeps` and `AgentMenuDeps` because there is no object to own the reload.
|
|
12
|
+
|
|
13
|
+
## Goals
|
|
14
|
+
|
|
15
|
+
- Wrap the module-scoped `agents` Map and its free functions into an injectable `AgentTypeRegistry` class.
|
|
16
|
+
- Replace the `reloadCustomAgents` callback (threaded through 2 dependency bags) with `registry.reload()`.
|
|
17
|
+
- Move `DEFAULT_AGENT_NAMES` from `types.ts` to the registry (it is a constant, not a type).
|
|
18
|
+
- Enable test isolation without module resets — each test creates its own registry instance.
|
|
19
|
+
- Use lift-and-shift: introduce the class alongside the free functions, migrate consumers incrementally, then remove the free functions.
|
|
20
|
+
|
|
21
|
+
## Non-Goals
|
|
22
|
+
|
|
23
|
+
- `SettingsManager` extraction (#109) — separate Phase 7 step.
|
|
24
|
+
- `AgentActivityTracker` extraction (#110) — separate Phase 7 step.
|
|
25
|
+
- Splitting `AgentRecord` lifecycle state (#111) — separate Phase 7 step.
|
|
26
|
+
- Narrowing `AgentConfig` (21 fields) — tracked in the architecture doc but out of scope.
|
|
27
|
+
- Moving `BUILTIN_TOOL_NAMES` — it is a constant with no Map dependency, stays as a module export.
|
|
28
|
+
|
|
29
|
+
## Background
|
|
30
|
+
|
|
31
|
+
### Architecture reference
|
|
32
|
+
|
|
33
|
+
Phase 7, Step A1 in `docs/architecture/architecture.md`.
|
|
34
|
+
Steps A1–A3 are independent and can proceed in any order.
|
|
35
|
+
This plan addresses A1 only.
|
|
36
|
+
|
|
37
|
+
### Relevant modules
|
|
38
|
+
|
|
39
|
+
| Module | Role | agent-types dependency |
|
|
40
|
+
| --------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
41
|
+
| `agent-types.ts` | Module-scoped `agents` Map + 11 free functions | Owns the state |
|
|
42
|
+
| `default-agents.ts` | `DEFAULT_AGENTS` Map constant | Read by `registerAgents` |
|
|
43
|
+
| `custom-agents.ts` | `loadCustomAgents()` → disk scan | Imports `BUILTIN_TOOL_NAMES` |
|
|
44
|
+
| `session-config.ts` | `assembleSessionConfig()` | Imports `resolveAgentConfig`, `getToolNamesForType`, `getMemoryToolNames`, `getReadOnlyMemoryToolNames` |
|
|
45
|
+
| `agent-runner.ts` | `runAgent()` → calls `assembleSessionConfig` | No direct import (transitive via session-config) |
|
|
46
|
+
| `agent-manager.ts` | `AgentManager` → calls runner | No direct import |
|
|
47
|
+
| `tools/agent-tool.ts` | Agent tool definition | Imports `resolveAgentConfig`, `resolveType`; receives `reloadCustomAgents` via `AgentToolDeps` |
|
|
48
|
+
| `ui/agent-menu.ts` | `/agents` command handler | Imports `BUILTIN_TOOL_NAMES`, `getAllTypes`, `resolveAgentConfig`, `resolveType`; receives `reloadCustomAgents` via `AgentMenuDeps` |
|
|
49
|
+
| `ui/agent-widget.ts` | Widget + `getDisplayName` / `getPromptModeLabel` helpers | Imports `resolveAgentConfig` |
|
|
50
|
+
| `ui/conversation-viewer.ts` | Live conversation overlay | Imports `getDisplayName`, `getPromptModeLabel` from `agent-widget.ts` |
|
|
51
|
+
| `tools/get-result-tool.ts` | `get_subagent_result` tool | Imports `getDisplayName` from `agent-widget.ts` |
|
|
52
|
+
| `index.ts` | Extension entry point, wiring | Imports `registerAgents`, `getAvailableTypes`, `getDefaultAgentNames`, `getUserAgentNames`, `resolveAgentConfig`; defines `reloadCustomAgents` closure |
|
|
53
|
+
|
|
54
|
+
### Test files affected
|
|
55
|
+
|
|
56
|
+
| Test file | Current agent-types coupling | Change needed |
|
|
57
|
+
| -------------------------------------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
58
|
+
| `agent-types.test.ts` (333 lines) | Tests free functions directly, calls `registerAgents()` in `beforeEach` | Add class tests; eventually migrate free-function tests to class tests |
|
|
59
|
+
| `session-config.test.ts` (601 lines) | `vi.mock("../src/agent-types.js")` | Pass mock registry param, remove module mock |
|
|
60
|
+
| `agent-runner.test.ts` (302 lines) | `vi.mock("../src/agent-types.js")` | Provide mock registry in `RunOptions`, remove module mock |
|
|
61
|
+
| `agent-runner-extension-tools.test.ts` (307 lines) | `vi.mock("../src/agent-types.js")` | Same as `agent-runner.test.ts` |
|
|
62
|
+
| `tools/agent-tool.test.ts` (240 lines) | Mocks `reloadCustomAgents` in deps | Replace with `registry` in deps |
|
|
63
|
+
| `ui/agent-menu.test.ts` (184 lines) | `vi.mock("../../src/agent-types.js")`, mocks `reloadCustomAgents` in deps | Replace with `registry` in deps, remove module mock |
|
|
64
|
+
| `agent-widget.test.ts` (26 lines) | Minimal | Pass registry to widget constructor |
|
|
65
|
+
| `agent-manager.test.ts` (712 lines) | No `agent-types.js` mock | Add registry to constructor options |
|
|
66
|
+
|
|
67
|
+
### Constraints from AGENTS.md / code-design skill
|
|
68
|
+
|
|
69
|
+
- **ISP:** `session-config.ts` should accept a narrow interface (only `resolveAgentConfig` + `getToolNamesForType`), not the full registry class.
|
|
70
|
+
- **DIP:** Accept collaborators as parameters; keep IO at the edges.
|
|
71
|
+
- **Pi SDK boundaries:** Pure helpers must not import Pi SDK types.
|
|
72
|
+
- **Lift-and-shift:** Never plan a single step that rewrites an entire large test file at once.
|
|
73
|
+
|
|
74
|
+
## Design Overview
|
|
75
|
+
|
|
76
|
+
### `AgentTypeRegistry` class
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
export class AgentTypeRegistry {
|
|
80
|
+
private agents = new Map<string, AgentConfig>();
|
|
81
|
+
|
|
82
|
+
constructor(private loadUserAgents: () => Map<string, AgentConfig>) {
|
|
83
|
+
this.reload();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Re-scan custom agents from disk and merge with defaults. */
|
|
87
|
+
reload(): void { /* clear + merge DEFAULT_AGENTS + loadUserAgents() */ }
|
|
88
|
+
|
|
89
|
+
resolveAgentConfig(type: string): AgentConfig { /* ... */ }
|
|
90
|
+
resolveType(name: string): string | undefined { /* ... */ }
|
|
91
|
+
getAvailableTypes(): string[] { /* ... */ }
|
|
92
|
+
getAllTypes(): string[] { /* ... */ }
|
|
93
|
+
getDefaultAgentNames(): string[] { /* ... */ }
|
|
94
|
+
getUserAgentNames(): string[] { /* ... */ }
|
|
95
|
+
isValidType(type: string): boolean { /* ... */ }
|
|
96
|
+
getToolNamesForType(type: string): string[] { /* ... */ }
|
|
97
|
+
|
|
98
|
+
static readonly DEFAULT_AGENT_NAMES = ["general-purpose", "Explore", "Plan"] as const;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The constructor accepts a `loadUserAgents` callback (typically `() => loadCustomAgents(process.cwd())`).
|
|
103
|
+
This keeps `process.cwd()` at the edge (in `index.ts`) and makes tests trivial — they pass `() => new Map()` or a fixture.
|
|
104
|
+
|
|
105
|
+
### What stays as free functions
|
|
106
|
+
|
|
107
|
+
- **`BUILTIN_TOOL_NAMES`** — constant array, no Map dependency.
|
|
108
|
+
Stays as a module-level export in `agent-types.ts`.
|
|
109
|
+
- **`getMemoryToolNames` / `getReadOnlyMemoryToolNames`** — pure functions over constant arrays, no Map access.
|
|
110
|
+
Stay as module-level exports.
|
|
111
|
+
|
|
112
|
+
### Narrow interface for `session-config.ts`
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
/** Narrow registry interface for session-config (ISP). */
|
|
116
|
+
export interface AgentConfigLookup {
|
|
117
|
+
resolveAgentConfig(type: string): AgentConfig;
|
|
118
|
+
getToolNamesForType(type: string): string[];
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`assembleSessionConfig` gains a `registry: AgentConfigLookup` parameter.
|
|
123
|
+
Tests construct a plain object satisfying this interface — no class instantiation needed.
|
|
124
|
+
|
|
125
|
+
### Threading through the call chain
|
|
126
|
+
|
|
127
|
+
`session-config.ts` ← called by `agent-runner.ts` ← called by `AgentManager`:
|
|
128
|
+
|
|
129
|
+
1. `assembleSessionConfig(type, ctx, options, env, registry)` — new param.
|
|
130
|
+
2. `RunOptions` gains `registry: AgentConfigLookup`.
|
|
131
|
+
3. `AgentManagerOptions` gains `registry: AgentTypeRegistry`.
|
|
132
|
+
4. `AgentManager` stores the registry and passes it to the runner via `RunOptions`.
|
|
133
|
+
|
|
134
|
+
### Replacing `reloadCustomAgents` callback
|
|
135
|
+
|
|
136
|
+
Before:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// index.ts
|
|
140
|
+
const reloadCustomAgents = () => {
|
|
141
|
+
const userAgents = loadCustomAgents(process.cwd());
|
|
142
|
+
registerAgents(userAgents);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// AgentToolDeps / AgentMenuDeps
|
|
146
|
+
reloadCustomAgents: () => void;
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
After:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// index.ts
|
|
153
|
+
const registry = new AgentTypeRegistry(() => loadCustomAgents(process.cwd()));
|
|
154
|
+
|
|
155
|
+
// AgentToolDeps / AgentMenuDeps
|
|
156
|
+
registry: AgentTypeRegistry;
|
|
157
|
+
|
|
158
|
+
// Callers use:
|
|
159
|
+
deps.registry.reload();
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Display helpers (`getDisplayName`, `getPromptModeLabel`)
|
|
163
|
+
|
|
164
|
+
These functions in `agent-widget.ts` call `resolveAgentConfig(type)`.
|
|
165
|
+
After the extraction, they accept the registry (or a `resolveAgentConfig` callback) as a parameter.
|
|
166
|
+
The `AgentWidget` constructor gains a `registry` parameter and passes it to these helpers internally.
|
|
167
|
+
External callers (`conversation-viewer.ts`, `get-result-tool.ts`, etc.) pass the registry they already hold via their deps.
|
|
168
|
+
|
|
169
|
+
## Module-Level Changes
|
|
170
|
+
|
|
171
|
+
### New
|
|
172
|
+
|
|
173
|
+
No new files.
|
|
174
|
+
|
|
175
|
+
### Modified
|
|
176
|
+
|
|
177
|
+
1. **`src/agent-types.ts`**
|
|
178
|
+
- Add `AgentTypeRegistry` class with all instance methods.
|
|
179
|
+
- Add `AgentConfigLookup` interface.
|
|
180
|
+
- Keep free functions temporarily (delegation shim during migration).
|
|
181
|
+
- Final step: remove free functions, module-scoped `agents` Map, and `registerAgents`.
|
|
182
|
+
|
|
183
|
+
2. **`src/types.ts`**
|
|
184
|
+
- Remove `DEFAULT_AGENT_NAMES` constant (moved to `AgentTypeRegistry.DEFAULT_AGENT_NAMES`).
|
|
185
|
+
|
|
186
|
+
3. **`src/session-config.ts`**
|
|
187
|
+
- `assembleSessionConfig` gains a `registry: AgentConfigLookup` parameter.
|
|
188
|
+
- Remove imports of `resolveAgentConfig`, `getToolNamesForType` from `agent-types.ts`.
|
|
189
|
+
|
|
190
|
+
4. **`src/agent-runner.ts`**
|
|
191
|
+
- `RunOptions` gains `registry: AgentConfigLookup`.
|
|
192
|
+
- `runAgent` passes `options.registry` to `assembleSessionConfig`.
|
|
193
|
+
|
|
194
|
+
5. **`src/agent-manager.ts`**
|
|
195
|
+
- `AgentManagerOptions` gains `registry: AgentTypeRegistry`.
|
|
196
|
+
- `AgentManager` stores `this.registry` and passes it into `RunOptions` when calling `runner.run`.
|
|
197
|
+
|
|
198
|
+
6. **`src/tools/agent-tool.ts`**
|
|
199
|
+
- `AgentToolDeps`: add `registry: AgentTypeRegistry`, remove `reloadCustomAgents`.
|
|
200
|
+
- Replace `resolveType(...)` / `resolveAgentConfig(...)` imports with `deps.registry.resolveType(...)` / `deps.registry.resolveAgentConfig(...)`.
|
|
201
|
+
- Replace `deps.reloadCustomAgents()` with `deps.registry.reload()`.
|
|
202
|
+
|
|
203
|
+
7. **`src/ui/agent-menu.ts`**
|
|
204
|
+
- `AgentMenuDeps`: add `registry: AgentTypeRegistry`, remove `reloadCustomAgents`.
|
|
205
|
+
- Replace `getAllTypes()`/`resolveAgentConfig()`/`resolveType()` imports with `deps.registry.*` calls.
|
|
206
|
+
- `BUILTIN_TOOL_NAMES` import stays (it is a constant, not a method).
|
|
207
|
+
- Replace `deps.reloadCustomAgents()` with `deps.registry.reload()`.
|
|
208
|
+
|
|
209
|
+
8. **`src/ui/agent-widget.ts`**
|
|
210
|
+
- `AgentWidget` constructor gains a `registry: AgentTypeRegistry` (or narrow interface) parameter.
|
|
211
|
+
- `getDisplayName(type, registry)` / `getPromptModeLabel(type, registry)` gain a registry parameter.
|
|
212
|
+
- Internal render methods use `this.registry`.
|
|
213
|
+
|
|
214
|
+
9. **`src/ui/conversation-viewer.ts`**
|
|
215
|
+
- `ConversationViewer` constructor gains a registry parameter.
|
|
216
|
+
- Passes it to `getDisplayName` / `getPromptModeLabel` calls.
|
|
217
|
+
|
|
218
|
+
10. **`src/tools/get-result-tool.ts`**
|
|
219
|
+
- `GetResultToolDeps` (or equivalent) gains registry.
|
|
220
|
+
- Passes it to `getDisplayName` calls.
|
|
221
|
+
|
|
222
|
+
11. **`src/index.ts`**
|
|
223
|
+
- Construct `AgentTypeRegistry` with `() => loadCustomAgents(process.cwd())`.
|
|
224
|
+
- Pass registry to `AgentManager`, agent-tool deps, menu deps, widget, get-result-tool deps.
|
|
225
|
+
- Remove `reloadCustomAgents` closure.
|
|
226
|
+
- Remove free-function imports (`registerAgents`, `getDefaultAgentNames`, `getUserAgentNames`, `getAvailableTypes`, `resolveAgentConfig`).
|
|
227
|
+
- Use `registry.*` methods directly for `buildTypeListText`.
|
|
228
|
+
|
|
229
|
+
### Removed
|
|
230
|
+
|
|
231
|
+
- Free functions from `agent-types.ts`: `registerAgents`, `resolveType`, `resolveAgentConfig`, `getAvailableTypes`, `getAllTypes`, `getDefaultAgentNames`, `getUserAgentNames`, `isValidType`, `getToolNamesForType` (final cleanup step).
|
|
232
|
+
- Module-scoped `agents` Map and `resolveKey` helper.
|
|
233
|
+
- `DEFAULT_AGENT_NAMES` from `types.ts`.
|
|
234
|
+
- `reloadCustomAgents` field from `AgentToolDeps` and `AgentMenuDeps`.
|
|
235
|
+
|
|
236
|
+
## Test Impact Analysis
|
|
237
|
+
|
|
238
|
+
### New tests enabled
|
|
239
|
+
|
|
240
|
+
- **Isolation without module resets:** Each test creates its own `AgentTypeRegistry` with a fixture callback, eliminating cross-test state leakage and the `registerAgents(new Map())` ceremony.
|
|
241
|
+
- **Reload behavior:** Tests can verify `registry.reload()` picks up new agents without touching module state.
|
|
242
|
+
|
|
243
|
+
### Existing tests that become redundant
|
|
244
|
+
|
|
245
|
+
- The existing free-function tests in `agent-types.test.ts` become redundant once the class tests cover the same behavior.
|
|
246
|
+
They can be removed in the final cleanup step.
|
|
247
|
+
|
|
248
|
+
### Existing tests that must stay
|
|
249
|
+
|
|
250
|
+
- `session-config.test.ts` — tests `assembleSessionConfig` behavior; mock setup changes from `vi.mock("agent-types.js")` to passing a mock `AgentConfigLookup` object.
|
|
251
|
+
- `agent-runner.test.ts`, `agent-runner-extension-tools.test.ts` — test runner behavior; mock setup changes from `vi.mock("agent-types.js")` to providing mock registry in `RunOptions`.
|
|
252
|
+
- `tools/agent-tool.test.ts` — tests tool handler; deps mock changes from `reloadCustomAgents: vi.fn()` to `registry: mockRegistry`.
|
|
253
|
+
- `ui/agent-menu.test.ts` — tests menu handler; deps mock changes similarly.
|
|
254
|
+
- `agent-manager.test.ts` — must add a mock registry to constructor options.
|
|
255
|
+
|
|
256
|
+
## TDD Order
|
|
257
|
+
|
|
258
|
+
1. **Create `AgentTypeRegistry` class** — Add class to `agent-types.ts` alongside existing free functions.
|
|
259
|
+
Add `AgentConfigLookup` interface.
|
|
260
|
+
Test all methods in a new `describe('AgentTypeRegistry')` block in `agent-types.test.ts`: construction, `reload()`, `resolveAgentConfig`, `resolveType`, `getAvailableTypes`, `getAllTypes`, `getDefaultAgentNames`, `getUserAgentNames`, `isValidType`, `getToolNamesForType`.
|
|
261
|
+
- Test surface: `agent-types.test.ts` — new describe block
|
|
262
|
+
- Commit: `feat(pi-subagents): add AgentTypeRegistry class (#108)`
|
|
263
|
+
|
|
264
|
+
2. **Inject through the config-assembly chain** — `assembleSessionConfig` gains `registry: AgentConfigLookup` param.
|
|
265
|
+
`RunOptions` gains `registry: AgentConfigLookup`.
|
|
266
|
+
`AgentManagerOptions` gains `registry: AgentTypeRegistry`.
|
|
267
|
+
Construct registry in `index.ts` and pass through `AgentManager` → `runAgent` → `assembleSessionConfig`.
|
|
268
|
+
Update `session-config.test.ts` (replace `vi.mock("agent-types.js")` with mock `AgentConfigLookup` object), `agent-runner.test.ts` and `agent-runner-extension-tools.test.ts` (provide mock registry in `RunOptions`, remove `vi.mock`), `agent-manager.test.ts` (add registry to constructor options).
|
|
269
|
+
- Test surface: `session-config.test.ts`, `agent-runner.test.ts`, `agent-runner-extension-tools.test.ts`, `agent-manager.test.ts`
|
|
270
|
+
- Commit: `refactor(pi-subagents): inject registry through config-assembly chain (#108)`
|
|
271
|
+
|
|
272
|
+
3. **Inject into agent tool** — Add `registry: AgentTypeRegistry` to `AgentToolDeps`, remove `reloadCustomAgents`.
|
|
273
|
+
Replace `resolveType` / `resolveAgentConfig` module imports with `deps.registry.*` calls.
|
|
274
|
+
Replace `deps.reloadCustomAgents()` with `deps.registry.reload()`.
|
|
275
|
+
Update `index.ts` agent-tool deps and `tools/agent-tool.test.ts`.
|
|
276
|
+
- Test surface: `tools/agent-tool.test.ts`
|
|
277
|
+
- Commit: `refactor(pi-subagents): inject registry into agent tool (#108)`
|
|
278
|
+
|
|
279
|
+
4. **Inject into agent menu** — Add `registry: AgentTypeRegistry` to `AgentMenuDeps`, remove `reloadCustomAgents`.
|
|
280
|
+
Replace `getAllTypes` / `resolveAgentConfig` / `resolveType` module imports with `deps.registry.*` calls.
|
|
281
|
+
Replace `deps.reloadCustomAgents()` with `deps.registry.reload()`.
|
|
282
|
+
Update `index.ts` menu deps and `ui/agent-menu.test.ts`.
|
|
283
|
+
- Test surface: `ui/agent-menu.test.ts`
|
|
284
|
+
- Commit: `refactor(pi-subagents): inject registry into agent menu (#108)`
|
|
285
|
+
|
|
286
|
+
5. **Inject into agent widget and display helpers** — `AgentWidget` constructor gains `registry`.
|
|
287
|
+
`getDisplayName(type, registry)` and `getPromptModeLabel(type, registry)` gain registry parameters.
|
|
288
|
+
`ConversationViewer` constructor gains registry.
|
|
289
|
+
`GetResultToolDeps` gains registry for `getDisplayName` calls.
|
|
290
|
+
Update `index.ts` wiring, `agent-widget.test.ts`, and any test files that call these helpers.
|
|
291
|
+
- Test surface: `agent-widget.test.ts`, related callers
|
|
292
|
+
- Commit: `refactor(pi-subagents): inject registry into agent widget (#108)`
|
|
293
|
+
|
|
294
|
+
6. **Move `DEFAULT_AGENT_NAMES` to registry** — Add static `DEFAULT_AGENT_NAMES` property on `AgentTypeRegistry`.
|
|
295
|
+
Remove the constant from `types.ts`.
|
|
296
|
+
Grep confirms no import consumers exist — the constant is defined but unused.
|
|
297
|
+
- Test surface: `agent-types.test.ts` — add assertion for static property
|
|
298
|
+
- Commit: `refactor(pi-subagents): move DEFAULT_AGENT_NAMES to registry (#108)`
|
|
299
|
+
|
|
300
|
+
7. **Remove free-function exports** — Delete `registerAgents`, `resolveType`, `resolveAgentConfig`, `getAvailableTypes`, `getAllTypes`, `getDefaultAgentNames`, `getUserAgentNames`, `isValidType`, `getToolNamesForType`, the module-scoped `agents` Map, and `resolveKey` helper from `agent-types.ts`.
|
|
301
|
+
Remove the free-function tests from `agent-types.test.ts` (now covered by class tests).
|
|
302
|
+
Remove any remaining free-function imports from `index.ts`.
|
|
303
|
+
Verify with `pnpm run check` that no dangling references remain.
|
|
304
|
+
- Test surface: `agent-types.test.ts` — remove old describe block
|
|
305
|
+
- Commit: `refactor(pi-subagents): remove free-function exports from agent-types (#108)`
|
|
306
|
+
|
|
307
|
+
## Risks and Mitigations
|
|
308
|
+
|
|
309
|
+
| Risk | Impact | Mitigation |
|
|
310
|
+
| --------------------------------------------------------------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
311
|
+
| Large blast radius — 11 source files, 8 test files | Merge conflicts if other PRs land concurrently | Lift-and-shift: free functions keep working until final removal; each step is a valid commit |
|
|
312
|
+
| `vi.mock("agent-types.js")` removal in runner tests changes behavior | Tests may expose latent bugs in session-config | Mock `AgentConfigLookup` with the same values the current `vi.mock` provides |
|
|
313
|
+
| Display helpers (`getDisplayName`, `getPromptModeLabel`) thread registry through many callers | Signature churn in UI layer | These callers already have a deps bag or constructor params — registry fits naturally |
|
|
314
|
+
| `agent-types.test.ts` (333 lines) needs migration from free-function tests to class tests | Large test rewrite in step 7 | Step 1 creates class tests first; step 7 only deletes the now-redundant free-function tests |
|
|
315
|
+
| `AgentRunner` interface change (`RunOptions` gains `registry`) | Breaks callers that construct `RunOptions` | Only `agent-manager.ts` constructs `RunOptions`; single-site change |
|
|
316
|
+
|
|
317
|
+
## Open Questions
|
|
318
|
+
|
|
319
|
+
- **`getDisplayName` / `getPromptModeLabel` placement:** These are thin display helpers that wrap `resolveAgentConfig`.
|
|
320
|
+
The plan proposes adding a registry parameter.
|
|
321
|
+
An alternative is to make them methods on the registry itself (e.g., `registry.getDisplayName(type)`), trading purity for convenience.
|
|
322
|
+
Decide during implementation based on how natural the call sites feel.
|
package/package.json
CHANGED
package/src/agent-manager.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { Model } from "@earendil-works/pi-ai";
|
|
|
11
11
|
import type { AgentSession, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { AgentRecord } from "./agent-record.js";
|
|
13
13
|
import type { AgentRunner } from "./agent-runner.js";
|
|
14
|
+
import { AgentTypeRegistry } from "./agent-types.js";
|
|
14
15
|
import { debugLog } from "./debug.js";
|
|
15
16
|
import { buildParentSnapshot } from "./parent-snapshot.js";
|
|
16
17
|
import { subscribeRecordObserver } from "./record-observer.js";
|
|
@@ -30,6 +31,7 @@ export interface AgentManagerOptions {
|
|
|
30
31
|
runner: AgentRunner;
|
|
31
32
|
worktrees: WorktreeManager;
|
|
32
33
|
exec: ShellExec;
|
|
34
|
+
registry: AgentTypeRegistry;
|
|
33
35
|
maxConcurrent?: number;
|
|
34
36
|
getRunConfig?: () => RunConfig;
|
|
35
37
|
onStart?: OnAgentStart;
|
|
@@ -81,6 +83,7 @@ export class AgentManager {
|
|
|
81
83
|
private readonly runner: AgentRunner;
|
|
82
84
|
private readonly worktrees: WorktreeManager;
|
|
83
85
|
private readonly exec: ShellExec;
|
|
86
|
+
private readonly registry: AgentTypeRegistry;
|
|
84
87
|
private maxConcurrent: number;
|
|
85
88
|
private getRunConfig?: () => RunConfig;
|
|
86
89
|
|
|
@@ -93,6 +96,7 @@ export class AgentManager {
|
|
|
93
96
|
this.runner = options.runner;
|
|
94
97
|
this.worktrees = options.worktrees;
|
|
95
98
|
this.exec = options.exec;
|
|
99
|
+
this.registry = options.registry;
|
|
96
100
|
this.onComplete = options.onComplete;
|
|
97
101
|
this.onStart = options.onStart;
|
|
98
102
|
this.onCompact = options.onCompact;
|
|
@@ -203,6 +207,7 @@ export class AgentManager {
|
|
|
203
207
|
parentSessionFile: options.parentSessionFile,
|
|
204
208
|
parentSessionId: options.parentSessionId,
|
|
205
209
|
signal: record.abortController!.signal,
|
|
210
|
+
registry: this.registry,
|
|
206
211
|
onSessionCreated: (session) => {
|
|
207
212
|
record.session = session;
|
|
208
213
|
// Capture the session file path early so it's available for display
|
package/src/agent-runner.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
SessionManager,
|
|
13
13
|
SettingsManager,
|
|
14
14
|
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import type { AgentConfigLookup } from "./agent-types.js";
|
|
15
16
|
import { extractText } from "./context.js";
|
|
16
17
|
import { detectEnv } from "./env.js";
|
|
17
18
|
import { assembleSessionConfig } from "./session-config.js";
|
|
@@ -91,6 +92,8 @@ export interface RunOptions {
|
|
|
91
92
|
* module-scope `graceTurns` during migration.
|
|
92
93
|
*/
|
|
93
94
|
graceTurns?: number;
|
|
95
|
+
/** Agent config lookup — provides resolveAgentConfig and getToolNamesForType. */
|
|
96
|
+
registry: AgentConfigLookup;
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
export interface RunResult {
|
|
@@ -189,6 +192,7 @@ export async function runAgent(
|
|
|
189
192
|
thinkingLevel: options.thinkingLevel,
|
|
190
193
|
},
|
|
191
194
|
env,
|
|
195
|
+
options.registry,
|
|
192
196
|
);
|
|
193
197
|
|
|
194
198
|
const agentDir = getAgentDir();
|
package/src/agent-types.ts
CHANGED
|
@@ -8,79 +8,132 @@
|
|
|
8
8
|
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
9
9
|
import type { AgentConfig } from "./types.js";
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
11
|
+
// ── AgentConfigLookup interface ──────────────────────────────────────────────
|
|
13
12
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Narrow registry interface for consumers that only need config resolution.
|
|
15
|
+
* Prefer this over the full `AgentTypeRegistry` in function signatures (ISP).
|
|
16
|
+
*/
|
|
17
|
+
export interface AgentConfigLookup {
|
|
18
|
+
resolveAgentConfig(type: string): AgentConfig;
|
|
19
|
+
getToolNamesForType(type: string): string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── AgentTypeRegistry class ──────────────────────────────────────────────────
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
25
|
+
* Injectable registry of all agent configurations (defaults + user-defined).
|
|
26
|
+
*
|
|
27
|
+
* Replaces the module-scoped `agents` Map and its companion free functions.
|
|
28
|
+
* The constructor accepts a `loadUserAgents` callback to defer disk I/O to the
|
|
29
|
+
* call site, keeping this class side-effect-free and easy to test.
|
|
21
30
|
*/
|
|
22
|
-
export
|
|
23
|
-
agents
|
|
31
|
+
export class AgentTypeRegistry implements AgentConfigLookup {
|
|
32
|
+
private agents = new Map<string, AgentConfig>();
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
/** The three embedded default agent names. */
|
|
35
|
+
static readonly DEFAULT_AGENT_NAMES = ["general-purpose", "Explore", "Plan"] as const;
|
|
36
|
+
|
|
37
|
+
constructor(private loadUserAgents: () => Map<string, AgentConfig>) {
|
|
38
|
+
this.reload();
|
|
28
39
|
}
|
|
29
40
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Re-scan user agents and rebuild the registry.
|
|
43
|
+
* Starts with DEFAULT_AGENTS, then overlays whatever `loadUserAgents()` returns.
|
|
44
|
+
*/
|
|
45
|
+
reload(): void {
|
|
46
|
+
this.agents.clear();
|
|
47
|
+
for (const [name, config] of DEFAULT_AGENTS) {
|
|
48
|
+
this.agents.set(name, config);
|
|
49
|
+
}
|
|
50
|
+
for (const [name, config] of this.loadUserAgents()) {
|
|
51
|
+
this.agents.set(name, config);
|
|
52
|
+
}
|
|
33
53
|
}
|
|
34
|
-
}
|
|
35
54
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const lower = name.toLowerCase();
|
|
40
|
-
for (const key of agents.keys()) {
|
|
41
|
-
if (key.toLowerCase() === lower) return key;
|
|
55
|
+
/** Resolve a type name case-insensitively. Returns the canonical key or undefined. */
|
|
56
|
+
resolveType(name: string): string | undefined {
|
|
57
|
+
return this.resolveKey(name);
|
|
42
58
|
}
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
59
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
/** Get all enabled type names (for spawning and tool descriptions). */
|
|
61
|
+
getAvailableTypes(): string[] {
|
|
62
|
+
return [...this.agents.entries()]
|
|
63
|
+
.filter(([_, config]) => config.enabled !== false)
|
|
64
|
+
.map(([name]) => name);
|
|
65
|
+
}
|
|
50
66
|
|
|
51
|
-
/** Get all
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.map(([name]) => name);
|
|
56
|
-
}
|
|
67
|
+
/** Get all type names including disabled (for UI listing). */
|
|
68
|
+
getAllTypes(): string[] {
|
|
69
|
+
return [...this.agents.keys()];
|
|
70
|
+
}
|
|
57
71
|
|
|
58
|
-
/** Get
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
/** Get names of default agents currently in the registry. */
|
|
73
|
+
getDefaultAgentNames(): string[] {
|
|
74
|
+
return [...this.agents.entries()]
|
|
75
|
+
.filter(([_, config]) => config.isDefault === true)
|
|
76
|
+
.map(([name]) => name);
|
|
77
|
+
}
|
|
62
78
|
|
|
63
|
-
/** Get names of
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
79
|
+
/** Get names of user-defined agents (non-defaults) currently in the registry. */
|
|
80
|
+
getUserAgentNames(): string[] {
|
|
81
|
+
return [...this.agents.entries()]
|
|
82
|
+
.filter(([_, config]) => config.isDefault !== true)
|
|
83
|
+
.map(([name]) => name);
|
|
84
|
+
}
|
|
69
85
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.
|
|
75
|
-
}
|
|
86
|
+
/** Check if a type is valid and enabled (case-insensitive). */
|
|
87
|
+
isValidType(type: string): boolean {
|
|
88
|
+
const key = this.resolveKey(type);
|
|
89
|
+
if (!key) return false;
|
|
90
|
+
return this.agents.get(key)?.enabled !== false;
|
|
91
|
+
}
|
|
76
92
|
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
/** Get built-in tool names for a type (case-insensitive). */
|
|
94
|
+
getToolNamesForType(type: string): string[] {
|
|
95
|
+
const key = this.resolveKey(type);
|
|
96
|
+
const raw = key ? this.agents.get(key) : undefined;
|
|
97
|
+
const config = raw?.enabled !== false ? raw : undefined;
|
|
98
|
+
const names = config?.builtinToolNames?.length ? config.builtinToolNames : [...BUILTIN_TOOL_NAMES];
|
|
99
|
+
return names;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Resolve agent config with guaranteed non-null return. Falls back: unknown → general-purpose → absolute fallback. */
|
|
103
|
+
resolveAgentConfig(type: string): AgentConfig {
|
|
104
|
+
const key = this.resolveKey(type);
|
|
105
|
+
const config = key ? this.agents.get(key) : undefined;
|
|
106
|
+
if (config) return config;
|
|
107
|
+
|
|
108
|
+
const gp = this.agents.get("general-purpose");
|
|
109
|
+
if (gp) return gp;
|
|
110
|
+
|
|
111
|
+
// Absolute fallback (should never happen in practice)
|
|
112
|
+
return {
|
|
113
|
+
name: type,
|
|
114
|
+
displayName: "Agent",
|
|
115
|
+
description: "General-purpose agent for complex, multi-step tasks",
|
|
116
|
+
builtinToolNames: BUILTIN_TOOL_NAMES,
|
|
117
|
+
extensions: true,
|
|
118
|
+
skills: true,
|
|
119
|
+
systemPrompt: "",
|
|
120
|
+
promptMode: "append",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private resolveKey(name: string): string | undefined {
|
|
125
|
+
if (this.agents.has(name)) return name;
|
|
126
|
+
const lower = name.toLowerCase();
|
|
127
|
+
for (const key of this.agents.keys()) {
|
|
128
|
+
if (key.toLowerCase() === lower) return key;
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
82
132
|
}
|
|
83
133
|
|
|
134
|
+
/** All known built-in tool names. */
|
|
135
|
+
export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
136
|
+
|
|
84
137
|
/** Tool names required for memory management. */
|
|
85
138
|
const MEMORY_TOOL_NAMES = ["read", "write", "edit"];
|
|
86
139
|
|
|
@@ -100,39 +153,3 @@ const READONLY_MEMORY_TOOL_NAMES = ["read"];
|
|
|
100
153
|
export function getReadOnlyMemoryToolNames(existingToolNames: Set<string>): string[] {
|
|
101
154
|
return READONLY_MEMORY_TOOL_NAMES.filter(n => !existingToolNames.has(n));
|
|
102
155
|
}
|
|
103
|
-
|
|
104
|
-
/** Get built-in tool names for a type (case-insensitive). */
|
|
105
|
-
export function getToolNamesForType(type: string): string[] {
|
|
106
|
-
const key = resolveKey(type);
|
|
107
|
-
const raw = key ? agents.get(key) : undefined;
|
|
108
|
-
const config = raw?.enabled !== false ? raw : undefined;
|
|
109
|
-
const names = config?.builtinToolNames?.length ? config.builtinToolNames : [...BUILTIN_TOOL_NAMES];
|
|
110
|
-
return names;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Resolve agent config with guaranteed non-null return. Falls back: unknown → general-purpose → absolute fallback. */
|
|
114
|
-
export function resolveAgentConfig(type: string): AgentConfig {
|
|
115
|
-
const key = resolveKey(type);
|
|
116
|
-
const config = key ? agents.get(key) : undefined;
|
|
117
|
-
if (config) {
|
|
118
|
-
return config;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Fallback to general-purpose for unknown types
|
|
122
|
-
const gp = agents.get("general-purpose");
|
|
123
|
-
if (gp) {
|
|
124
|
-
return gp;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Absolute fallback (should never happen in practice)
|
|
128
|
-
return {
|
|
129
|
-
name: type,
|
|
130
|
-
displayName: "Agent",
|
|
131
|
-
description: "General-purpose agent for complex, multi-step tasks",
|
|
132
|
-
builtinToolNames: BUILTIN_TOOL_NAMES,
|
|
133
|
-
extensions: true,
|
|
134
|
-
skills: true,
|
|
135
|
-
systemPrompt: "",
|
|
136
|
-
promptMode: "append",
|
|
137
|
-
};
|
|
138
|
-
}
|