@tintinweb/pi-subagents 0.2.6 → 0.3.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,48 @@ 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
+ ## [0.3.0] - 2026-03-08
9
+
10
+ ### Added
11
+ - **Case-insensitive agent type lookup** — `"explore"`, `"EXPLORE"`, and `"Explore"` all resolve to the same agent. LLMs frequently lowercase type names; this prevents validation failures.
12
+ - **Unknown type fallback** — unrecognized agent types fall back to `general-purpose` with a note, instead of hard-rejecting. Matches Claude Code behavior.
13
+ - **Dynamic tool list for general-purpose** — `builtinToolNames` is now optional in `AgentConfig`. When omitted, the agent gets all tools from `TOOL_FACTORIES` at lookup time, so new tools added upstream are automatically available.
14
+ - **Agent source indicators in `/agents` menu** — `•` (project), `◦` (global), `✕` (disabled) with legend. Defaults are unmarked.
15
+ - **Disabled agents visible in UI** — disabled agents now show in the "Agent types" list (marked `✕`) with an Enable action, instead of being invisible.
16
+ - **Enable action** — re-enable a disabled agent from the `/agents` menu. Stub files are auto-cleaned.
17
+ - **Disable action for all agent types** — custom and ejected default agents can now be disabled from the UI, not just built-in defaults.
18
+ - `resolveType()` export — case-insensitive type name resolution for external use.
19
+ - `getAllTypes()` export — returns all agent names including disabled (for UI listing).
20
+ - `source` field on `AgentConfig` — tracks where an agent was loaded from (`"default"`, `"project"`, `"global"`).
21
+
22
+ ### Fixed
23
+ - **Model resolver checks auth for exact matches** — `resolveModel("anthropic/claude-haiku-4-5-20251001")` now fails gracefully when no Anthropic API key is configured, instead of returning a model that errors at the API call. Explore silently falls back to the parent model on non-Anthropic setups.
24
+
25
+ ### Changed
26
+ - **Unified agent registry** — built-in and custom agents now use the same `AgentConfig` type and a single registry. No more separate code paths for built-in vs custom agents.
27
+ - **Default agents are overridable** — creating a `.md` file with the same name as a default agent (e.g. `.pi/agents/Explore.md`) overrides it.
28
+ - **`/agents` menu** — "Agent types" list shows defaults and custom agents together with source indicators. Default agents get Eject/Disable actions; overridden defaults get Reset to default.
29
+ - **Eject action** — export a default agent's embedded config as a `.md` file to project or personal location for customization.
30
+ - **Model labels** — provider-agnostic: strips `provider/` prefix and `-YYYYMMDD` date suffix (e.g. `anthropic/claude-haiku-4-5-20251001` → `claude-haiku-4-5`). Works for any provider.
31
+ - **New frontmatter fields** — `display_name` (UI display name) and `enabled` (default: true; set to false to disable).
32
+ - **Menu navigation** — Esc in agent detail returns to agent list (not main menu).
33
+
34
+ ### Removed
35
+ - **`statusline-setup` and `claude-code-guide` agents** — removed as built-in types (never spawned programmatically). Users can recreate them as custom agents if needed.
36
+ - `BuiltinSubagentType` union type, `SUBAGENT_TYPES` array, `DISPLAY_NAMES` map, `SubagentTypeConfig` interface — replaced by unified `AgentConfig`.
37
+ - `buildSystemPrompt()` switch statement — replaced by config-driven `buildAgentPrompt()`.
38
+ - `HAIKU_MODEL_IDS` fallback array — Explore's haiku default is now just the `model` field in its config.
39
+ - `BUILTIN_MODEL_LABELS` — model labels now derived from config.
40
+ - `ALL_TOOLS` hardcoded constant — general-purpose now derives tools dynamically.
41
+
42
+ ### Added
43
+ - `src/default-agents.ts` — embedded default configs for general-purpose, Explore, and Plan.
44
+
45
+ ## [0.2.7] - 2026-03-08
46
+
47
+ ### Fixed
48
+ - **Widget crash in narrow terminals** — agent widget lines were not truncated to terminal width, causing `doRender` to throw when the tmux pane was narrower than the rendered content. All widget lines are now truncated using `truncateToWidth()` with the actual terminal column count.
49
+
8
50
  ## [0.2.6] - 2026-03-07
