Agents in the Open Part 3: How the Coordinator Picks a Specialist
Orrery's coordinator doesn't answer questions itself. It hands each one to a specialist: Kafka, Kubernetes, observability, the ops journal, and any others installed. This part is about how that...
Orrery's coordinator doesn't answer questions itself. It hands each one to a specialist: Kafka, Kubernetes, observability, the ops journal, and any others installed. This part is about how that choice is made, and why the old way kept producing the same kind of bug once the platform was open to specialists I didn't write (Part 17 of the previous series).
The Bug That Kept Coming Back
Specialists were already plugins, registered through Python entry points. But the coordinator still kept a table of facts about the built-in ones:
a routing line per agent name;
which tools each one contributes to a health sweep;
the batch workflow's five checkers and their report keys;
the Kubernetes and Kafka tools of the remediation loop;
the ops journal's name and tools.
Every one of those facts could outlive the specialist it described. In the lab, with Kubernetes, Docker and Elasticsearch switched off, they did:
the health sweep still called their tools (they failed, harmlessly);
the coordinator still offered to save notes to a journal that was switched off;
in a scheduled run, a "degraded" verdict could still send the remediation loop to
restart_deploymenton the disabled Kubernetes. Only the approval gate stopped it.
Fixing these one by one wasn't enough, because each one came from the same design. A third-party specialist also suffered from it the other way round: it could be routed to, but it could never join a sweep or offer a fix.
The Decision: A Specialist Declares What It Contributes
As of Orrery 0.6.6 (ADR-005), an entry point names a SpecialistSpec: the agent, plus what it adds to the rest of the platform. Each contribution carries the tools that implement it.
specialist = SpecialistSpec(
agent=root_agent,
routing_hint="Kubernetes cluster info, nodes, pods, deployments, ...",
health_check=HealthCheck(
system="Kubernetes",
checks="...", criteria="...",
tools=(get_cluster_info, get_nodes, get_events, list_pods),
),
remediation=Remediation(
actions=(restart_deployment, scale_deployment, rollback_deployment),
playbook="...",
verify=(get_deployment_status, list_pods, describe_pod),
),
)The coordinator now imports no specialist. Its routing section, the chat sweep, the batch workflow's checkers, the remediation loop and the journal step are all built from the declarations of the enabled specialists. The coordinator can't mention a specialist that is switched off, because nothing about that specialist is in its inputs.

Two design choices matter more than the shape:
Typed capabilities, not flags. The first draft marked the journal with a string,
"journal". A flag has nothing behind it: the journal step still had to import the ops journal's tools, and a typo was silently ignored.Journal(save=..., log=...)carries its interface.The rules are checked when the specialist loads. A health check may list only the specialist's own tools, none labelled
@confirmor@destructive, and none named like a write. Sweeps run with nobody watching, and before this, an instruction saying "take no action" was the only thing keeping them read-only. Remediation actions, on the other hand, must be labelled, so they always stop at the approval gate.
Three Layers, Each One Narrows the Last
Which specialists exist is decided before any question, in three layers:

Installed: every package in the environment, transitive dependencies included. That is why installed isn't trusted (Part 4).
Allowed: the operator's
ORRERY_SPECIALISTSandORRERY_DISABLED_SPECIALISTS. An unknown name stops startup.Enabled: an admin's switches in the console, at run time. Switching on something the environment excluded is refused. Note the difference: the environment removes a specialist's tools, while a switch in the console removes its route. An agent an admin composes in the console may still use a switched-off specialist's tools, each behind its own guard. That's deliberate (a narrow agent over a broad specialist's tools is a common reason to switch the broad one off), and the ADR lists it as a limit.
The coordinator tree is rebuilt for every catalog change and swapped in atomically. A turn already running finishes on the tree it started with.
Then the Model Picks
The per-turn choice is still the model's: each enabled specialist is an AgentTool, and the routing section gives one line per specialist. That step is probabilistic by design. Three things bound it:
A closed world. The model is never told about a specialist it can't call.
One owner per line. The routing hint sits next to the specialist's tools and is written by the people who know them.
One line each. A hint enters the prompt as a single line of at most 200 characters, so a hint can't add a section of its own.
What isn't covered yet: there is no routing eval. make eval scores each specialist on its own questions, not the coordinator's choice between them. Now that third parties write routing hints, an eval generated from the enabled set, including a case where a hint tries to inject instructions, is the next check to add. The ADR lists this and its other limits openly. An independent review of the first version found that it claimed more than the code did, so the record now says what is not guaranteed.
The Pattern
When a central component keeps facts about plugins, those facts outlive the plugins. Move each fact into the plugin, with the code that makes it true, and build the central component from the enabled plugins' declarations. The same idea runs through Python entry points, pytest plugins and A2A Agent Cards. What made it safe here was checking each declaration when the specialist loads.
That leaves one question: what happens when the specialist wasn't written by me? That's Part 4.