Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 4: Giving Models Tools

Intake has the O-1001 case open. Before Outreach can draft a status email to Jordan, ShopOps must load the live order, customer profile, and shipment record from systems that own those facts — not from whatever the model remembers from the prompt.

Without typed tools, the model guesses. It invents tracking numbers, skips preferred_channel, and drafts against facts that were never loaded. Policy cannot check required fields that never entered case state.

Intake → get_customer + get_order → facts in case state → Outreach draft_email

Tools are the agent’s actuators: named operations with schemas the model can see and the runtime can enforce. This chapter adds the tool registry — spec, handler, side-effect flags, scopes — so proposals stay grounded in real reads and reviewable writes.

First principles

A tool is a typed capability: one named operation with a contract the model can see and the runtime can enforce.

PieceRole
NameStable identifier in the action vocabulary
DescriptionDiscoverability text for the model
ParametersJSON-Schema-ish contract
HandlerCode that returns an Observation
Side-effect flagRead vs write (feeds Ch 5)
ScopesAuthz vocabulary (feeds Ch 5 / 27)

For O-1001, get_customer and get_order return evidence. draft_email produces a reviewable artifact. send_email changes the outside world. Those are different capabilities, even when they are all represented as tool calls.

The tool registry maps name → spec → handler. The spec is the shared contract: it tells the model how to ask and tells the runtime how to validate and dispatch.

Two error classes to keep separate:

  1. Model errors — wrong tool, wrong args, invented fields.
  2. Execution errors — handler/env failures (timeouts, 404s).

Both must become observations the loop can consume, not uncaught exceptions that kill the worker mid-case. The agent can then retry, choose another read, or escalate from an explicit state.

Two practical consequences follow:

  • Natural-language argument blobs are not a production interface; use typed fields that validation can reject.
  • More tools do not automatically add capability. They also add selection errors, permissions to protect, and behavior to test.

Concrete example

Registry with get_customer, list_order_notes, get_order, draft_email, … Intake lookup runs before any contact draft in the scripted policy from Ch 1.

Diagram

Registry dispatch:

sequenceDiagram
  participant P as Policy / model
  participant R as ToolRegistry
  participant H as Handler
  participant L as Loop / F
  P->>R: validate_args(name, args)
  alt unknown or invalid
    R-->>P: error + known_tools
  else ok
    R->>H: handler(state, args)
    H-->>R: Observation
    R-->>L: Observation
  end

Notice: validation happens before side effects.

Implementation

PYTHONPATH=. python examples/ch04_tools.py

shopops/tools/registry.py:

@dataclass
class ToolSpec:
    name: str
    description: str
    parameters: dict[str, Any]
    handler: Handler
    side_effect: bool = False
    scopes: frozenset[str] = field(default_factory=frozenset)

class ToolRegistry:
    def register(self, spec: ToolSpec) -> None: ...
    def validate_args(self, name: str, args: dict) -> str | None: ...
    def schemas_for_model(self) -> list[dict]: ...

Unknown tool returns a useful error:

unknown_tool:teleport; known=['draft_email', 'get_order', ...]

draft_email requires body: string. Missing required fields fail closed.

Side-effect awareness starts here: get_customer is read; send_email and add_order_note are writes. Chapter 5 will refuse to treat those equally.

Walkthrough: profile before draft

For O-1001, the teaching policy refuses to draft until get_customer has returned. That is not politeness; it is grounding. Without the tool, the model happily invents “Alex” and a wrong order total. With the tool, State.customer_name becomes "Jordan Lee" via (F), and the draft references real fields.

What the model sees (via schemas_for_model) should match what handlers accept. Drift between the two is a class of production bug: you update the handler, forget the schema, and the model keeps sending obsolete arguments that validate against the old contract in docs but fail at runtime — or worse, validate loosely and do the wrong thing.

Discoverability is an API design problem

Tool descriptions are UX for a stochastic client. Good descriptions:

  • State preconditions (“call after case is loaded”)
  • State side effects (“does not send; returns draft body only”)
  • Disambiguate siblings (“use this for order total, not for proposed refund amounts”)

Bad descriptions:

  • Marketing fluff (“powerfully retrieves customer essence”)
  • Overlap without contrast
  • Secret requirements not listed in required

If two tools cannot be distinguished from their schemas alone, merge them or rename until a careful engineer — and thus a careful model — can choose.

Observations over exceptions

Across the loop boundary, prefer:

Observation(ok=False, error="missing_required:body")

over raising KeyError inside the worker. Raises are fine inside handlers if the registry catches and translates them; uncaught raises skip (F), skip checkpoints, and leave the FSM lying.

Failure modes

Ambiguous twins. get_user vs get_customer vs fetch_account_profile — the model oscillates.

Schema lies. Description says “returns email” but handler never does; the model fabricates.

Exception leakage. Handler raises; worker dies; checkpoint incomplete (Ch 8).

Argument soup. Passing prose blobs instead of typed fields ("amount": "about four hundred").

Tool sprawl. Fifty tools in one agent; selection accuracy collapses; debug cost explodes.

Production considerations

  • Generate model-facing schemas from the same ToolSpec objects handlers use — one source of truth.
  • Include examples in descriptions sparingly; prefer crisp contracts.
  • Version tool schemas; pin versions in traces.
  • Return structured errors (missing_required, type_error, unknown_tool).
  • Count tool selection mistakes in evals (Part VI), not only final email quality.
  • Prefer a small tool surface per agent/module; split by permission domain later (Part V / X).

Chapter summary

  • Tools are actuators; schemas and descriptions are program text.
  • Registry = name → schema → handler.
  • Validate before execute; convert failures to observations.
  • Separate model errors from execution errors.
  • Mark side effects early.
  • Ambiguity and sprawl are design bugs.
  • Intake before contact draft for O-1001.
  • Code: shopops/tools/registry.py, examples/ch04_tools.py.

Exercises

  1. Register two deliberately ambiguous tools (get_order / fetch_owed_amount); log which the mock policy picks.
  2. On unknown tool, always return known_tools in the observation error string; unit-test it.
  3. Add typed errors for wrong JSON types (string where integer required).
  4. Remove half the tools from the registry and list which ShopOps flows still work — practice least surface.

References

  • OpenAI / Anthropic tool-calling / function-calling documentation. [VERIFY SOURCE for current API shapes]
  • JSON Schema specification — https://json-schema.org/ [VERIFY SOURCE for draft version you implement]
  • Yao et al., 2022. ReAct. arXiv:2210.03629 — acting via tools as a research pattern.