9
51
 
10
52
  ### Added
@@ -125,6 +167,8 @@ Initial release.
125
167
  - **Thinking level** — per-agent extended thinking control
126
168
  - **`/agent` and `/agents` commands**
127
169
 
170
+ [0.3.0]: https://github.com/tintinweb/pi-subagents/compare/v0.2.7...v0.3.0
171
+ [0.2.7]: https://github.com/tintinweb/pi-subagents/compare/v0.2.6...v0.2.7
128
172
  [0.2.6]: https://github.com/tintinweb/pi-subagents/compare/v0.2.5...v0.2.6
129
173
  [0.2.5]: https://github.com/tintinweb/pi-subagents/compare/v0.2.4...v0.2.5
130
174
  [0.2.4]: https://github.com/tintinweb/pi-subagents/compare/v0.2.3...v0.2.4
package/README.md CHANGED
@@ -19,6 +19,7 @@ https://github.com/user-attachments/assets/5d1331e8-6d02-420b-b30a-dcbf838b1660
19
19
  - **Mid-run steering** — inject messages into running agents to redirect their work without restarting
20
20
  - **Session resume** — pick up where an agent left off, preserving full conversation context
21
21
  - **Graceful turn limits** — agents get a "wrap up" warning before hard abort, producing clean partial results instead of cut-off output
22
+ - **Case-insensitive agent types** — `"explore"`, `"Explore"`, `"EXPLORE"` all work. Unknown types fall back to general-purpose with a note
22
23
  - **Fuzzy model selection** — specify models by name (`"haiku"`, `"sonnet"`) instead of full IDs, with automatic filtering to only available/configured models
23
24
  - **Context inheritance** — optionally fork the parent conversation into a sub-agent so it knows what's been discussed
24
25
 
@@ -75,21 +76,21 @@ Individual agent results render Claude Code-style in the conversation:
75
76
 
76
77
  Completed results can be expanded (ctrl+o in pi) to show the full agent output inline.
77
78
 
78
- ## Built-in Agent Types
79
+ ## Default Agent Types
79
80
 
80
- | Type | Tools | Description |
81
- |------|-------|-------------|
82
- | `general-purpose` | all 7 | Full read/write access for complex multi-step tasks |
83
- | `Explore` | read, bash, grep, find, ls | Fast codebase exploration (read-only, defaults to haiku) |
84
- | `Plan` | read, bash, grep, find, ls | Software architect for implementation planning (read-only) |
85
- | `statusline-setup` | read, edit | Configuration editor |
86
- | `claude-code-guide` | read, grep, find | Documentation and help queries |
81
+ | Type | Tools | Model | Description |
82
+ |------|-------|-------|-------------|
83
+ | `general-purpose` | all 7 | inherit | Full read/write access for complex multi-step tasks |
84
+ | `Explore` | read, bash, grep, find, ls | haiku (falls back to inherit) | Fast codebase exploration (read-only) |
85
+ | `Plan` | read, bash, grep, find, ls | inherit | Software architect for implementation planning (read-only) |
86
+
87
+ Default agents can be **overridden** by creating a `.md` file with the same name (e.g. `.pi/agents/Explore.md`), or **disabled** per-project by creating a `.md` file with `enabled: false` frontmatter.
87
88
 
88
89
  ## Custom Agents
89
90
 
90
- Define custom agent types by creating `.md` files. The filename becomes the agent type name.
91
+ Define custom agent types by creating `.md` files. The filename becomes the agent type name. Any name is allowed — using a default agent's name overrides it.
91
92
 
92
- Custom agents are discovered from two locations (higher priority wins):
93
+ Agents are discovered from two locations (higher priority wins):
93
94
 
94
95
  | Priority | Location | Scope |
95
96
  |----------|----------|-------|
@@ -131,16 +132,18 @@ All fields are optional — sensible defaults for everything.
131
132
  | Field | Default | Description |
