The Tool Sprawl Problem
An agent with forty tools pasted into every prompt is a common failure pattern. The model picks the wrong tool, a provider changes its response shape and the agent breaks at 2 a.m., and nobody knows who owns the broken integration. Teams rebuild the same API wrapper three times because no catalog records that it already exists.
This is tool sprawl: tools multiply across prompts, notebooks, and team wikis with no shared source of truth. The symptoms are consistent:
- Wrong selection: the model chooses a near-duplicate tool because nothing distinguishes them.
- Silent breakage: a provider changes its schema; the agent keeps parsing the old shape.
- No ownership: a failing tool has no accountable team, so it stays broken.
- Duplicated wrappers: each team integrates the same API independently.
A tool registry attacks all four symptoms at once. It is a single catalog where every tool is a record carrying its schema, description, owner, version, and rate limits. Agents discover tools from the registry instead of hardcoded prompt lists, and every integration has exactly one owner.
Before continuing, make sure you understand defining tools for LLMs — the registry assumes tools already have names, descriptions, and parameter schemas. The registry adds the operational metadata around them.
Once the registry exists, the next problem is keeping selection accurate as the catalog grows. That is the subject of tool selection under many options.
Anatomy of a Registry Record
Every tool in the registry is one record. The record has to answer four questions: what does the tool do, what does it accept, who owns it, and how often may it be called?
from dataclasses import dataclass, field
from typing import Any
@dataclass
class ToolRecord:
name: str
description: str
input_schema: dict[str, Any]
owner: str
version: str
rate_limit_per_min: int
domains: list[str] = field(default_factory=list)
The registry itself is a thin store around these records:
class ToolRegistry:
def __init__(self) -> None:
self._tools: dict[str, ToolRecord] = {}
def register(self, record: ToolRecord) -> None:
if record.name in self._tools:
raise ValueError(f"{record.name} is already registered")
self._tools[record.name] = record
def discover(self, domains: list[str]) -> list[ToolRecord]:
return [t for t in self._tools.values() if set(t.domains) & set(domains)]
discover is the load-bearing method: an agent asks for the tools in its current domain and gets a scoped subset instead of the full catalog.
| Field | Purpose | Example |
|---|---|---|
name |
Unique identifier the model sees | get_weather |
description |
When to use the tool, written for the model | Current conditions for a city |
input_schema |
JSON Schema for arguments | {type: object, required: [city]} |
owner |
Team accountable for the tool | payments-team |
version |
Semantic version of the schema | 1.2.0 |
rate_limit_per_min |
Throttle before the provider rejects calls | 60 |
domains |
Groups for scoped discovery | [weather, travel] |
Two fields deserve emphasis. description is read by the model on every selection decision, so it carries the same weight as the schema itself — see defining tools for LLMs for what makes one effective. owner is what makes breakage fixable: when a tool fails, the registry routes the incident to a team instead of a wiki page nobody reads.
Versioning Schemas and Grouping by Domain
A schema that changes without a version is a schema that breaks agents silently. Treat the input schema like a public API and version it with semantic versioning.
| Change | Version bump | Example |
|---|---|---|
| Add an optional field | minor | New units parameter with a default |
| Remove a field the model never used | minor | Drop a deprecated parameter |
| Add a required field | major | Make city required where it was optional |
| Narrow a type or enum | major | string becomes enum[metric, imperial] |
The check that matters is whether prompts written against the old schema still work against the new one:
def is_backward_compatible(old_schema: dict, new_schema: dict) -> bool:
old_required = set(old_schema.get("required", []))
new_required = set(new_schema.get("required", []))
return new_required <= old_required
Existing prompts supply everything the old schema required. If the new schema requires more, those prompts break — so the new required set must be a subset of the old one. Anything else is a major bump, and the registry should refuse to register it under the same version.
Domain grouping solves a different problem: an agent handling refunds should not see the weather tools. Tag every record with domains and let discover filter, as shown in the previous chapter. This keeps the model's decision space small, which directly improves selection accuracy — the core idea behind tool selection under many options.
Validating at the Boundary
Providers drift. A tool that returned temp_c last quarter returns temperature this quarter, and the agent parses None until someone notices. The registry's last job is to catch that drift at the boundary, before it reaches the model.
Declare an output schema alongside the input schema, then validate every response against it:
from pydantic import BaseModel, ValidationError
class WeatherOutput(BaseModel):
temperature: float
conditions: str
def validate_response(tool_name: str, payload: dict) -> bool:
output_model = REGISTRY.output_schemas[tool_name]
try:
output_model(**payload)
return True
except ValidationError:
return False
When validation fails, the agent receives a structured error instead of garbage, and the tool's owner gets an alert. Input validation matters for the same reason — see JSON handling and Pydantic models for the validation patterns.
Failure modes this boundary check prevents:
- Silent shape drift: the provider renames a field; the agent keeps running on stale parsing logic.
- Partial responses: an error page returns HTTP 200 with an HTML body; without schema validation the agent treats it as data.
- Cascading retries: the model retries a call that was never valid, burning tokens on a response that cannot parse.
A registry without boundary validation is a catalog that lies. The schema is a contract; enforcing it on the way out is what makes the contract real.
Quiz
1. What failure mode does a tool registry primarily prevent?
2. Which registry field tells the model when to use a tool?
3. A tool adds a new required parameter. Which version bump is correct?
4. Why validate tool responses against the declared schema at the boundary?
Flashcards
Question
What problem does a tool registry solve?
Click to reveal answer
Answer
Tool sprawl: unowned, unversioned tools that break agents silently. A registry catalogs each tool's schema, owner, version, and limits in one place.
Question
What fields does a registry record hold?
Click to reveal answer
Answer
Name, description, input schema, owner, semantic version, rate limit, and domain tags for scoped discovery.
Question
When is a schema change backward compatible?
Click to reveal answer
Answer
When it only removes or relaxes requirements. Adding a required field or narrowing a type is a breaking change that needs a major version bump.
Question
Why validate tool responses at the boundary?
Click to reveal answer
Answer
Providers drift. Validating responses against the declared schema catches shape changes before they corrupt agent logic.
Question
How do domain tags help agents?
Click to reveal answer
Answer
They let an agent load only the tool subset relevant to its current task instead of the full catalog, keeping the decision space small.
Revision Notes
Key Takeaways
- 1. A registry gives every tool a schema, owner, version, and rate limits so breakage is attributable and preventable
- 2. The description field is read by the model on every selection decision — it carries the same weight as the schema
- 3. Schema changes follow semantic versioning: adding a required field is a major bump, not a minor one
- 4. Validating responses against the declared output schema catches provider drift before it reaches the model
Interview Tips
- • Open with the symptom: agents breaking silently because tools have no owner and no version
- • Explain the versioning rule concretely: adding a required field breaks old prompts, so it is a major bump
- • Mention boundary validation as the mechanism that turns a schema into an enforced contract
- • Connect the registry to selection accuracy: domain tags shrink the model's decision space
Cheat Sheet
Tool Registry Cheat Sheet
What it is: one catalog where every tool is a record with schema, description, owner, version, rate limits, and domain tags.
Registry record fields:
| Field | Purpose |
|---|---|
| name | Unique id the model sees |
| description | When to use the tool (read by the model) |
| input_schema | JSON Schema for arguments |
| owner | Accountable team |
| version | Semantic version of the schema |
| rate_limit_per_min | Throttle before provider 429s |
| domains | Scoped discovery groups |
Versioning rules:
- Add optional field → minor
- Remove unused field → minor
- Add required field → major (breaking)
- Narrow a type/enum → major (breaking)
- Compatible iff new required set ⊆ old required set
Boundary validation:
- Validate every response against the declared output schema
- Catch silent shape drift, HTML error pages with HTTP 200, cascading retries
- Alert the tool owner on validation failure
Failure modes prevented: wrong selection, silent breakage, no ownership, duplicated wrappers