"""Agent manifest dataclass (ADR-0014). A manifest is the single point of registration for an agent. The orchestrator, admin UI, registry endpoint, and inference framework all read from it. Adding an agent is adding a manifest + agent class — never editing a list elsewhere. The manifest lives next to the agent code (each agent module in ml/agents/ exposes a module-level `MANIFEST` constant). The registry surfaces both the agent instance and its manifest. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Any, Callable @dataclass(frozen=True) class InferredParam: """One auto-inferred preference key (#111-#116). The inference framework owns scheduling, history reads, persistence, and cold-start. Each agent's `inferred_params` list declares what to infer and how, leaving each agent to implement just `infer()`. """ key: str # e.g. 'quietStart' ttl_sec: int # how often to recompute cold_start_default: Any # value used until min_history is met min_history: int # event count threshold # Pure function: given a UserHistory snapshot, return the inferred value. # Typed as a generic callable here; concrete signature lives in the framework. infer: Callable[[Any], Any] | None = None @dataclass(frozen=True) class AgentManifest: """Declarative description of an agent — see ADR-0014 §1.""" id: str # 'time-of-day' version: str # bump invalidates cached outputs + inferences description: str # one-line human summary for admin UI pref_schema: dict # JSON Schema for user-tunable knobs context_schema: list[str] # signals it reads, e.g. ['todoist.tasks'] required_consents: list[str] # ['data:todoist', 'agent:time-of-day'] output_contract: dict # snippet shape (free text + optional tags) ttl_sec: int # snippet freshness for agent_outputs silenced_in_contexts: list[str] = field(default_factory=list) # active context names that suppress this agent inferred_params: list[InferredParam] = field(default_factory=list) def to_dict(self) -> dict: """Serialise for the registry endpoint. `inferred_params` drops `infer` (callable) since the wire format only carries metadata.""" return { "id": self.id, "version": self.version, "description": self.description, "pref_schema": self.pref_schema, "context_schema": self.context_schema, "required_consents": self.required_consents, "output_contract": self.output_contract, "ttl_sec": self.ttl_sec, "silenced_in_contexts": list(self.silenced_in_contexts), "inferred_params": [ { "key": p.key, "ttl_sec": p.ttl_sec, "cold_start_default": p.cold_start_default, "min_history": p.min_history, } for p in self.inferred_params ], }