"""Configuration system — YAML config + Pydantic validation.""" from __future__ import annotations from pathlib import Path from typing import Any import yaml from pydantic import BaseModel, Field class ProviderConfig(BaseModel): """Single LLM provider configuration.""" provider: str = "anthropic" model: str = "claude-sonnet-4-5-20250929" max_tokens: int = 8192 temperature: float = 0.3 api_key_env: str = "" class LLMConfig(BaseModel): """LLM providers section.""" providers: dict[str, ProviderConfig] = Field(default_factory=dict) class CLIChannelConfig(BaseModel): enabled: bool = True default_agent: str = "coder" class DiscordChannelConfig(BaseModel): enabled: bool = False token_env: str = "DISCORD_BOT_TOKEN" default_agent: str = "coder" allowed_users: list[str] = Field(default_factory=list) class ChannelsConfig(BaseModel): cli: CLIChannelConfig = Field(default_factory=CLIChannelConfig) discord: DiscordChannelConfig = Field(default_factory=DiscordChannelConfig) class ToolsConfig(BaseModel): workspace: str = "./data" auto_approve: list[str] = Field( default_factory=lambda: ["read_file", "list_dir", "web_fetch", "delegate"] ) require_approval: list[str] = Field( default_factory=lambda: ["bash", "write_file", "edit_file"] ) class MCPServerConfig(BaseModel): """Single MCP server configuration.""" command: str = "" args: list[str] = Field(default_factory=list) env: dict[str, str] = Field(default_factory=dict) url: str = "" class PerformanceConfig(BaseModel): """Performance tuning — caching, cost tracking, model routing.""" cache_ttl: int = 3600 daily_budget_usd: float = 0.0 monthly_budget_usd: float = 0.0 fallback_model: str = "" model_routing: dict[str, str] = Field(default_factory=dict) class OrchestratorConfig(BaseModel): max_concurrent: int = 5 delegation_timeout: int = 120 class AgentFileConfig(BaseModel): """Parsed from agent markdown frontmatter.""" name: str = "" provider: str = "anthropic" model: str = "" temperature: float = 0.3 max_iterations: int = 30 tools: list[str] = Field(default_factory=list) instructions: str = "" class Config(BaseModel): """Top-level application config.""" llm: LLMConfig = Field(default_factory=LLMConfig) channels: ChannelsConfig = Field(default_factory=ChannelsConfig) tools: ToolsConfig = Field(default_factory=ToolsConfig) mcp_servers: dict[str, MCPServerConfig] = Field(default_factory=dict) agents: dict[str, str] = Field(default_factory=dict) orchestrator: OrchestratorConfig = Field(default_factory=OrchestratorConfig) performance: PerformanceConfig = Field(default_factory=PerformanceConfig) def load_config(path: str | Path = "config.yaml") -> Config: """Load and validate config from YAML file.""" p = Path(path) if not p.exists(): return Config() raw = yaml.safe_load(p.read_text()) or {} return Config.model_validate(raw) def parse_agent_file(path: str | Path) -> AgentFileConfig: """Parse a markdown agent definition with YAML frontmatter.""" text = Path(path).read_text() if not text.startswith("---"): return AgentFileConfig(instructions=text) parts = text.split("---", 2) if len(parts) < 3: return AgentFileConfig(instructions=text) frontmatter = yaml.safe_load(parts[1]) or {} body = parts[2].strip() frontmatter["instructions"] = body return AgentFileConfig.model_validate(frontmatter)