"""Agent / MCP registries + the user-facing ``Gateway`` collector. The user's ``/config/config.py`` ends with:: gateway = Gateway(agents=[...], mcps=[...], frontends=[...]) ``cli.main`` picks that object up and builds the registries. """ from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Awaitable, Callable, Iterable, Iterator from beaver_gateway.agents.base import BaseAgent from beaver_gateway.core.conversations import ConversationTexts, UserSaid from beaver_gateway.core.distill import Distiller from beaver_gateway.core.envelope import RecallContext from beaver_gateway.core.rotation import RotationPolicy from beaver_gateway.core.scheduler import Budget, Job from beaver_gateway.core.watch import VaultWatch from beaver_gateway.frontends.base import Frontend from beaver_gateway.mcp.types import McpServerT class AgentRegistry: """Name → agent lookup with duplicate detection.""" def __init__(self, agents: Iterable[BaseAgent]) -> None: self._agents: dict[str, BaseAgent] = {} for a in agents: if a.name in self._agents: msg = f"duplicate agent name: {a.name!r}" raise ValueError(msg) self._agents[a.name] = a def __getitem__(self, name: str) -> BaseAgent: return self._agents[name] def get(self, name: str) -> BaseAgent | None: return self._agents.get(name) def __iter__(self) -> Iterator[BaseAgent]: return iter(self._agents.values()) def __len__(self) -> int: return len(self._agents) def __contains__(self, name: object) -> bool: return name in self._agents class McpRegistry: """Name → MCP server lookup with duplicate detection.""" def __init__(self, mcps: Iterable[McpServerT]) -> None: self._mcps: dict[str, McpServerT] = {} for m in mcps: if m.name in self._mcps: msg = f"duplicate mcp name: {m.name!r}" raise ValueError(msg) self._mcps[m.name] = m def __getitem__(self, name: str) -> McpServerT: return self._mcps[name] def get(self, name: str) -> McpServerT | None: return self._mcps.get(name) def __iter__(self) -> Iterator[McpServerT]: return iter(self._mcps.values()) def __len__(self) -> int: return len(self._mcps) def __contains__(self, name: object) -> bool: return name in self._mcps @dataclass(slots=True) class Gateway: """Top-level object the user assembles in ``/config/config.py``.""" agents: list[BaseAgent] = field(default_factory=list) mcps: list[McpServerT] = field(default_factory=list) frontends: list[Frontend] = field(default_factory=list) texts: ConversationTexts | None = None """Merge prompt and seed bodies for ``core/conversations`` (§8.2-8.3).""" jobs: list[Job] = field(default_factory=list) """Cron / webhook / event jobs for ``core/scheduler`` (§3.6, §4.5).""" rotation: RotationPolicy | None = None """When a master is rotated (§4.5); ``None`` keeps the defaults.""" watch: VaultWatch | None = None """Vault watcher feeding the envelope (§3.5, §4.6); ``None`` = no vault block.""" recall: Callable[[RecallContext], str | None] | None = None """Envelope lookup on the user's text: pointers into the vault (cards, the agent's notes, due dates) the gateway knows no paths for (§3.3).""" user_sink: Callable[[UserSaid], Awaitable[None] | None] | None = None """Sees every user message as it enters a master or branch turn - the setup's own grep-able log of what the user said, outside the transcript.""" budget: Budget | None = None """Subscription window past which non-critical jobs wait (§4.5).""" distiller: Distiller | None = None """Who closes deep chats and where the digests and the index live (§8.4).""" tz: str = "UTC" """Local zone for the envelope clock and the rotation hour.""" host: str = "0.0.0.0" # noqa: S104 port: int = 8000 """The one listener; every HTTP frontend is mounted under its ``path``.""" public_url: str | None = None """Origin the reverse proxy shows the world (``https://b.example.com``). Advertised endpoints and MCP discovery are built on it; ``None`` derives the origin from each request."""