feat(agents): manifest plumbing + GET /agents/registry (ADR-0014 step 3)

Each agent now exports a module-level MANIFEST declaring id, version,
pref_schema, required_consents, ttl_sec, and silenced_in_contexts. The
registry surfaces both the agent and its manifest, and rejects on
mismatch so the two cannot drift.

ml/serving exposes GET /agents/registry; services/api proxies it as
GET /api/agents/registry with a 60s in-process cache so admin pageviews
don't hammer upstream. Failures aren't cached.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-05 10:55:54 +00:00
parent 5d43339616
commit 305eeae38b
13 changed files with 511 additions and 33 deletions

View File

@@ -1,21 +1,41 @@
from __future__ import annotations
from .base import BaseAgent
from .overdue_task import OverdueTaskAgent
from .momentum import MomentumAgent
from .time_of_day import TimeOfDayAgent
from .recent_patterns import RecentPatternsAgent
from .focus_area import FocusAreaAgent
"""Agent registry — single point of registration for sub-agents (ADR-0014).
_AGENTS: dict[str, BaseAgent] = {
a.agent_id: a
for a in [
OverdueTaskAgent(),
MomentumAgent(),
TimeOfDayAgent(),
RecentPatternsAgent(),
FocusAreaAgent(),
]
}
Each agent module contributes:
- a `BaseAgent` subclass instance
- a module-level `MANIFEST: AgentManifest`
The orchestrator, registry endpoint, and inference framework all read from
here. Adding an agent is: add a module, register it once below.
"""
from __future__ import annotations
from .base import BaseAgent
from .manifest import AgentManifest
from .overdue_task import OverdueTaskAgent, MANIFEST as OVERDUE_TASK_MANIFEST
from .momentum import MomentumAgent, MANIFEST as MOMENTUM_MANIFEST
from .time_of_day import TimeOfDayAgent, MANIFEST as TIME_OF_DAY_MANIFEST
from .recent_patterns import RecentPatternsAgent, MANIFEST as RECENT_PATTERNS_MANIFEST
from .focus_area import FocusAreaAgent, MANIFEST as FOCUS_AREA_MANIFEST
_REGISTERED: list[tuple[BaseAgent, AgentManifest]] = [
(OverdueTaskAgent(), OVERDUE_TASK_MANIFEST),
(MomentumAgent(), MOMENTUM_MANIFEST),
(TimeOfDayAgent(), TIME_OF_DAY_MANIFEST),
(RecentPatternsAgent(), RECENT_PATTERNS_MANIFEST),
(FocusAreaAgent(), FOCUS_AREA_MANIFEST),
]
# Sanity check — agent_id and manifest.id must agree, otherwise the registry
# becomes inconsistent across endpoints.
for _agent, _manifest in _REGISTERED:
if _agent.agent_id != _manifest.id:
raise RuntimeError(
f"Manifest mismatch: {_agent.__class__.__name__}.agent_id={_agent.agent_id!r} "
f"≠ MANIFEST.id={_manifest.id!r}"
)
_AGENTS: dict[str, BaseAgent] = {a.agent_id: a for a, _ in _REGISTERED}
_MANIFESTS: dict[str, AgentManifest] = {m.id: m for _, m in _REGISTERED}
def get_agent(agent_id: str) -> BaseAgent:
@@ -26,3 +46,13 @@ def get_agent(agent_id: str) -> BaseAgent:
def all_agents() -> list[BaseAgent]:
return list(_AGENTS.values())
def get_manifest(agent_id: str) -> AgentManifest:
if agent_id not in _MANIFESTS:
raise KeyError(f"Unknown agent: {agent_id!r}. Known: {sorted(_MANIFESTS)}")
return _MANIFESTS[agent_id]
def all_manifests() -> list[AgentManifest]:
return list(_MANIFESTS.values())