@gotgenes/pi-subagents 5.3.0 → 5.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 +13 -0
- package/docs/plans/0080-consolidate-agent-config-lookup.md +247 -0
- package/docs/retro/0071-extract-session-config-assembler.md +60 -0
- package/package.json +1 -1
- package/src/agent-types.ts +10 -36
- package/src/index.ts +8 -8
- package/src/session-config.ts +17 -37
- package/src/tools/agent-tool.ts +3 -3
- package/src/ui/agent-menu.ts +11 -10
- package/src/ui/agent-widget.ts +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [5.4.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.3.0...pi-subagents-v5.4.0) (2026-05-20)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add resolveAgentConfig with guaranteed-non-null fallback chain ([6b676a0](https://github.com/gotgenes/pi-packages/commit/6b676a0b59ebc3598d366cb5db600a8177b301e6))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
* plan consolidate getConfig/getAgentConfig into resolveAgentConfig ([#80](https://github.com/gotgenes/pi-packages/issues/80)) ([1c14b47](https://github.com/gotgenes/pi-packages/commit/1c14b4760fa67dff5dbf17306d04bbe992b38bce))
|
|
19
|
+
* **retro:** add retro notes for issue [#71](https://github.com/gotgenes/pi-packages/issues/71) ([a70e52f](https://github.com/gotgenes/pi-packages/commit/a70e52f840cf3b2f65689987dcb7316e32dc12ff))
|
|
20
|
+
|
|
8
21
|
## [5.3.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.2.0...pi-subagents-v5.3.0) (2026-05-19)
|
|
9
22
|
|
|
10
23
|
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 80
|
|
3
|
+
issue_title: "refactor: consolidate getConfig / getAgentConfig into a single resolution path"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Consolidate agent config lookup into resolveAgentConfig
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`agent-types.ts` exports two overlapping lookup functions:
|
|
11
|
+
|
|
12
|
+
- `getConfig(type)` — returns a narrow shape (`displayName`, `description`, `builtinToolNames`, `extensions`, `skills`, `promptMode`) with a guaranteed-non-null fallback chain (unknown → general-purpose → absolute fallback).
|
|
13
|
+
- `getAgentConfig(type)` — returns the full `AgentConfig | undefined`.
|
|
14
|
+
|
|
15
|
+
Every field `getConfig()` returns also exists on `AgentConfig`.
|
|
16
|
+
Callers that need both must call both and keep them in sync:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
const config = getConfig(type);
|
|
20
|
+
const agentConfig = getAgentConfig(type);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This showed up concretely in `session-config.ts` (extracted in #71), where the assembler calls both, then handles the `agentConfig === undefined` fallback separately — duplicating the same fallback chain that `getConfig()` already handles internally.
|
|
24
|
+
Test mocks must also set up both `mockGetConfig` and `mockGetAgentConfig` with compatible values, which is error-prone.
|
|
25
|
+
|
|
26
|
+
## Goals
|
|
27
|
+
|
|
28
|
+
- Add a single `resolveAgentConfig(type): AgentConfig` function that returns a guaranteed-non-null `AgentConfig`, handling the fallback chain internally (unknown → general-purpose → absolute fallback).
|
|
29
|
+
- Migrate all callers of `getConfig()` and `getAgentConfig()` to `resolveAgentConfig()`.
|
|
30
|
+
- Remove `getConfig()` and `getAgentConfig()`.
|
|
31
|
+
- Simplify test mocks from two compatible stubs to one.
|
|
32
|
+
|
|
33
|
+
This is a pure internal refactor — no behavior change, no public API impact (the package's `exports` only expose `service.ts`).
|
|
34
|
+
|
|
35
|
+
## Non-Goals
|
|
36
|
+
|
|
37
|
+
- Changing the `AgentConfig` type shape.
|
|
38
|
+
- Refactoring `getToolNamesForType()` (it has its own lookup logic and is a separate concern).
|
|
39
|
+
- Modifying the public API surface (`service.ts`).
|
|
40
|
+
- Changing the fallback semantics (the chain stays: type → general-purpose → absolute fallback).
|
|
41
|
+
|
|
42
|
+
## Background
|
|
43
|
+
|
|
44
|
+
### Relevant modules
|
|
45
|
+
|
|
46
|
+
| Module | Role |
|
|
47
|
+
| --------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
48
|
+
| `agent-types.ts` | Unified agent type registry — owns `getConfig`, `getAgentConfig`, `resolveType`, `getToolNamesForType`, `isValidType` |
|
|
49
|
+
| `session-config.ts` | Pure configuration assembler — calls both `getConfig` + `getAgentConfig` |
|
|
50
|
+
| `ui/agent-widget.ts` | Widget rendering — calls `getConfig` via `getDisplayName()` and `getPromptModeLabel()` helpers |
|
|
51
|
+
| `index.ts` | Extension entry point — calls `getAgentConfig` (3 sites) for type listing and model label |
|
|
52
|
+
| `tools/agent-tool.ts` | Agent tool handler — calls `getAgentConfig` (1 site) after `resolveType()` |
|
|
53
|
+
| `ui/agent-menu.ts` | Agent menu UI — calls `getAgentConfig` (5 sites) for listing, detail, and existence checks |
|
|
54
|
+
|
|
55
|
+
### Current caller inventory
|
|
56
|
+
|
|
57
|
+
Source files that import `getConfig` or `getAgentConfig`:
|
|
58
|
+
|
|
59
|
+
| File | `getConfig` calls | `getAgentConfig` calls | Notes |
|
|
60
|
+
| --------------------- | ----------------- | ---------------------- | --------------------------------------------- |
|
|
61
|
+
| `session-config.ts` | 1 | 1 | Primary motivation; dual-call pattern |
|
|
62
|
+
| `ui/agent-widget.ts` | 2 (via helpers) | 0 | `getDisplayName()`, `getPromptModeLabel()` |
|
|
63
|
+
| `index.ts` | 0 | 3 | Iterates known names; `?.` is defensive |
|
|
64
|
+
| `tools/agent-tool.ts` | 0 | 1 | After `resolveType()` — guaranteed to exist |
|
|
65
|
+
| `ui/agent-menu.ts` | 0 | 5 | 3 iterate known names; 2 are existence guards |
|
|
66
|
+
|
|
67
|
+
Test files that mock `agent-types.js` with both functions:
|
|
68
|
+
|
|
69
|
+
| Test file | Mocks `getConfig` | Mocks `getAgentConfig` |
|
|
70
|
+
| -------------------------------------- | ----------------- | ---------------------- |
|
|
71
|
+
| `session-config.test.ts` | ✓ | ✓ |
|
|
72
|
+
| `agent-runner.test.ts` | ✓ | ✓ |
|
|
73
|
+
| `agent-runner-extension-tools.test.ts` | ✓ | ✓ |
|
|
74
|
+
|
|
75
|
+
### Caller migration notes
|
|
76
|
+
|
|
77
|
+
- **`session-config.ts`**: Replace both calls with one `resolveAgentConfig(type)`.
|
|
78
|
+
Read `extensions` and `skills` directly from the returned `AgentConfig` instead of the narrow shape.
|
|
79
|
+
The prompt-building fallback (`agentConfig` null → use `DEFAULT_AGENTS.get("general-purpose")`) collapses into the single call since `resolveAgentConfig` already handles the fallback.
|
|
80
|
+
- **`ui/agent-widget.ts`**: `getDisplayName()` becomes `resolveAgentConfig(type).displayName ?? resolveAgentConfig(type).name` (or cache the result).
|
|
81
|
+
`getPromptModeLabel()` reads `.promptMode` from the resolved config.
|
|
82
|
+
- **`index.ts`**: All 3 call sites iterate names from `getDefaultAgentNames()` or `getUserAgentNames()` — configs are guaranteed to exist.
|
|
83
|
+
Direct replacement with `resolveAgentConfig()`.
|
|
84
|
+
- **`tools/agent-tool.ts`**: Called after `resolveType()` — config guaranteed.
|
|
85
|
+
Direct replacement.
|
|
86
|
+
- **`ui/agent-menu.ts`**: 3 of 5 calls iterate `getAllTypes()` — configs guaranteed.
|
|
87
|
+
The 2 defensive existence guards (lines 187, 248) switch to `resolveType(name) != null` checks, then use `resolveAgentConfig()` for the config.
|
|
88
|
+
|
|
89
|
+
### Constraints from AGENTS.md
|
|
90
|
+
|
|
91
|
+
- Keep modules focused and composable (one concern per file).
|
|
92
|
+
- Prefer explicit configuration over hidden behavior.
|
|
93
|
+
- Use `vi.hoisted()` for module-level mocks, `.mockClear()` when the factory provides a default.
|
|
94
|
+
- Run `pnpm run check` after interface changes.
|
|
95
|
+
|
|
96
|
+
## Design Overview
|
|
97
|
+
|
|
98
|
+
### `resolveAgentConfig` semantics
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
export function resolveAgentConfig(type: string): AgentConfig
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
1. Case-insensitive key lookup via `resolveKey(type)`.
|
|
105
|
+
2. If found and `enabled !== false`, return the `AgentConfig`.
|
|
106
|
+
3. Fallback to `general-purpose` (if present and enabled).
|
|
107
|
+
4. Absolute fallback: a synthetic `AgentConfig` with safe defaults (same values as today's `getConfig` absolute fallback, plus the missing `AgentConfig` fields like `name`, `systemPrompt`).
|
|
108
|
+
|
|
109
|
+
The function is pure (reads from the module-level `agents` map) and deterministic.
|
|
110
|
+
|
|
111
|
+
### Absolute fallback shape
|
|
112
|
+
|
|
113
|
+
The absolute fallback is a complete `AgentConfig` synthesized inline, matching the current `getConfig` absolute fallback values plus required `AgentConfig` fields:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
{
|
|
117
|
+
name: type,
|
|
118
|
+
displayName: "Agent",
|
|
119
|
+
description: "General-purpose agent for complex, multi-step tasks",
|
|
120
|
+
builtinToolNames: BUILTIN_TOOL_NAMES,
|
|
121
|
+
extensions: true,
|
|
122
|
+
skills: true,
|
|
123
|
+
systemPrompt: "",
|
|
124
|
+
promptMode: "append",
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### session-config.ts simplification
|
|
129
|
+
|
|
130
|
+
Before (two calls, two variables, null-check branching):
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
const config = getConfig(type);
|
|
134
|
+
const agentConfig = getAgentConfig(type);
|
|
135
|
+
// ...
|
|
136
|
+
const extensions = options.isolated ? false : config.extensions;
|
|
137
|
+
const skills = options.isolated ? false : config.skills;
|
|
138
|
+
// ...
|
|
139
|
+
if (agentConfig) {
|
|
140
|
+
systemPrompt = buildAgentPrompt(agentConfig, ...);
|
|
141
|
+
} else {
|
|
142
|
+
const fallback = DEFAULT_AGENTS.get("general-purpose");
|
|
143
|
+
systemPrompt = buildAgentPrompt({ ...fallback, name: type }, ...);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
After (one call, no null checks):
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
const agentConfig = resolveAgentConfig(type);
|
|
151
|
+
// ...
|
|
152
|
+
const extensions = options.isolated ? false : agentConfig.extensions;
|
|
153
|
+
const skills = options.isolated ? false : agentConfig.skills;
|
|
154
|
+
// ...
|
|
155
|
+
systemPrompt = buildAgentPrompt(agentConfig, ...);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The `DEFAULT_AGENTS` import in `session-config.ts` becomes unnecessary.
|
|
159
|
+
|
|
160
|
+
## Module-Level Changes
|
|
161
|
+
|
|
162
|
+
### `src/agent-types.ts`
|
|
163
|
+
|
|
164
|
+
- **Add** `resolveAgentConfig(type: string): AgentConfig` — guaranteed-non-null lookup with fallback chain.
|
|
165
|
+
- **Remove** `getConfig(type)` — replaced by `resolveAgentConfig`.
|
|
166
|
+
- **Remove** `getAgentConfig(type)` — replaced by `resolveAgentConfig`.
|
|
167
|
+
|
|
168
|
+
### `src/session-config.ts`
|
|
169
|
+
|
|
170
|
+
- **Change** imports: replace `getConfig`, `getAgentConfig` with `resolveAgentConfig`.
|
|
171
|
+
- **Remove** `DEFAULT_AGENTS` import (no longer needed for the unknown-type fallback prompt path).
|
|
172
|
+
- **Simplify** `assembleSessionConfig()`: one `resolveAgentConfig(type)` call replaces two; remove the `if (agentConfig)` / `else` branch for prompt building.
|
|
173
|
+
|
|
174
|
+
### `src/ui/agent-widget.ts`
|
|
175
|
+
|
|
176
|
+
- **Change** import: replace `getConfig` with `resolveAgentConfig`.
|
|
177
|
+
- **Update** `getDisplayName()`: `const config = resolveAgentConfig(type); return config.displayName ?? config.name;`.
|
|
178
|
+
- **Update** `getPromptModeLabel()`: read `promptMode` from resolved config.
|
|
179
|
+
|
|
180
|
+
### `src/index.ts`
|
|
181
|
+
|
|
182
|
+
- **Change** import: replace `getAgentConfig` with `resolveAgentConfig`.
|
|
183
|
+
- **Update** 3 call sites: direct replacement (all iterate known names).
|
|
184
|
+
|
|
185
|
+
### `src/tools/agent-tool.ts`
|
|
186
|
+
|
|
187
|
+
- **Change** import: replace `getAgentConfig` with `resolveAgentConfig`.
|
|
188
|
+
- **Update** 1 call site: direct replacement (type already resolved via `resolveType()`).
|
|
189
|
+
|
|
190
|
+
### `src/ui/agent-menu.ts`
|
|
191
|
+
|
|
192
|
+
- **Change** import: replace `getAgentConfig` with `resolveAgentConfig`, add `resolveType` if not already imported.
|
|
193
|
+
- **Update** 5 call sites: 3 are direct replacements; 2 existence guards switch to `resolveType(name) != null` before calling `resolveAgentConfig()`.
|
|
194
|
+
|
|
195
|
+
### Test files
|
|
196
|
+
|
|
197
|
+
- **`test/agent-types.test.ts`**: Add tests for `resolveAgentConfig`; remove tests for `getConfig` and `getAgentConfig`.
|
|
198
|
+
- **`test/session-config.test.ts`**: Replace `mockGetConfig` + `mockGetAgentConfig` with a single `mockResolveAgentConfig`; remove all `mockGetConfig` usages.
|
|
199
|
+
- **`test/agent-runner.test.ts`**: Update `vi.mock("../src/agent-types.js")` factory: replace `getConfig` + `getAgentConfig` with `resolveAgentConfig`.
|
|
200
|
+
- **`test/agent-runner-extension-tools.test.ts`**: Same mock update as above.
|
|
201
|
+
|
|
202
|
+
## Test Impact Analysis
|
|
203
|
+
|
|
204
|
+
1. **New unit tests enabled**: `resolveAgentConfig` gets focused tests for the fallback chain (unknown → general-purpose → absolute fallback), case-insensitive lookup, and disabled-type fallback.
|
|
205
|
+
These replace the scattered `getConfig` fallback tests in `agent-types.test.ts`.
|
|
206
|
+
2. **Redundant tests**: `getConfig`-specific tests (return shape, fallback, extension allowlist) become redundant since `resolveAgentConfig` returns the full `AgentConfig` directly.
|
|
207
|
+
Remove them.
|
|
208
|
+
3. **Tests that must stay**: `agent-types.test.ts` tests for `registerAgents`, `resolveType`, `isValidType`, `getAvailableTypes`, `getAllTypes`, `getToolNamesForType`, `getMemoryToolNames`, `getReadOnlyMemoryToolNames` are unaffected.
|
|
209
|
+
`session-config.test.ts` tests all stay — they test assembler behavior, just with a simpler mock setup.
|
|
210
|
+
|
|
211
|
+
## TDD Order
|
|
212
|
+
|
|
213
|
+
1. **Add `resolveAgentConfig` with tests** — add the function to `agent-types.ts` alongside existing functions.
|
|
214
|
+
Tests: known type returns config; unknown type falls back to general-purpose; disabled type falls back; absolute fallback when general-purpose is missing; case-insensitive lookup.
|
|
215
|
+
Commit: `feat: add resolveAgentConfig with guaranteed-non-null fallback chain`.
|
|
216
|
+
|
|
217
|
+
2. **Migrate `session-config.ts` and its tests** — replace `getConfig` + `getAgentConfig` imports with `resolveAgentConfig`; remove `DEFAULT_AGENTS` import; simplify the assembler body (one call, no null-check branch).
|
|
218
|
+
Update `session-config.test.ts`: replace `mockGetConfig` + `mockGetAgentConfig` with single `mockResolveAgentConfig`; update all test setups.
|
|
219
|
+
Commit: `refactor: migrate session-config to resolveAgentConfig`.
|
|
220
|
+
|
|
221
|
+
3. **Migrate `agent-widget.ts`** — update `getDisplayName()` and `getPromptModeLabel()` to use `resolveAgentConfig`.
|
|
222
|
+
Commit: `refactor: migrate agent-widget to resolveAgentConfig`.
|
|
223
|
+
|
|
224
|
+
4. **Migrate remaining source callers** — update `index.ts` (3 sites), `tools/agent-tool.ts` (1 site), `ui/agent-menu.ts` (5 sites, including 2 existence-guard rewrites).
|
|
225
|
+
Commit: `refactor: migrate remaining callers to resolveAgentConfig`.
|
|
226
|
+
|
|
227
|
+
5. **Update transitive test mocks** — update `agent-runner.test.ts` and `agent-runner-extension-tools.test.ts` mock factories to export `resolveAgentConfig` instead of `getConfig` + `getAgentConfig`.
|
|
228
|
+
Commit: `test: update agent-runner test mocks for resolveAgentConfig`.
|
|
229
|
+
|
|
230
|
+
6. **Remove `getConfig` and `getAgentConfig`** — delete both functions from `agent-types.ts`; remove their tests from `agent-types.test.ts`.
|
|
231
|
+
Run `pnpm run check` to verify no remaining references.
|
|
232
|
+
Commit: `refactor: remove getConfig and getAgentConfig`.
|
|
233
|
+
|
|
234
|
+
## Risks and Mitigations
|
|
235
|
+
|
|
236
|
+
| Risk | Mitigation |
|
|
237
|
+
| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
238
|
+
| Overlooked caller of removed functions | Grep all `src/` and `test/` files for `getConfig` and `getAgentConfig` before the removal step; `pnpm run check` catches any remaining imports. |
|
|
239
|
+
| `agent-menu.ts` existence guards behave differently with `resolveType()` | `resolveType()` is already the canonical existence check used by `agent-tool.ts`; the guard semantics are identical. |
|
|
240
|
+
| Absolute fallback `AgentConfig` shape drift | The absolute fallback is a single inline literal — any future `AgentConfig` field additions will cause a type error at the definition site. |
|
|
241
|
+
| Test mock compatibility during migration | Lift-and-shift: `resolveAgentConfig` coexists with old functions until all callers are migrated; each step leaves tests green. |
|
|
242
|
+
|
|
243
|
+
## Open Questions
|
|
244
|
+
|
|
245
|
+
- `getToolNamesForType()` does its own lookup with similar fallback logic.
|
|
246
|
+
It could be simplified to delegate to `resolveAgentConfig()`, but that is out of scope for this issue.
|
|
247
|
+
Consider a follow-up if the duplication becomes a maintenance concern.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 71
|
|
3
|
+
issue_title: "refactor: extract pure agent-session assembler from agent-runner.ts"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #71 - extract session-config assembler
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-19T22:15:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned, implemented, and shipped `assembleSessionConfig()` - a pure configuration assembler extracted from `runAgent()` in `agent-runner.ts`.
|
|
13
|
+
Eight TDD steps completed across three phases (plan, implement, ship), adding 33 new tests in `test/session-config.test.ts` and reducing `runAgent()` from ~390 lines to ~198.
|
|
14
|
+
Released as `pi-subagents-v5.3.0`.
|
|
15
|
+
Filed follow-up #80 (consolidate `getConfig`/`getAgentConfig`) and updated the architecture roadmap.
|
|
16
|
+
|
|
17
|
+
### Observations
|
|
18
|
+
|
|
19
|
+
#### What went well
|
|
20
|
+
|
|
21
|
+
- The user's redirecting question ("Do we need more decomposition?
|
|
22
|
+
Are there any seams we can exploit?") at the `as any` fix point was perfectly timed.
|
|
23
|
+
It shifted the approach from mechanical casting (8 `as any` casts) to a structural fix: `unknown` for opaque model handles, `Array<{ provider: string; id: string }>` for the availability check.
|
|
24
|
+
The final design removed the `@earendil-works/pi-ai` import from `session-config.ts` entirely - cleaner than the plan's original specification of `Model<any>`.
|
|
25
|
+
- The prior art from `pi-permission-system` (`evaluate()` extraction) provided a clear template for the pure-core-from-IO-shell pattern.
|
|
26
|
+
Design decisions were minimal.
|
|
27
|
+
- All 451 existing tests stayed green through every intermediate commit.
|
|
28
|
+
The mock-at-module-boundary strategy (`vi.mock("../src/agent-types.js")` etc.) meant existing `agent-runner.test.ts` mocks continued to intercept the same module paths even after the assembler delegated to them.
|
|
29
|
+
|
|
30
|
+
#### What caused friction (agent side)
|
|
31
|
+
|
|
32
|
+
- `premature-convergence` - When `pnpm run check` surfaced 10 type errors from `Model<any>` in `SessionConfig`, the first fix attempt was adding `as any` casts to the `vi.fn` factory return values and all test model objects (8 casts total).
|
|
33
|
+
This partially addressed the symptom but created new `never[]` inference errors and left a fundamentally wrong interface.
|
|
34
|
+
The user's first pushback ("Can we find a real type?") prompted a proper analysis of `Model<TApi>`'s ~10 required fields, leading to the `unknown` solution.
|
|
35
|
+
Impact: ~20 minutes of rework across 3 attempts (the `as any` factory cast, diagnosing the residual `never[]` errors, and the final `unknown` rewrite). (user-caught)
|
|
36
|
+
|
|
37
|
+
- `missing-context` - The plan omitted `agentMaxTurns` from the `SessionConfig` return type.
|
|
38
|
+
`runAgent()`'s turn-limit resolution reads `agentConfig?.maxTurns`, which is no longer available after the assembly delegation.
|
|
39
|
+
Caught during step 7 (wiring) when the code wouldn't compile without it.
|
|
40
|
+
Impact: added one field to `SessionConfig` and `assembleSessionConfig`; no rework of earlier steps, but the commit body noted the deviation. (self-identified at implementation time)
|
|
41
|
+
|
|
42
|
+
- `missing-context` - The `vi.fn(() => [])` → `never[]` TypeScript inference issue wasn't anticipated.
|
|
43
|
+
Five mock factories (`mockGetMemoryToolNames`, `mockGetReadOnlyMemoryToolNames`, `mockPreloadSkills`, `mockRegistry.find`, `mockRegistry.getAvailable`) needed explicit return-type annotations.
|
|
44
|
+
Impact: one debug cycle to diagnose, then a second to fix with `import type` annotations. (self-identified during `pnpm run check`)
|
|
45
|
+
|
|
46
|
+
#### What caused friction (user side)
|
|
47
|
+
|
|
48
|
+
- The user's second pushback ("Do we need more decomposition?") was the highest-leverage intervention in the session.
|
|
49
|
+
Without it, the `as any` approach would have landed and the `pi-ai` import would have stayed in `session-config.ts` - defeating the goal of a SDK-free business-logic module.
|
|
50
|
+
The pattern of catching design-level issues through "is there a better seam?"
|
|
51
|
+
questions is worth preserving.
|
|
52
|
+
|
|
53
|
+
#### Design observations (not actionable as rules)
|
|
54
|
+
|
|
55
|
+
- The `cfg.model as Model<any> | undefined` cast in `agent-runner.ts` is a legitimate interim cost — it's one line at the SDK boundary and will be resolved when #66 (replace `as any` casts with proper SDK types) and #72 (AgentManager DI) refine the interface contracts.
|
|
56
|
+
Codifying “use `unknown` + boundary cast” as a general pattern would normalize something that should feel uncomfortable and motivate further interface refinement.
|
|
57
|
+
|
|
58
|
+
### Changes made
|
|
59
|
+
|
|
60
|
+
1. `.pi/skills/testing/SKILL.md` — added `vi.fn()` return-type annotation rule under “Vitest mock patterns”: annotate factories that return empty arrays or narrow literals to prevent `never[]` inference.
|
package/package.json
CHANGED
package/src/agent-types.ts
CHANGED
|
@@ -48,12 +48,6 @@ export function resolveType(name: string): string | undefined {
|
|
|
48
48
|
return resolveKey(name);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** Get the agent config for a type (case-insensitive). */
|
|
52
|
-
export function getAgentConfig(name: string): AgentConfig | undefined {
|
|
53
|
-
const key = resolveKey(name);
|
|
54
|
-
return key ? agents.get(key) : undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
51
|
/** Get all enabled type names (for spawning and tool descriptions). */
|
|
58
52
|
export function getAvailableTypes(): string[] {
|
|
59
53
|
return [...agents.entries()]
|
|
@@ -116,49 +110,29 @@ export function getToolNamesForType(type: string): string[] {
|
|
|
116
110
|
return names;
|
|
117
111
|
}
|
|
118
112
|
|
|
119
|
-
/**
|
|
120
|
-
export function
|
|
121
|
-
displayName: string;
|
|
122
|
-
description: string;
|
|
123
|
-
builtinToolNames: string[];
|
|
124
|
-
extensions: true | string[] | false;
|
|
125
|
-
skills: true | string[] | false;
|
|
126
|
-
promptMode: "replace" | "append";
|
|
127
|
-
} {
|
|
113
|
+
/** Resolve agent config with guaranteed non-null return. Falls back: unknown → general-purpose → absolute fallback. */
|
|
114
|
+
export function resolveAgentConfig(type: string): AgentConfig {
|
|
128
115
|
const key = resolveKey(type);
|
|
129
116
|
const config = key ? agents.get(key) : undefined;
|
|
130
|
-
if (config
|
|
131
|
-
return
|
|
132
|
-
displayName: config.displayName ?? config.name,
|
|
133
|
-
description: config.description,
|
|
134
|
-
builtinToolNames: config.builtinToolNames ?? BUILTIN_TOOL_NAMES,
|
|
135
|
-
extensions: config.extensions,
|
|
136
|
-
skills: config.skills,
|
|
137
|
-
promptMode: config.promptMode,
|
|
138
|
-
};
|
|
117
|
+
if (config) {
|
|
118
|
+
return config;
|
|
139
119
|
}
|
|
140
120
|
|
|
141
|
-
// Fallback for unknown
|
|
121
|
+
// Fallback to general-purpose for unknown types
|
|
142
122
|
const gp = agents.get("general-purpose");
|
|
143
|
-
if (gp
|
|
144
|
-
return
|
|
145
|
-
displayName: gp.displayName ?? gp.name,
|
|
146
|
-
description: gp.description,
|
|
147
|
-
builtinToolNames: gp.builtinToolNames ?? BUILTIN_TOOL_NAMES,
|
|
148
|
-
extensions: gp.extensions,
|
|
149
|
-
skills: gp.skills,
|
|
150
|
-
promptMode: gp.promptMode,
|
|
151
|
-
};
|
|
123
|
+
if (gp) {
|
|
124
|
+
return gp;
|
|
152
125
|
}
|
|
153
126
|
|
|
154
|
-
// Absolute fallback (should never happen)
|
|
127
|
+
// Absolute fallback (should never happen in practice)
|
|
155
128
|
return {
|
|
129
|
+
name: type,
|
|
156
130
|
displayName: "Agent",
|
|
157
131
|
description: "General-purpose agent for complex, multi-step tasks",
|
|
158
132
|
builtinToolNames: BUILTIN_TOOL_NAMES,
|
|
159
133
|
extensions: true,
|
|
160
134
|
skills: true,
|
|
135
|
+
systemPrompt: "",
|
|
161
136
|
promptMode: "append",
|
|
162
137
|
};
|
|
163
138
|
}
|
|
164
|
-
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { join } from "node:path";
|
|
|
14
14
|
import { defineTool, type ExtensionAPI, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { AgentManager } from "./agent-manager.js";
|
|
16
16
|
import { getAgentConversation, normalizeMaxTurns, steerAgent } from "./agent-runner.js";
|
|
17
|
-
import {
|
|
17
|
+
import { getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, resolveAgentConfig, } from "./agent-types.js";
|
|
18
18
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
19
19
|
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
20
20
|
import { buildEventData, createNotificationSystem } from "./notification.js";
|
|
@@ -149,14 +149,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
149
149
|
const userNames = getUserAgentNames();
|
|
150
150
|
|
|
151
151
|
const defaultDescs = defaultNames.map((name) => {
|
|
152
|
-
const cfg =
|
|
153
|
-
const modelSuffix = cfg
|
|
154
|
-
return `- ${name}: ${cfg
|
|
152
|
+
const cfg = resolveAgentConfig(name);
|
|
153
|
+
const modelSuffix = cfg.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
|
|
154
|
+
return `- ${name}: ${cfg.description}${modelSuffix}`;
|
|
155
155
|
});
|
|
156
156
|
|
|
157
157
|
const customDescs = userNames.map((name) => {
|
|
158
|
-
const cfg =
|
|
159
|
-
return `- ${name}: ${cfg
|
|
158
|
+
const cfg = resolveAgentConfig(name);
|
|
159
|
+
return `- ${name}: ${cfg.description}`;
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
return [
|
|
@@ -237,8 +237,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
237
237
|
reloadCustomAgents,
|
|
238
238
|
agentActivity: runtime.agentActivity,
|
|
239
239
|
getModelLabel: (type, registry) => {
|
|
240
|
-
const cfg =
|
|
241
|
-
if (!cfg
|
|
240
|
+
const cfg = resolveAgentConfig(type);
|
|
241
|
+
if (!cfg.model) return 'inherit';
|
|
242
242
|
if (registry) {
|
|
243
243
|
const resolved = resolveModel(cfg.model, registry as any);
|
|
244
244
|
if (typeof resolved === 'string') return 'inherit';
|
package/src/session-config.ts
CHANGED
|
@@ -11,13 +11,11 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
|
-
getAgentConfig,
|
|
15
|
-
getConfig,
|
|
16
14
|
getMemoryToolNames,
|
|
17
15
|
getReadOnlyMemoryToolNames,
|
|
18
16
|
getToolNamesForType,
|
|
17
|
+
resolveAgentConfig,
|
|
19
18
|
} from "./agent-types.js";
|
|
20
|
-
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
21
19
|
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
22
20
|
import { buildAgentPrompt, type PromptExtras } from "./prompts.js";
|
|
23
21
|
import { preloadSkills } from "./skill-loader.js";
|
|
@@ -149,14 +147,13 @@ export function assembleSessionConfig(
|
|
|
149
147
|
options: AssemblerOptions,
|
|
150
148
|
env: EnvInfo,
|
|
151
149
|
): SessionConfig {
|
|
152
|
-
const
|
|
153
|
-
const agentConfig = getAgentConfig(type);
|
|
150
|
+
const agentConfig = resolveAgentConfig(type);
|
|
154
151
|
|
|
155
152
|
const effectiveCwd = options.cwd ?? ctx.cwd;
|
|
156
153
|
|
|
157
154
|
// Resolve extensions/skills: isolated overrides to false
|
|
158
|
-
const extensions = options.isolated ? false :
|
|
159
|
-
const skills = options.isolated ? false :
|
|
155
|
+
const extensions = options.isolated ? false : agentConfig.extensions;
|
|
156
|
+
const skills = options.isolated ? false : agentConfig.skills;
|
|
160
157
|
|
|
161
158
|
// Build prompt extras (memory, preloaded skills)
|
|
162
159
|
const extras: PromptExtras = {};
|
|
@@ -174,7 +171,7 @@ export function assembleSessionConfig(
|
|
|
174
171
|
// Persistent memory: detect write capability and branch accordingly.
|
|
175
172
|
// Account for disallowedTools — a tool in the base set but on the denylist
|
|
176
173
|
// is not truly available.
|
|
177
|
-
if (agentConfig
|
|
174
|
+
if (agentConfig.memory) {
|
|
178
175
|
const existingNames = new Set(toolNames);
|
|
179
176
|
const denied = agentConfig.disallowedTools
|
|
180
177
|
? new Set(agentConfig.disallowedTools)
|
|
@@ -202,51 +199,34 @@ export function assembleSessionConfig(
|
|
|
202
199
|
}
|
|
203
200
|
}
|
|
204
201
|
|
|
205
|
-
// Build system prompt from agent config
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
extras,
|
|
214
|
-
);
|
|
215
|
-
} else {
|
|
216
|
-
// Unknown type fallback: spread the canonical general-purpose config (defensive —
|
|
217
|
-
// unreachable in practice since index.ts resolves unknown types before calling runAgent).
|
|
218
|
-
const fallback = DEFAULT_AGENTS.get("general-purpose");
|
|
219
|
-
if (!fallback) {
|
|
220
|
-
throw new Error(`No fallback config available for unknown type "${type}"`);
|
|
221
|
-
}
|
|
222
|
-
systemPrompt = buildAgentPrompt(
|
|
223
|
-
{ ...fallback, name: type },
|
|
224
|
-
effectiveCwd,
|
|
225
|
-
env,
|
|
226
|
-
ctx.parentSystemPrompt,
|
|
227
|
-
extras,
|
|
228
|
-
);
|
|
229
|
-
}
|
|
202
|
+
// Build system prompt from the resolved agent config
|
|
203
|
+
const systemPrompt = buildAgentPrompt(
|
|
204
|
+
agentConfig,
|
|
205
|
+
effectiveCwd,
|
|
206
|
+
env,
|
|
207
|
+
ctx.parentSystemPrompt,
|
|
208
|
+
extras,
|
|
209
|
+
);
|
|
230
210
|
|
|
231
211
|
// noSkills: when we've already preloaded skills into the prompt, or skills = false,
|
|
232
212
|
// tell the resource loader not to load them again.
|
|
233
213
|
const noSkills = skills === false || Array.isArray(skills);
|
|
234
214
|
|
|
235
215
|
// Disallowed tools set (for filterActiveTools in runAgent)
|
|
236
|
-
const disallowedSet = agentConfig
|
|
216
|
+
const disallowedSet = agentConfig.disallowedTools
|
|
237
217
|
? new Set(agentConfig.disallowedTools)
|
|
238
218
|
: undefined;
|
|
239
219
|
|
|
240
220
|
// Model resolution: explicit option > config model string > parent model
|
|
241
221
|
const model =
|
|
242
222
|
options.model ??
|
|
243
|
-
resolveDefaultModel(ctx.parentModel, ctx.modelRegistry, agentConfig
|
|
223
|
+
resolveDefaultModel(ctx.parentModel, ctx.modelRegistry, agentConfig.model);
|
|
244
224
|
|
|
245
225
|
// Thinking level: explicit option > agent config > undefined (inherit)
|
|
246
|
-
const thinkingLevel = options.thinkingLevel ?? agentConfig
|
|
226
|
+
const thinkingLevel = options.thinkingLevel ?? agentConfig.thinking;
|
|
247
227
|
|
|
248
228
|
// Per-agent max turns (combined with options.maxTurns and defaultMaxTurns by runAgent)
|
|
249
|
-
const agentMaxTurns = agentConfig
|
|
229
|
+
const agentMaxTurns = agentConfig.maxTurns;
|
|
250
230
|
|
|
251
231
|
return {
|
|
252
232
|
effectiveCwd,
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Text } from "@earendil-works/pi-tui";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
3
|
import { normalizeMaxTurns } from "../agent-runner.js";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveAgentConfig, resolveType } from "../agent-types.js";
|
|
5
5
|
import { resolveAgentInvocationConfig } from "../invocation-config.js";
|
|
6
6
|
import { resolveInvocationModel } from "../model-resolver.js";
|
|
7
7
|
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "../output-file.js";
|
|
@@ -370,8 +370,8 @@ Guidelines:
|
|
|
370
370
|
|
|
371
371
|
const displayName = getDisplayName(subagentType);
|
|
372
372
|
|
|
373
|
-
// Get agent config
|
|
374
|
-
const customConfig =
|
|
373
|
+
// Get agent config for invocation resolution
|
|
374
|
+
const customConfig = resolveAgentConfig(subagentType);
|
|
375
375
|
|
|
376
376
|
const resolvedConfig = resolveAgentInvocationConfig(customConfig, params);
|
|
377
377
|
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -3,8 +3,9 @@ import { join } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
BUILTIN_TOOL_NAMES,
|
|
6
|
-
getAgentConfig,
|
|
7
6
|
getAllTypes,
|
|
7
|
+
resolveAgentConfig,
|
|
8
|
+
resolveType,
|
|
8
9
|
} from "../agent-types.js";
|
|
9
10
|
import type { AgentConfig, AgentRecord } from "../types.js";
|
|
10
11
|
import type { AgentActivity } from "./agent-widget.js";
|
|
@@ -152,21 +153,21 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
152
153
|
};
|
|
153
154
|
|
|
154
155
|
const entries = allNames.map((name) => {
|
|
155
|
-
const cfg =
|
|
156
|
-
const disabled = cfg
|
|
156
|
+
const cfg = resolveAgentConfig(name);
|
|
157
|
+
const disabled = cfg.enabled === false;
|
|
157
158
|
const model = deps.getModelLabel(name, ctx.modelRegistry);
|
|
158
159
|
const indicator = sourceIndicator(cfg);
|
|
159
160
|
const prefix = `${indicator}${name} · ${model}`;
|
|
160
|
-
const desc = disabled ? "(disabled)" :
|
|
161
|
+
const desc = disabled ? "(disabled)" : cfg.description;
|
|
161
162
|
return { name, prefix, desc };
|
|
162
163
|
});
|
|
163
164
|
const maxPrefix = Math.max(...entries.map((e) => e.prefix.length));
|
|
164
165
|
|
|
165
166
|
const hasCustom = allNames.some((n) => {
|
|
166
|
-
const c =
|
|
167
|
-
return
|
|
167
|
+
const c = resolveAgentConfig(n);
|
|
168
|
+
return !c.isDefault && c.enabled !== false;
|
|
168
169
|
});
|
|
169
|
-
const hasDisabled = allNames.some((n) =>
|
|
170
|
+
const hasDisabled = allNames.some((n) => resolveAgentConfig(n).enabled === false);
|
|
170
171
|
const legendParts: string[] = [];
|
|
171
172
|
if (hasCustom) legendParts.push("• = project ◦ = global");
|
|
172
173
|
if (hasDisabled) legendParts.push("✕ = disabled");
|
|
@@ -184,7 +185,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
184
185
|
.split(" · ")[0]
|
|
185
186
|
.replace(/^[•◦✕\s]+/, "")
|
|
186
187
|
.trim();
|
|
187
|
-
if (
|
|
188
|
+
if (resolveType(agentName) != null) {
|
|
188
189
|
await showAgentDetail(ctx, agentName);
|
|
189
190
|
await showAllAgentsList(ctx);
|
|
190
191
|
}
|
|
@@ -245,11 +246,11 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
245
246
|
}
|
|
246
247
|
|
|
247
248
|
async function showAgentDetail(ctx: MenuContext, name: string) {
|
|
248
|
-
|
|
249
|
-
if (!cfg) {
|
|
249
|
+
if (resolveType(name) == null) {
|
|
250
250
|
ctx.ui.notify(`Agent config not found for "${name}".`, "warning");
|
|
251
251
|
return;
|
|
252
252
|
}
|
|
253
|
+
const cfg = resolveAgentConfig(name);
|
|
253
254
|
|
|
254
255
|
const file = findAgentFile(name);
|
|
255
256
|
const isDefault = cfg.isDefault === true;
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import type { AgentManager } from "../agent-manager.js";
|
|
10
|
-
import {
|
|
10
|
+
import { resolveAgentConfig } from "../agent-types.js";
|
|
11
11
|
import type { AgentInvocation, SubagentType } from "../types.js";
|
|
12
12
|
import { getLifetimeTotal, getSessionContextPercent, type LifetimeUsage, type SessionLike } from "../usage.js";
|
|
13
13
|
|
|
@@ -144,12 +144,13 @@ export function formatDuration(startedAt: number, completedAt?: number): string
|
|
|
144
144
|
|
|
145
145
|
/** Get display name for any agent type (built-in or custom). */
|
|
146
146
|
export function getDisplayName(type: SubagentType): string {
|
|
147
|
-
|
|
147
|
+
const config = resolveAgentConfig(type);
|
|
148
|
+
return config.displayName ?? config.name;
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
/** Short label for prompt mode: "twin" for append, nothing for replace (the default). */
|
|
151
152
|
export function getPromptModeLabel(type: SubagentType): string | undefined {
|
|
152
|
-
const config =
|
|
153
|
+
const config = resolveAgentConfig(type);
|
|
153
154
|
return config.promptMode === "append" ? "twin" : undefined;
|
|
154
155
|
}
|
|
155
156
|
|