132
133
  |-------|---------|-------------|
133
134
  | `description` | filename | Agent description shown in tool listings |
135
+ | `display_name` | — | Display name for UI (e.g. widget, agent list) |
134
136
  | `tools` | all 7 | Comma-separated built-in tools: read, bash, edit, write, grep, find, ls. `none` for no tools |
135
137
  | `extensions` | `true` | Inherit MCP/extension tools. `false` to disable |
136
138
  | `skills` | `true` | Inherit skills from parent |
137
- | `model` | inherit parent | Model as `provider/modelId` |
139
+ | `model` | inherit parent | Model `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`) |
138
140
  | `thinking` | inherit | off, minimal, low, medium, high, xhigh |
139
141
  | `max_turns` | 50 | Max agentic turns before graceful shutdown |
140
142
  | `prompt_mode` | `replace` | `replace`: body is the full system prompt. `append`: body appended to default prompt |
141
143
  | `inherit_context` | `false` | Fork parent conversation into agent |
142
144
  | `run_in_background` | `false` | Run in background by default |
143
145
  | `isolated` | `false` | No extension/MCP tools, only built-in |
146
+ | `enabled` | `true` | Set to `false` to disable an agent (useful for hiding a default agent per-project) |
144
147
 
145
148
  Frontmatter sets defaults. Explicit `Agent` parameters always override them.
146
149
 
@@ -193,19 +196,19 @@ The `/agents` command opens an interactive menu:
193
196
 
194
197
  ```
195
198
  Running agents (2) — 1 running, 1 done ← only shown when agents exist
196
- Custom agents (3) submenu: edit or delete agents
199
+ Agent types (6) unified list: defaults + custom
197
200
  Create new agent ← manual wizard or AI-generated
198
201
  Settings ← max concurrency, max turns, grace turns, join mode
199
-
200
- Built-in (always available):
201
- general-purpose · inherit
202
- Explore · haiku
203
- Plan · inherit
204
- ...
205
202
  ```
206
203
 
207
- - **Custom agents submenu** — select an agent to edit (opens editor) or delete
208
- - **Create new agent** — choose project/personal location, then manual wizard (step-by-step prompts for name, tools, model, thinking, system prompt) or AI-generated (describe what the agent should do and a sub-agent writes the `.md` file)
204
+ - **Agent types** — unified list with source indicators: `•` (project), `◦` (global), `✕` (disabled). Select an agent to manage it:
205
+ - **Default agents** (no override): Eject (export as `.md`), Disable
206
+ - **Default agents** (ejected/overridden): Edit, Disable, Reset to default, Delete
207
+ - **Custom agents**: Edit, Disable, Delete
208
+ - **Disabled agents**: Enable, Edit, Delete
209
+ - **Eject** — writes the embedded default config as a `.md` file to project or personal location, so you can customize it
210
+ - **Disable/Enable** — toggle agent availability. Disabled agents stay visible in the list (marked `✕`) and can be re-enabled
211
+ - **Create new agent** — choose project/personal location, then manual wizard (step-by-step prompts for name, tools, model, thinking, system prompt) or AI-generated (describe what the agent should do and a sub-agent writes the `.md` file). Any name is allowed, including default agent names (overrides them)
209
212
  - **Settings** — configure max concurrency, default max turns, grace turns, and join mode at runtime
210
213
 
211
214
  ## Graceful Max Turns
