@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "5.3.0",
3
+ "version": "5.4.0",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },
@@ -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
- /** Get config for a type (case-insensitive, returns a SubagentTypeConfig-compatible object). Falls back to general-purpose. */
120
- export function getConfig(type: string): {
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 && config.enabled !== false) {
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/disabled types — general-purpose config
121
+ // Fallback to general-purpose for unknown types
142
122
  const gp = agents.get("general-purpose");
143
- if (gp && gp.enabled !== false) {
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 { getAgentConfig, getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, } from "./agent-types.js";
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 = getAgentConfig(name);
153
- const modelSuffix = cfg?.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
154
- return `- ${name}: ${cfg?.description ?? name}${modelSuffix}`;
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 = getAgentConfig(name);
159
- return `- ${name}: ${cfg?.description ?? name}`;
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 = getAgentConfig(type);
241
- if (!cfg?.model) return 'inherit';
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';
@@ -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 config = getConfig(type);
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 : config.extensions;
159
- const skills = options.isolated ? false : config.skills;
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?.memory) {
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 (or general-purpose fallback for unknown types)
206
- let systemPrompt: string;
207
- if (agentConfig) {
208
- systemPrompt = buildAgentPrompt(
209
- agentConfig,
210
- effectiveCwd,
211
- env,
212
- ctx.parentSystemPrompt,
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?.disallowedTools
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?.model);
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?.thinking;
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?.maxTurns;
229
+ const agentMaxTurns = agentConfig.maxTurns;
250
230
 
251
231
  return {
252
232
  effectiveCwd,
@@ -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 { getAgentConfig, resolveType } from "../agent-types.js";
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 (if any)
374
- const customConfig = getAgentConfig(subagentType);
373
+ // Get agent config for invocation resolution
374
+ const customConfig = resolveAgentConfig(subagentType);
375
375
 
376
376
  const resolvedConfig = resolveAgentInvocationConfig(customConfig, params);
377
377
 
@@ -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 = getAgentConfig(name);
156
- const disabled = cfg?.enabled === false;
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)" : (cfg?.description ?? name);
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 = getAgentConfig(n);
167
- return c && !c.isDefault && c.enabled !== false;
167
+ const c = resolveAgentConfig(n);
168
+ return !c.isDefault && c.enabled !== false;
168
169
  });
169
- const hasDisabled = allNames.some((n) => getAgentConfig(n)?.enabled === false);
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 (getAgentConfig(agentName)) {
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
- const cfg = getAgentConfig(name);
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;
@@ -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 { getConfig } from "../agent-types.js";
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
- return getConfig(type).displayName;
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 = getConfig(type);
153
+ const config = resolveAgentConfig(type);
153
154
  return config.promptMode === "append" ? "twin" : undefined;
154
155
  }
155
156