Tool Design
Agents are only as good as their tools. Descriptions drive selection, validation blocks chaos, errors teach recovery. Tool design IS agent design.
▶ Watch this reelWhat you'll learn
- Schemas & descriptions
- Validation
- Errors the model can fix
- Idempotency, parallelism & tool overload
Remember this
- Tool selection is reading: names, when/when-not descriptions, and argument examples directly shape agent behavior
- Validate like untrusted input: schema + authorization + blast-radius limits; fail closed with actionable errors returned to the loop
- Idempotent tools make retries safe; parallelize independent calls; keep the tool set small or layer its discovery
Schemas & descriptions
- Name = verb_noun, unambiguous. Description answers: what / when / when NOT.
- Argument descriptions carry examples — models imitate them.
- Defaults are behavioral guardrails.
Validation
- LLM args = untrusted input: Pydantic schema + authorization + blast-radius limits.
- Fail closed: invalid → actionable ToolError back to the loop; never silently coerce.
Errors the model can fix
- Constraint + received value + correct example (+ nearest alternatives).
- Distinguish retryable vs terminal in the message.
Craft
- Idempotent tools → retries safe (upsert/ensure/delete-if-present).
- Parallelize independent calls (gather).
- ~12+ tools → group/merge/layer discovery.
Code: A production-grade tool: validated, idempotent, self-explaining errors
from pydantic import BaseModel, Field
from typing import Literal
class SearchDocs(BaseModel):
query: str = Field(min_length=2, max_length=200,
description="standalone keywords, not a question")
top_k: int = Field(default=5, ge=1, le=10)
date_after: str | None = Field(
default=None, pattern=r"^\d{4}-\d{2}-\d{2}$")
def search_documents(args: SearchDocs, user: User):
if not user.can_search:
raise ToolError("permission denied: search requires the 'kb' role")
try:
hits = vdb.search(embed(args.query), k=args.top_k,
where={"acl": {"$in": user.groups}})
except IndexUnavailable:
raise ToolError("search index temporarily unavailable — "
"retry in ~30s or answer from general knowledge")
if not hits:
raise ToolError(
f"no results for '{args.query}'. Try broader keywords "
"(e.g. drop qualifiers) or a date filter reset.")
return [{"id": h.id, "snippet": h.text[:300]} for h in hits]
# Note: ToolError messages are written FOR THE MODEL — constraints,
# received values, next steps. They are the loop's feedback signal.