@@ -250,13 +253,14 @@ When background agents complete, they notify the main agent. The **join mode** c
250
253
  ```
251
254
  src/
252
255
  index.ts # Extension entry: tool/command registration, rendering
253
- types.ts # Type definitions (SubagentType, AgentRecord, configs)
254
- agent-types.ts # Agent type registry (built-in + custom), tool factories
256
+ types.ts # Type definitions (AgentConfig, AgentRecord, etc.)
257
+ default-agents.ts # Embedded default agent configs (general-purpose, Explore, Plan)
258
+ agent-types.ts # Unified agent registry (defaults + user), tool factories
255
259
  agent-runner.ts # Session creation, execution, graceful max_turns, steer/resume
256
260
  agent-manager.ts # Agent lifecycle, concurrency queue, completion notifications
257
261
  group-join.ts # Group join manager: batched completion notifications with timeout
258
- custom-agents.ts # Load custom agents from .pi/agents/*.md
259
- prompts.ts # System prompts per agent type
262
+ custom-agents.ts # Load user-defined agents from .pi/agents/*.md
263
+ prompts.ts # Config-driven system prompt builder
260
264
  context.ts # Parent conversation context for inherit_context
261
265
  env.ts # Environment detection (git, platform)
262
266
  ui/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tintinweb/pi-subagents",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "A pi extension extension that brings smart Claude Code-style autonomous sub-agents to pi.",
5
5
  "author": "tintinweb",
6
6
  "license": "MIT",
@@ -21,9 +21,9 @@
21
21
  "autonomous"
22
22
  ],
23
23
  "dependencies": {
24
- "@mariozechner/pi-ai": "latest",
25
- "@mariozechner/pi-coding-agent": "latest",
26
- "@mariozechner/pi-tui": "latest",
24
+ "@mariozechner/pi-ai": "^0.57.1",
25
+ "@mariozechner/pi-coding-agent": "^0.57.1",
26
+ "@mariozechner/pi-tui": "^0.57.1",
27
27
  "@sinclair/typebox": "latest"
28
28
  },
29
29
  "scripts": {
@@ -13,8 +13,8 @@ import {
13
13
  } from "@mariozechner/pi-coding-agent";
14
14
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
15
15
  import type { Model } from "@mariozechner/pi-ai";
16
- import { getToolsForType, getConfig, getCustomAgentConfig } from "./agent-types.js";
17
- import { buildSystemPrompt } from "./prompts.js";
16
+ import { getToolsForType, getConfig, getAgentConfig } from "./agent-types.js";
17
+ import { buildAgentPrompt } from "./prompts.js";
18
18
  import { buildParentContext, extractText } from "./context.js";
19
19
  import { detectEnv } from "./env.js";
20
20
  import type { SubagentType, ThinkingLevel } from "./types.js";
@@ -38,47 +38,34 @@ export function getGraceTurns(): number { return graceTurns; }
38
38
  /** Set the grace turns value (minimum 1). */
39
39
  export function setGraceTurns(n: number): void { graceTurns = Math.max(1, n); }
40
40
 
41
- /** Haiku model IDs to try for Explore agents (in preference order). */
42
- const HAIKU_MODEL_IDS = [
43
- "claude-haiku-4-5-20251001",
44
- "claude-3-5-haiku-20241022",
45
- ];
46
-
47
41
  /**
48
42
  * Try to find the right model for an agent type.
49
- * Priority: explicit option > custom agent model > type-specific default > parent model.
43
+ * Priority: explicit option > config.model > parent model.
50
44
  */
51
45
  function resolveDefaultModel(
52
- type: SubagentType,
53
46
  parentModel: Model<any> | undefined,
54
47
  registry: { find(provider: string, modelId: string): Model<any> | undefined; getAvailable?(): Model<any>[] },
55
- customModel?: string,
48
+ configModel?: string,
56
49
  ): Model<any> | undefined {
57
- // Build a set of available model keys for fast lookup
58
- const available = registry.getAvailable?.();
59
- const availableKeys = available
60
- ? new Set(available.map((m: any) => `${m.provider}/${m.id}`))
61
- : undefined;
62
- const isAvailable = (provider: string, modelId: string) =>
63
- !availableKeys || availableKeys.has(`${provider}/${modelId}`);
64
-
65
- // Custom agent model from frontmatter
66
- if (customModel) {
67
- const slashIdx = customModel.indexOf("/");
50
+ if (configModel) {
51
+ const slashIdx = configModel.indexOf("/");
68
52
  if (slashIdx !== -1) {
69
- const provider = customModel.slice(0, slashIdx);
70
- const modelId = customModel.slice(slashIdx + 1);
53
+ const provider = configModel.slice(0, slashIdx);
54
+ const modelId = configModel.slice(slashIdx + 1);
55
+
56
+ // Build a set of available model keys for fast lookup
57
+ const available = registry.getAvailable?.();
58
+ const availableKeys = available
59
+ ? new Set(available.map((m: any) => `${m.provider}/${m.id}`))
60
+ : undefined;
61
+ const isAvailable = (p: string, id: string) =>
62
+ !availableKeys || availableKeys.has(`${p}/${id}`);
63
+
71
64
  const found = registry.find(provider, modelId);
72
65
  if (found && isAvailable(provider, modelId)) return found;
73
66
  }
74
67
  }
75
68
 
76
- if (type !== "Explore") return parentModel;
77
-
78
- for (const modelId of HAIKU_MODEL_IDS) {
79
- const found = registry.find("anthropic", modelId);
80
- if (found && isAvailable("anthropic", modelId)) return found;
81
- }
82
69
  return parentModel;
83
70
  }
84
71
 
@@ -152,17 +139,60 @@ export async function runAgent(
152
139
  options: RunOptions,
153
140
  ): Promise<RunResult> {
154
141
  const config = getConfig(type);
155
- const customConfig = getCustomAgentConfig(type);
142
+ const agentConfig = getAgentConfig(type);
156
143
  const env = await detectEnv(options.pi, ctx.cwd);
157
144
 
158
- // Build system prompt: custom override > custom append > built-in
145
+ // Build system prompt: custom override > custom append > config-driven
159
146
  let systemPrompt: string;
160
147
  if (options.systemPromptOverride) {
161
148
  systemPrompt = options.systemPromptOverride;
162
149
  } else if (options.systemPromptAppend) {
163
- systemPrompt = buildSystemPrompt(type, ctx.cwd, env) + "\n\n" + options.systemPromptAppend;
150
+ // Build a default prompt and append to it
151
+ const defaultConfig = agentConfig ?? {
152
+ name: type,
153
+ description: "",
154
+ builtinToolNames: [],
155
+ extensions: true,
156
+ skills: true,
157
+ systemPrompt: "",
158
+ promptMode: "replace" as const,
159
+ inheritContext: false,
160
+ runInBackground: false,
161
+ isolated: false,
162
+ };
163
+ systemPrompt = buildAgentPrompt(defaultConfig, ctx.cwd, env) + "\n\n" + options.systemPromptAppend;
164
+ } else if (agentConfig) {
165
+ systemPrompt = buildAgentPrompt(agentConfig, ctx.cwd, env);
164
166
  } else {
165
- systemPrompt = buildSystemPrompt(type, ctx.cwd, env);
167
+ // Unknown type use a minimal general-purpose prompt
168
+ systemPrompt = buildAgentPrompt({
169
+ name: type,
170
+ description: "General-purpose agent",
171
+ builtinToolNames: [],
172
+ extensions: true,
173
+ skills: true,
174
+ systemPrompt: `# Role
175
+ You are a general-purpose coding agent for complex, multi-step tasks.
176
+ You have full access to read, write, edit files, and execute commands.
177
+ Do what has been asked; nothing more, nothing less.
178
+
179
+ # Tool Usage
180
+ - Use the read tool instead of cat/head/tail
181
+ - Use the edit tool instead of sed/awk
182
+ - Use the write tool instead of echo/heredoc
183
+ - Use the find tool instead of bash find/ls for file search
184
+ - Use the grep tool instead of bash grep/rg for content search
185
+ - Make independent tool calls in parallel
186
+
187
+ # Output
188
+ - Use absolute file paths
189
+ - Do not use emojis
190
+ - Be concise but complete`,
191
+ promptMode: "replace",
192
+ inheritContext: false,
193
+ runInBackground: false,
194
+ isolated: false,
195
+ }, ctx.cwd, env);
166
196
  }
167
197
 
168
198
  const tools = getToolsForType(type, ctx.cwd);
@@ -182,13 +212,13 @@ export async function runAgent(
182
212
  });
183
213
  await loader.reload();
184
214
 
185
- // Resolve model: explicit option > custom agent config > type-specific default > parent model
215
+ // Resolve model: explicit option > config.model > parent model
186
216
  const model = options.model ?? resolveDefaultModel(
187
- type, ctx.model, ctx.modelRegistry, customConfig?.model,
217
+ ctx.model, ctx.modelRegistry, agentConfig?.model,
188
218
  );
189
219
 
190
- // Resolve thinking level: explicit option > custom agent config > undefined (inherit)
191
- const thinkingLevel = options.thinkingLevel ?? customConfig?.thinking;
220
+ // Resolve thinking level: explicit option > agent config > undefined (inherit)
221
+ const thinkingLevel = options.thinkingLevel ?? agentConfig?.thinking;
192
222
 
193
223
  const sessionOpts: Record<string, unknown> = {
194
224
  cwd: ctx.cwd,
@@ -225,7 +255,7 @@ export async function runAgent(
225
255
 
226
256
  // Track turns for graceful max_turns enforcement
227
257
  let turnCount = 0;
228
- const maxTurns = options.maxTurns ?? customConfig?.maxTurns ?? defaultMaxTurns;
258
+ const maxTurns = options.maxTurns ?? agentConfig?.maxTurns ?? defaultMaxTurns;
229
259
  let softLimitReached = false;
230
260
  let aborted = false;
231
261
 
@@ -1,7 +1,8 @@
1
1
  /**
2
- * agent-types.ts — Agent type registry: tool sets and configs per subagent type.
2
+ * agent-types.ts — Unified agent type registry.
3
3
  *
4
- * Supports both built-in types and custom agents loaded from .pi/agents/*.md.
4
+ * Merges embedded default agents with user-defined agents from .pi/agents/*.md.
5
+ * User agents override defaults with the same name. Disabled agents are kept but excluded from spawning.
5
6
  */
6
7
 
7
8
  import {
@@ -14,7 +15,8 @@ import {
14
15
  createLsTool,
15
16
  } from "@mariozechner/pi-coding-agent";
16
17
  import type { AgentTool } from "@mariozechner/pi-agent-core";
17
- import type { BuiltinSubagentType, SubagentType, SubagentTypeConfig, CustomAgentConfig } from "./types.js";
18
+ import type { AgentConfig } from "./types.js";
19
+ import { DEFAULT_AGENTS } from "./default-agents.js";
18
20
 
19
21
  type ToolFactory = (cwd: string) => AgentTool<any>;
20
22
 
@@ -31,107 +33,145 @@ const TOOL_FACTORIES: Record<string, ToolFactory> = {
31
33
  /** All known built-in tool names, derived from the factory registry. */
32
34
  export const BUILTIN_TOOL_NAMES = Object.keys(TOOL_FACTORIES);
33
35
 
34
- const BUILTIN_CONFIGS: Record<BuiltinSubagentType, SubagentTypeConfig> = {
35
- "general-purpose": {
36
- displayName: "Agent",
37
- description: "General-purpose agent for complex, multi-step tasks",
38
- builtinToolNames: BUILTIN_TOOL_NAMES,
39
- extensions: true,
40
- skills: true,
41
- },
42
- "Explore": {
43
- displayName: "Explore",
44
- description: "Fast codebase exploration agent (read-only)",
45
- builtinToolNames: ["read", "bash", "grep", "find", "ls"],
46
- extensions: true,
47
- skills: true,
48
- },
49
- "Plan": {
50
- displayName: "Plan",
51
- description: "Software architect for implementation planning (read-only)",
52
- builtinToolNames: ["read", "bash", "grep", "find", "ls"],
53
- extensions: true,
54
- skills: true,
55
- },
56
- "statusline-setup": {
57
- displayName: "Config",
58
- description: "Configuration editor (read + edit only)",
59
- builtinToolNames: ["read", "edit"],
60
- extensions: false,
61
- skills: false,
62
- },
63
- "claude-code-guide": {
64
- displayName: "Guide",
65
- description: "Documentation and help queries",
66
- builtinToolNames: ["read", "grep", "find"],
67
- extensions: false,
68
- skills: false,
69
- },
70
- };
36
+ /** Unified runtime registry of all agents (defaults + user-defined). */
37
+ const agents = new Map<string, AgentConfig>();
71
38
 
72
- /** Runtime registry of custom agent configs. */
73
- const customAgents = new Map<string, CustomAgentConfig>();
39
+ /**
40
+ * Register agents into the unified registry.
41
+ * Starts with DEFAULT_AGENTS, then overlays user agents (overrides defaults with same name).
42
+ * Disabled agents (enabled === false) are kept in the registry but excluded from spawning.
43
+ */
44
+ export function registerAgents(userAgents: Map<string, AgentConfig>): void {
45
+ agents.clear();
46
+
47
+ // Start with defaults
48
+ for (const [name, config] of DEFAULT_AGENTS) {
49
+ agents.set(name, config);
50
+ }
51
+
52
+ // Overlay user agents (overrides defaults with same name)
53
+ for (const [name, config] of userAgents) {
54
+ agents.set(name, config);
55
+ }
56
+ }
74
57
 
75
- /** Register custom agents into the runtime registry. */
76
- export function registerCustomAgents(agents: Map<string, CustomAgentConfig>): void {
77
- customAgents.clear();
78
- for (const [name, config] of agents) {
79
- customAgents.set(name, config);
58
+ /** Case-insensitive key resolution. */
59
+ function resolveKey(name: string): string | undefined {
60
+ if (agents.has(name)) return name;
61
+ const lower = name.toLowerCase();
62
+ for (const key of agents.keys()) {
63
+ if (key.toLowerCase() === lower) return key;
80
64
  }
65
+ return undefined;
66
+ }
67
+
68
+ /** Resolve a type name case-insensitively. Returns the canonical key or undefined. */
69
+ export function resolveType(name: string): string | undefined {
70
+ return resolveKey(name);
81
71
  }
82
72
 
83
- /** Get the custom agent config if it exists. */
84
- export function getCustomAgentConfig(name: string): CustomAgentConfig | undefined {
85
- return customAgents.get(name);
73
+ /** Get the agent config for a type (case-insensitive). */
74
+ export function getAgentConfig(name: string): AgentConfig | undefined {
75
+ const key = resolveKey(name);
76
+ return key ? agents.get(key) : undefined;
86
77
  }
87
78
 
88
- /** Get all available type names (built-in + custom). */
79
+ /** Get all enabled type names (for spawning and tool descriptions). */
89
80
  export function getAvailableTypes(): string[] {
90
- return [...Object.keys(BUILTIN_CONFIGS), ...customAgents.keys()];
81
+ return [...agents.entries()]
82
+ .filter(([_, config]) => config.enabled !== false)
83
+ .map(([name]) => name);
91
84
  }
92
85
 
93
- /** Get all custom agent names. */
94
- export function getCustomAgentNames(): string[] {
95
- return [...customAgents.keys()];
86
+ /** Get all type names including disabled (for UI listing). */
87
+ export function getAllTypes(): string[] {
88
+ return [...agents.keys()];
96
89
  }
97
90
 
98
- /** Check if a type is valid (built-in or custom). */
91
+ /** Get names of default agents currently in the registry. */
92
+ export function getDefaultAgentNames(): string[] {
93
+ return [...agents.entries()]
94
+ .filter(([_, config]) => config.isDefault === true)
95
+ .map(([name]) => name);
96
+ }
97
+
98
+ /** Get names of user-defined agents (non-defaults) currently in the registry. */
99
+ export function getUserAgentNames(): string[] {
100
+ return [...agents.entries()]
101
+ .filter(([_, config]) => config.isDefault !== true)
102
+ .map(([name]) => name);
103
+ }
104
+
105
+ /** Check if a type is valid and enabled (case-insensitive). */
99
106
  export function isValidType(type: string): boolean {
100
- return type in BUILTIN_CONFIGS || customAgents.has(type);
107
+ const key = resolveKey(type);
108
+ if (!key) return false;
109
+ return agents.get(key)?.enabled !== false;
101
110
  }
102
111
 
103
- /** Get built-in tools for a type. Works for both built-in and custom agents. */
104
- export function getToolsForType(type: SubagentType, cwd: string): AgentTool<any>[] {
105
- const config = BUILTIN_CONFIGS[type as BuiltinSubagentType];
106
- if (config) {
107
- return config.builtinToolNames.map((n) => TOOL_FACTORIES[n](cwd));
108
- }
109
- const custom = customAgents.get(type);
110
- if (custom) {
111
- return custom.builtinToolNames
112
- .filter((n) => n in TOOL_FACTORIES)
113
- .map((n) => TOOL_FACTORIES[n](cwd));
114
- }
115
- // Fallback: all tools
116
- return BUILTIN_TOOL_NAMES.map((n) => TOOL_FACTORIES[n](cwd));
112
+ /** Get built-in tools for a type (case-insensitive). */
113
+ export function getToolsForType(type: string, cwd: string): AgentTool<any>[] {
114
+ const key = resolveKey(type);
115
+ const raw = key ? agents.get(key) : undefined;
116
+ const config = raw?.enabled !== false ? raw : undefined;
117
+ const toolNames = config?.builtinToolNames?.length ? config.builtinToolNames : BUILTIN_TOOL_NAMES;
118
+ return toolNames.filter((n) => n in TOOL_FACTORIES).map((n) => TOOL_FACTORIES[n](cwd));
117
119
  }
118
120
 
119
- /** Get config for a type. Works for both built-in and custom agents. */
120
- export function getConfig(type: SubagentType): SubagentTypeConfig {
121
- const builtin = BUILTIN_CONFIGS[type as BuiltinSubagentType];
122
- if (builtin) return builtin;
121
+ /** Get config for a type (case-insensitive, returns a SubagentTypeConfig-compatible object). Falls back to general-purpose. */
122
+ export function getConfig(type: string): {
123
+ displayName: string;
124
+ description: string;
125
+ builtinToolNames: string[];
126
+ extensions: true | string[] | false;
127
+ skills: true | string[] | false;
128
+ } {
129
+ const key = resolveKey(type);
130
+ const config = key ? agents.get(key) : undefined;
131
+ if (config && config.enabled !== false) {
132
+ return {
133
+ displayName: config.displayName ?? config.name,
134
+ description: config.description,
135
+ builtinToolNames: config.builtinToolNames ?? BUILTIN_TOOL_NAMES,
136
+ extensions: config.extensions,
137
+ skills: config.skills,
138
+ };
139
+ }
123
140
 
124
- const custom = customAgents.get(type);
125
- if (custom) {
141
+ // Fallback for unknown/disabled types — general-purpose config
142
+ const gp = agents.get("general-purpose");
143
+ if (gp && gp.enabled !== false) {
126
144
  return {
127
- displayName: custom.name,
128
- description: custom.description,
129
- builtinToolNames: custom.builtinToolNames,
130
- extensions: custom.extensions,
131
- skills: custom.skills,
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,
132
150
  };
133
151
  }
134
152
 
135
- // Fallback for unknown types — general-purpose config
136
- return BUILTIN_CONFIGS["general-purpose"];
153
+ // Absolute fallback (should never happen)
154
+ return {
155
+ displayName: "Agent",
156
+ description: "General-purpose agent for complex, multi-step tasks",
157
+ builtinToolNames: BUILTIN_TOOL_NAMES,
158
+ extensions: true,
159
+ skills: true,
160
+ };
161
+ }
162
+
163
+ // ---- Backwards-compatible aliases ----
164
+
165
+ /** @deprecated Use registerAgents instead */
166
+ export const registerCustomAgents = registerAgents;
167
+
168
+ /** @deprecated Use getAgentConfig instead */
169
+ export function getCustomAgentConfig(name: string): AgentConfig | undefined {
170
+ const key = resolveKey(name);
171
+ return key ? agents.get(key) : undefined;
172
+ }
173
+
174
+ /** @deprecated Use getUserAgentNames instead */
175
+ export function getCustomAgentNames(): string[] {
176
+ return getUserAgentNames();
137
177
  }