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>
71 lines
3.2 KiB
Python
71 lines
3.2 KiB
Python
"""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
|
|
],
|
|
}
|