Agent registry as a project-local convention
The Microsoft Agent Framework deliberately does not ship an agent registry. Here's why that's the right call, and what the @register decorator pattern looks like when you build one yourself.
The reference architecture's Chapter 4 is Agent Registry. The opening line says it's the "source of truth describing every specialized agent: identity, capabilities, version, lifecycle state." That sounds important. So I went looking for it in the Microsoft Agent Framework and didn't find one.
This is on purpose. The closest thing MAF has is Azure AI Foundry's hosted agent catalog (AIProjectClient.agents.list()), but it only sees agents managed by Foundry. If you're running Ollama locally, or you mix Foundry-hosted with OpenAI-direct, or you want one canonical map of every agent your platform exposes regardless of provider, you build it.
The shape that works
The whole thing is four files in src/multi_agent/registry/:
registry/
├── __init__.py
├── models.py # AgentDescriptor, CapabilityManifest, LifecycleState
├── registry.py # in-memory AgentRegistry
└── self_register.py # @register(...) decorator
The data model is small enough to fit on one screen:
@dataclass(frozen=True)
class CapabilityManifest:
name: str
description: str
tags: tuple[str, ...] = ()
input_schema: dict[str, Any] | None = None
output_schema: dict[str, Any] | None = None
@dataclass
class AgentDescriptor:
id: str
name: str
version: str
capabilities: tuple[CapabilityManifest, ...]
lifecycle: LifecycleState = "draft"
owner: str | None = None
factory: Callable[..., Any] | None = None
metadata: dict[str, Any] = field(default_factory=dict)
LifecycleState is a Literal["draft", "staging", "production", "retired"]. The factory field is what makes this useful at runtime: it's how the registry produces a live Agent instance.
Two register patterns
The reference architecture's Chapter 4 calls out two paths into a registry. Both are real choices with real trade-offs.
Pattern A — Agent-Initiated Self-Register
sequenceDiagram
autonumber
Import->>Decorator: load agents/researcher.py
Decorator->>Registry: register(AgentDescriptor)
Registry-->>Decorator: stored
Note over Registry: lifecycle = "draft" by default
The factory carries the metadata; importing the module registers the agent. Zero boot-time configuration. The @register(...) decorator in registry/self_register.py is what does it:
@register(
id="researcher",
name="Researcher",
version="0.1.0",
capabilities=[
CapabilityManifest(
name="research",
description="Gather facts and citations; can call web_search and calculator tools.",
tags=("research", "facts", "citations"),
)
],
lifecycle="production",
)
def make_researcher(client: Any, *, name: str = "researcher", **agent_kwargs: Any) -> Agent:
return Agent(
client=client,
name=name,
instructions=RESEARCHER_INSTRUCTIONS,
tools=[governed_web_search, governed_calculator],
**agent_kwargs,
)
The agent is the decorator's argument list. The factory is the function body. They never drift because there's nowhere for them to drift to — they live in the same definition.
A useful side-effect: make registry in the project dumps every registered agent in one line:
classifier v0.1.0 [production] caps=classify_intent
critic v0.1.0 [production] caps=review
orchestrator v0.1.0 [production] caps=orchestrate
planner v0.1.0 [production] caps=plan
researcher v0.1.0 [production] caps=research
writer v0.1.0 [production] caps=write
This is the answer to "what does my platform expose?" without grepping the codebase.
Pattern B — Registry-Initiated Discovery
sequenceDiagram
autonumber
Registry->>AgentA: GET /agent-info
AgentA-->>Registry: AgentDescriptor JSON
Registry->>AgentB: GET /agent-info
AgentB-->>Registry: AgentDescriptor JSON
Note over Registry: store both
For microservices deployments each service exposes its descriptor at a well-known URL; a registry-side daemon polls and updates. This is what the reference architecture's Chapter 4 calls "Dynamic Agent Registry (Service Mesh for Agents)". I didn't implement it because the project ships as a modular monolith — but the data model is the same, and the migration is a swap of storage backend, not a rewrite of the consumer code.
Find by capability
The orchestrator's job is to pick the right specialist. find_by_capability is what makes that one line:
class AgentRegistry:
def find_by_capability(self, capability: str) -> list[AgentDescriptor]:
return [a for a in self.list() if a.has_capability(capability)]
def find_by_tag(self, tag: str) -> list[AgentDescriptor]:
return [a for a in self.list() if a.matches_tag(tag)]
When the orchestrator gets "I need a researched summary of CAP theorem", it can ask the registry for capability "research", get back [Researcher], and stop there. Or it can broaden the query to a tag and get the full set. The query API is small on purpose; the value is in what's stored, not in clever queries.
Lifecycle as the promotion gate
AgentRegistry.promote(agent_id, state) is the only way an agent transitions between lifecycle states. That's the whole point of having the registry hold the lifecycle field — it becomes the gate. The state machine in governance/lifecycle.py refuses illegal transitions:
_ALLOWED: dict[AgentLifecycle, set[AgentLifecycle]] = {
AgentLifecycle.DRAFT: {AgentLifecycle.STAGING, AgentLifecycle.RETIRED},
AgentLifecycle.STAGING: {AgentLifecycle.PRODUCTION, AgentLifecycle.DRAFT, AgentLifecycle.RETIRED},
AgentLifecycle.PRODUCTION: {AgentLifecycle.STAGING, AgentLifecycle.RETIRED},
AgentLifecycle.RETIRED: set(), # terminal
}
def transition(current: AgentLifecycle, target: AgentLifecycle) -> AgentLifecycle:
if target not in _ALLOWED.get(current, set()):
raise IllegalTransition(f"{current.value} -> {target.value} not permitted")
return target
A separate Responsible AI checklist gates promotions to production. That's covered in the governance post later in this series.
What MAF gives you that I didn't reinvent
Agent.id, Agent.name, Agent.description. Every MAF Agent has these natively. The registry doesn't reinvent agent identity — it references it, by factory() returning the configured Agent whose .id and .name are already there.
The architectural value of the registry is the layer above MAF's Agent: capability declarations, lifecycle, and a uniform discovery surface. Those are what the reference architecture asks for, and those are what MAF deliberately leaves to the application.
Falling back to Foundry
If you eventually go all-in on Foundry hosted agents, you can throw away the registry. AIProjectClient.agents.list() returns the catalog, and Foundry's own UI tracks lifecycle. The migration is a one-import swap because the registry was a project-local convention the whole time. It looked like a primitive; it never was.
The whole registry/ directory is 200-something lines of Python. The investment is in thinking about what an agent registry is for. The code falls out.