/** * agent-types.ts — Unified agent type registry. * * Merges embedded default agents with user-defined agents from .pi/agents/*.md. * User agents override defaults with the same name. Disabled agents are kept but excluded from spawning. */ import { DEFAULT_AGENTS } from "#src/config/default-agents"; import type { AgentConfig } from "#src/types"; // ── AgentConfigLookup interface ────────────────────────────────────────────── /** * Narrow registry interface for consumers that only need config resolution. * Prefer this over the full `AgentTypeRegistry` in function signatures (ISP). */ export interface AgentConfigLookup { resolveAgentConfig(type: string): AgentConfig; getToolNamesForType(type: string): string[]; } // ── AgentTypeRegistry class ────────────────────────────────────────────────── /** * Injectable registry of all agent configurations (defaults + user-defined). * * Replaces the module-scoped `agents` Map and its companion free functions. * The constructor accepts a `loadUserAgents` callback to defer disk I/O to the * call site, keeping this class side-effect-free and easy to test. */ export class AgentTypeRegistry implements AgentConfigLookup { private agents = new Map(); /** The two embedded default agent names. */ static readonly DEFAULT_AGENT_NAMES = ["general-purpose", "Explore"] as const; constructor(private loadUserAgents: () => Map) { this.reload(); } /** * Re-scan user agents and rebuild the registry. * Starts with DEFAULT_AGENTS, then overlays whatever `loadUserAgents()` returns. */ reload(): void { this.agents.clear(); for (const [name, config] of DEFAULT_AGENTS) { this.agents.set(name, config); } for (const [name, config] of this.loadUserAgents()) { this.agents.set(name, config); } } /** Resolve a type name case-insensitively. Returns the canonical key or undefined. */ resolveType(name: string): string | undefined { return this.resolveKey(name); } /** Get all enabled type names (for spawning and tool descriptions). */ getAvailableTypes(): string[] { return [...this.agents.entries()] .filter(([_, config]) => config.enabled !== false) .map(([name]) => name); } /** Get all type names including disabled (for UI listing). */ getAllTypes(): string[] { return [...this.agents.keys()]; } /** Get names of default agents currently in the registry. */ getDefaultAgentNames(): string[] { return [...this.agents.entries()] .filter(([_, config]) => config.isDefault === true) .map(([name]) => name); } /** Get names of user-defined agents (non-defaults) currently in the registry. */ getUserAgentNames(): string[] { return [...this.agents.entries()] .filter(([_, config]) => config.isDefault !== true) .map(([name]) => name); } /** Whether a non-exact case-insensitive lookup has more than one candidate. */ isCaseAmbiguous(type: string): boolean { if (this.agents.has(type)) return false; const lower = type.toLowerCase(); return [...this.agents.keys()].filter((key) => key.toLowerCase() === lower).length > 1; } /** Check if a type is valid and enabled (case-insensitive). */ isValidType(type: string): boolean { const key = this.resolveKey(type); if (!key) return false; return this.agents.get(key)?.enabled !== false; } /** Get built-in tool names for a type (case-insensitive). */ getToolNamesForType(type: string): string[] { const key = this.resolveKey(type); const raw = key ? this.agents.get(key) : undefined; const config = raw?.enabled !== false ? raw : undefined; const names = config?.builtinToolNames?.length ? config.builtinToolNames : [...BUILTIN_TOOL_NAMES]; return names; } /** Resolve agent config with guaranteed non-null return. Falls back: unknown → general-purpose → absolute fallback. */ resolveAgentConfig(type: string): AgentConfig { const key = this.resolveKey(type); const config = key ? this.agents.get(key) : undefined; if (config) return config; const gp = this.agents.get("general-purpose"); if (gp) return gp; // Absolute fallback (should never happen in practice) return { name: type, displayName: "Agent", description: "General-purpose agent for complex, multi-step tasks", builtinToolNames: BUILTIN_TOOL_NAMES, systemPrompt: "", promptMode: "append", }; } private resolveKey(name: string): string | undefined { if (this.agents.has(name)) return name; const lower = name.toLowerCase(); const matches = [...this.agents.keys()].filter((key) => key.toLowerCase() === lower); return matches.length === 1 ? matches[0] : undefined; } } /** All known built-in tool names. */ export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep", "find", "ls"];