Service discovery
Find agents, reasoners, and skills at runtime through the control plane discovery API, and publish selected capabilities through ARD when they need to be discoverable outside the deployment.
Find every agent, reasoner, and skill in your fleet -- at runtime, not at deploy time.
Hard-coded service addresses break the moment you scale past a handful of agents. AgentField's discovery API lets any agent query the control plane for live capabilities, filtered by tags, health status, or name patterns. Build orchestrators that discover what is available, introspect input schemas, and dynamically decide which agents to call -- all without a single hardcoded address.
For capabilities that should be discoverable outside this control plane, AgentField also supports external agent discovery through Agentic Resource Discovery (ARD). Treat it as the outside-network extension of this model: AgentField-to-AgentField communication stays simple through app.call("node.function"); ARD decides what selected capabilities other systems can find, import, and possibly call after explicit approval.
@app.reasoner(tags=["orchestrator"])
async def dynamic_router(question: str) -> dict:
# 1. Discover active agents with input schemas — zero hardcoded addresses
caps = app.discover(
tags=["public"],
health_status="active",
include_input_schema=True,
format="json",
)
print(f"{caps.json.total_agents} agents online, {len(caps.json.capabilities)} capabilities")
# 2. Feed discovered capabilities + schemas into an LLM — it picks the best agent
result = await app.ai(
system=(
"You are a routing agent. Pick the best tool for the user's question.\n"
"Available capabilities:\n" + caps.raw
),
user=question,
tools="discover", # built-in: turns discovered capabilities into callable tools
)
# 3. The LLM chose an agent and called it — no static routing table needed
return result
# New agents register at startup → the router discovers them automatically.
# No config changes, no redeployment, no service mesh.What just happened
- The agent queried the live control plane registry instead of relying on hardcoded targets
- Discovery results included enough metadata to drive dynamic routing decisions
- New active agents could appear in routing immediately after registration
Internal discovery vs ARD
| Surface | Scope | Primary use |
|---|---|---|
app.call("node.function") | Inside one AgentField control plane | Make inter-agent communication feel like a native backend call, with routing and execution context handled by AgentField. |
app.discover() / /api/v1/discovery/capabilities | Inside one AgentField control plane | Route to healthy local agents, reasoners, and skills. |
/.well-known/ai-catalog.json | Public catalog for selected entries | Let external agents, registries, and partner systems discover published capabilities. |
/api/v1/ard/search | Public ARD registry search when enabled | Let an outside client search your published catalog. |
| Discovery -> Imports | Your control plane | Record external ARD entries without making them callable yet. |
external.* callable binding | Your control plane | Route app.call() to an imported external resource after approval. |
The important product boundary: global ARD enablement does not publish or invoke anything by itself. Publishing is per reasoner or skill; external invocation is per imported binding.
Example discovery summary:
{
"discovered_at": "2026-03-23T12:00:00Z",
"total_agents": 2,
"total_reasoners": 2,
"total_skills": 0,
"pagination": { "limit": 100, "offset": 0, "has_more": false },
"capabilities": [
{
"agent_id": "weather-agent",
"base_url": "http://weather-agent:9000",
"version": "1.0.0",
"health_status": "active",
"deployment_type": "sidecar",
"last_heartbeat": "2026-03-23T12:00:00Z",
"reasoners": [
{ "id": "forecast", "tags": ["public", "weather"], "invocation_target": "weather-agent:forecast" }
],
"skills": []
},
{
"agent_id": "translator",
"base_url": "http://translator:9000",
"version": "1.0.0",
"health_status": "active",
"deployment_type": "sidecar",
"last_heartbeat": "2026-03-23T12:00:00Z",
"reasoners": [
{ "id": "translate", "tags": ["public", "translation"], "invocation_target": "translator:translate" }
],
"skills": []
}
]
}
What you get
- Runtime registry -- every agent auto-registers its reasoners and skills on startup.
- Tag-based filtering -- find agents by domain (
"insurance","nlp") instead of memorizing node IDs. - Health-aware routing -- filter to only active agents so you never call a dead endpoint.
- Schema introspection -- request input/output schemas to build dynamic tool calling at runtime.
- Three output formats --
jsonfor programmatic use,compactfor LLM context windows,xmlfor legacy integrations.
Full SDK examples
from agentfield import Agent
app = Agent(node_id="orchestrator")
# Discover all registered capabilities
result = app.discover()
print(f"Found {result.json.total_agents} agents")
# Filter by tags
result = app.discover(tags=["insurance", "claims"])
for cap in result.json.capabilities:
print(f"{cap.agent_id}: {[r.id for r in cap.reasoners]}")
# Only active agents, with input schemas
result = app.discover(
health_status="active",
include_input_schema=True,
include_output_schema=True,
)
# Compact format for feeding into LLM context
result = app.discover(format="compact")
print(result.raw) # raw string for LLM context
# result.compact is a CompactDiscoveryResponse with .reasoners and .skillsAuto-registration
Agents register themselves with the control plane automatically. When an agent starts, it sends a heartbeat containing its node ID, version, and the full list of reasoners and skills it exposes. The control plane updates the registry, and other agents can immediately discover the new capabilities.
af server # Terminal 1 -- control plane at http://localhost:8080
python app.py # Terminal 2 -- agent registers on startupThe agent exposes a /discover endpoint that the control plane polls. No manual registration step is required.
Patterns
Dynamic Tool Calling
Use discovery to build tool definitions for LLM calls at runtime. When agents come and go, the tool list updates automatically.
@app.reasoner()
async def smart_router(question: str) -> dict:
# Discover available capabilities
caps = app.discover(
tags=["public"],
include_input_schema=True,
health_status="active",
)
# Feed discovered tools into an LLM call
result = await app.ai(
system="Route the question to the best available agent.",
user=question,
tools="discover", # built-in shorthand
)
return resultHealth-Gated Failover
Before calling a specific agent, verify it is active. If not, discover an alternative with the same tags.
async def call_with_failover(target: str, tags: list[str], **kwargs) -> dict:
try:
return await app.call(target, **kwargs)
except Exception:
# Find an active alternative
fallback = app.discover(tags=tags, health_status="active")
if not fallback.json.capabilities:
raise RuntimeError(f"No active agents with tags {tags}")
alt_cap = fallback.json.capabilities[0]
alt_target = f"{alt_cap.agent_id}.{alt_cap.reasoners[0].id}"
return await app.call(alt_target, **kwargs)Publishing through ARD
Use ARD when a selected reasoner or skill should be discoverable outside the deployment.
- Enable ARD deployment guardrails in
agentfield.yamlor environment variables. - Open Discovery -> Overview and confirm the catalog URL and DID/trust status.
- Open Discovery -> Public Catalog or expand an agent in Agent nodes.
- Turn on Expose through ARD for one reasoner or skill.
- Add a display name, description, tags, representative queries, and artifact type.
Published entries are served at:
curl https://control-plane.example.com/.well-known/ai-catalog.jsonSee Expose agents to external discovery for the full external search, import, and callability workflow.
SDK reference
Python -- app.discover()
| Parameter | Type | Default | Description |
|---|---|---|---|
agent | str | None | None | Filter by single agent ID |
node_id | str | None | None | Alias for agent |
agent_ids | list[str] | None | None | Filter by multiple agent IDs |
node_ids | list[str] | None | None | Alias for agent_ids |
reasoner | str | None | None | Filter by reasoner name pattern |
skill | str | None | None | Filter by skill name pattern |
tags | list[str] | None | None | Filter by tags |
include_input_schema | bool | False | Include input schemas in response |
include_output_schema | bool | False | Include output schemas in response |
include_descriptions | bool | True | Include descriptions in response |
include_examples | bool | False | Include usage examples |
format | str | "json" | Response format: "json", "compact", "xml" |
health_status | str | None | None | Filter by health: "active", "inactive", "degraded", "unknown" |
limit | int | None | None | Pagination limit |
offset | int | None | None | Pagination offset |
Returns: DiscoveryResult with .json, .compact, .xml, .raw, and .format fields.
TypeScript -- agent.discover(options?)
| Property | Type | Default | Description |
|---|---|---|---|
agent | string | -- | Filter by single agent ID |
nodeId | string | -- | Alias for agent |
agentIds | string[] | -- | Filter by multiple agent IDs |
nodeIds | string[] | -- | Alias for agentIds |
reasoner | string | -- | Filter by reasoner name pattern |
skill | string | -- | Filter by skill name pattern |
tags | string[] | -- | Filter by tags |
includeInputSchema | boolean | -- | Include input schemas |
includeOutputSchema | boolean | -- | Include output schemas |
includeDescriptions | boolean | -- | Include descriptions |
includeExamples | boolean | -- | Include usage examples |
format | DiscoveryFormat | "json" | "json", "compact", "xml" |
healthStatus | string | -- | Filter by health status |
limit | number | -- | Pagination limit |
offset | number | -- | Pagination offset |
Returns: Promise<DiscoveryResult> with json?, compact?, xml?, raw, and format fields.
Go -- agent.Discover(ctx, ...DiscoveryOption)
| Option Function | Description |
|---|---|
WithAgent(id) | Filter by single agent ID |
WithNodeID(id) | Alias for WithAgent |
WithAgentIDs(ids) | Filter by multiple agent IDs |
WithNodeIDs(ids) | Alias for WithAgentIDs |
WithReasonerPattern(pattern) | Wildcard pattern for reasoner IDs |
WithSkillPattern(pattern) | Wildcard pattern for skill IDs |
WithTags(tags) | Filter by tags (supports wildcards) |
WithDiscoveryInputSchema(bool) | Include input schemas |
WithDiscoveryOutputSchema(bool) | Include output schemas |
WithDiscoveryDescriptions(bool) | Include descriptions |
WithDiscoveryExamples(bool) | Include usage examples |
WithFormat(format) | "json", "compact", "xml" |
WithHealthStatus(status) | Filter by health status |
WithLimit(n) | Pagination limit |
WithOffset(n) | Pagination offset |
Returns: (*types.DiscoveryResult, error) with JSON, Compact, XML, Raw, and Format fields.