From d65ce078b7e8de4379100e7ae0e0d2a432aed8e8 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Fri, 6 Feb 2026 15:52:58 -0800 Subject: [PATCH] feat: add AgentConfigRegistry for named agent configurations --- src/agents/registry.test.ts | 64 +++++++++++++++++++++++++++++++++++++ src/agents/registry.ts | 57 +++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 src/agents/registry.test.ts create mode 100644 src/agents/registry.ts diff --git a/src/agents/registry.test.ts b/src/agents/registry.test.ts new file mode 100644 index 0000000..fc838db --- /dev/null +++ b/src/agents/registry.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest'; +import { AgentConfigRegistry, type AgentConfig } from './registry.js'; + +describe('AgentConfigRegistry', () => { + describe('register()', () => { + it('registers a named agent config', () => { + const registry = new AgentConfigRegistry(); + const config: AgentConfig = { name: 'assistant', systemPrompt: 'Be helpful.' }; + registry.register(config); + + expect(registry.get('assistant')).toEqual(config); + }); + + it('throws on duplicate name', () => { + const registry = new AgentConfigRegistry(); + registry.register({ name: 'assistant' }); + expect(() => registry.register({ name: 'assistant' })).toThrow('already registered'); + }); + }); + + describe('get()', () => { + it('returns undefined for unknown name', () => { + const registry = new AgentConfigRegistry(); + expect(registry.get('nonexistent')).toBeUndefined(); + }); + }); + + describe('list()', () => { + it('returns all registered configs', () => { + const registry = new AgentConfigRegistry(); + registry.register({ name: 'a' }); + registry.register({ name: 'b' }); + expect(registry.list().map(c => c.name).sort()).toEqual(['a', 'b']); + }); + }); + + describe('loadFromConfig()', () => { + it('loads configs from a raw config object', () => { + const registry = new AgentConfigRegistry(); + registry.loadFromConfig({ + assistant: { + system_prompt: 'Be helpful.', + model_tier: 'default', + tool_profile: 'messaging', + sandbox: false, + }, + coder: { + model_tier: 'complex', + tool_profile: 'coding', + sandbox: true, + }, + }); + + expect(registry.list()).toHaveLength(2); + const assistant = registry.get('assistant')!; + expect(assistant.systemPrompt).toBe('Be helpful.'); + expect(assistant.modelTier).toBe('default'); + expect(assistant.toolProfile).toBe('messaging'); + + const coder = registry.get('coder')!; + expect(coder.sandbox).toBe(true); + }); + }); +}); diff --git a/src/agents/registry.ts b/src/agents/registry.ts new file mode 100644 index 0000000..25f300b --- /dev/null +++ b/src/agents/registry.ts @@ -0,0 +1,57 @@ +import type { ToolProfile, ToolOverrideConfig } from '../config/schema.js'; +import type { ModelTier } from '../models/router.js'; + +export interface AgentConfig { + name: string; + systemPrompt?: string; + modelTier?: ModelTier; + toolProfile?: ToolProfile; + toolOverrides?: ToolOverrideConfig; + sandbox?: boolean; +} + +/** + * AgentConfigRegistry — stores named agent configurations. + * Loaded from YAML config at startup. + */ +export class AgentConfigRegistry { + private configs = new Map(); + + register(config: AgentConfig): void { + if (this.configs.has(config.name)) { + throw new Error(`Agent config '${config.name}' is already registered`); + } + this.configs.set(config.name, config); + } + + get(name: string): AgentConfig | undefined { + return this.configs.get(name); + } + + list(): AgentConfig[] { + return Array.from(this.configs.values()); + } + + /** + * Load agent configs from the parsed YAML config. + * Maps from the config schema format to the internal AgentConfig format. + */ + loadFromConfig(rawConfigs: Record): void { + for (const [name, raw] of Object.entries(rawConfigs)) { + this.register({ + name, + systemPrompt: raw.system_prompt, + modelTier: raw.model_tier as ModelTier | undefined, + toolProfile: raw.tool_profile as ToolProfile | undefined, + toolOverrides: raw.tool_overrides, + sandbox: raw.sandbox, + }); + } + } +}