TAI BUI
Back to curriculum
CapstonePython

Tool Registry with Schema Validation

A tool the agent can't validate is a tool the agent can't call. Build the registry and the schema checker before the tools.

TIME: ~90 minutesPREREQUISITES: Phase 13 lessons 01-07, Phase 14 lesson 01

Learning Objectives

  • Hold a typed registry of tool name → schema → handler that the dispatcher can ask once and trust afterwards.
  • Implement a JSON Schema 2020-12 subset that covers the keywords ninety percent of tool calls actually use.
  • Return precise, json-pointer-shaped error paths so the model can self-correct in one round trip.
  • Reject re-registration without explicit override, since silent overwrites are how production tool catalogs drift.
  • Keep the validator pure (no I/O, no time, no globals) so it can be re-run on a replay log.

Why the registry comes before the tool

The tool registry is the single table that answers three questions: what tools exist, what shape each tool's arguments take, and which handler to call. Once those three answers are pinned down, the rest of the harness can stop guessing.

It matters more than it sounds. A coding agent in 2026 has more registered tools than fit in one context window. A non-trivial harness registers two hundred and surfaces ten to forty on any given turn, so something has to own the full catalog.

The mistake to avoid is shipping handlers without schemas, or schemas without validation. Both are common. Both turn the next layer (the dispatcher in lesson twenty-three) into a guessing game where the only failure you ever see is a stack trace from inside a handler.

Check: Why the registry comes before the tool0/3
1. What is a tool registry?Define
2. Why does shipping handlers without schemas turn the dispatcher into a guessing game?Explain
3. A harness holds two hundred tools but only ten to forty fit in the model's context on any turn. Which component should own the full catalog?Design

What a tool record looks like

A tool record is the unit the registry stores: one name, one schema, one handler, plus the metadata the dispatcher needs.

text
ToolRecord
  name        : str          (unique, lowercase alphanumeric and underscore segments separated by dots, e.g., snake_case.segment.case)
  description : str          (one line, shown to the model)
  schema      : dict         (JSON Schema 2020-12 subset)
  handler     : Callable     (async or sync, returns Any)
  idempotent  : bool         (dispatcher uses this for retry decisions)
  timeout_ms  : int          (override per-tool dispatcher default)

The validator touches the schema and nothing else. The handler is opaque to it. That split is deliberate: the schema is data and the handler is code. Mix them and you will be tempted to put validation logic inside the handler, which is exactly the bug this lesson exists to stop.

Check: What a tool record looks like0/4
1. Which set of fields makes up one tool record in this lesson?Define
2. Why does the tool record keep the schema and the handler in separate fields?Explain
3. Under the naming rule for a tool record, which name is accepted?Apply
4. A team puts email-format checks inside each handler and now error messages differ between tools. Which design choice of the lesson did they ignore?Analyze

The JSON Schema 2020-12 subset

JSON Schema is a standard format for describing the shape of a JSON value. The full 2020-12 spec is a paper, and a tool registry needs eight of its keywords.

text
type           string / number / integer / boolean / object / array / null
properties     map of property name -> schema
required       list of property names
enum           list of allowed primitive values
minLength      integer, applies to strings
maxLength      integer, applies to strings
pattern        ECMA-262-compatible regex, applies to strings
items          schema applied to every array element

That covers what a tool API actually needs. The keywords left out (oneOf, anyOf, allOf, $ref, conditionals) are valid in production schemas, but they turn the validator into a tree walker with cycles. This is a registry, not a JSON Schema engine.

Check: The JSON Schema 2020-12 subset0/4
1. Which keywords are intentionally outside the subset this lesson implements?Define
2. What is JSON Schema?Define
3. Why does the subset leave out oneOf, anyOf, allOf, $ref and conditionals?Explain
4. A new tool takes an object with an emails list. Each email must be a string of at least 5 characters, and an error on the third element must point at that element. Which schema fits the subset?Design

Json pointer error paths

A json pointer is a slash-prefixed sequence of property names and array indices that locates one value inside a JSON document. When validation fails, the validator returns a list of errors, and each error carries a pointer into the input.

text
{"a": {"b": [1, 2, "x"]}}
                    ^
                    /a/b/2

The model reads error paths better than it reads sentences. Say a schema requires args.user.email and the model passed an integer. The error should carry the path /user/email and a message saying it expected a string, and the model can fix that on the next call without a round of natural language.

Check: Json pointer error paths0/3
1. What is a json pointer?Define
2. Why should validation errors carry a json pointer instead of only a sentence?Explain
3. The input is {"user": {"email": 0}} and the schema requires user.email to be a string. What is the error path?Apply

Registration and override

register(name, schema, handler, **opts) adds a tool to the registry and rejects re-registration by default. To replace an existing tool, the caller has to pass override=True. Think of it as operational hygiene: two parts of the codebase silently registering the same tool name is the kind of bug that takes a week to find in production.

Reads go through three methods. get(name) returns the record or raises. validate(name, args) returns an Ok or a list of errors. names() returns the tool names in registration order.

Check: Registration and override0/3
1. What does passing override=True to register() mean?Define
2. Why does register() reject duplicate names by default?Explain
3. Two modules both register a tool named search. After a deploy the later module's behavior wins and nobody notices for a week. Which rule was missing or bypassed?Analyze

What the validator is and is not

The validator is a single recursive pass over the schema tree. It is pure. It does not call handlers, it does not coerce types (a string "42" does not pass a number schema), and it does not silently truncate.

It is also not a security boundary. A malicious handler can still misbehave after validation passes. The dispatcher in lesson twenty-three adds the timeout and sandbox layers. The registry adds shape.

Check: What the validator is and is not0/5
1. What does it mean that the validator is pure?Define
2. Why is the registry not a security boundary?Explain
3. The schema says type integer and the argument is the value True. What does the validator do?Apply
4. A caller sends {"id": "forty-two"} to a tool whose schema says id is an integer. What does validate() return?Apply
5. Replaying a logged tool call through the validator months later gives a different verdict than it did live. Which property was most likely violated?Analyze

Shape

Mermaid diagram

Author's Note

I'd keep the schema and the handler in separate fields even for a tool small enough to validate inline. The record above does it on purpose, and I'd rather pay that small cost up front than find validation logic scattered across handlers later.

My bet is that the eight-keyword subset holds up longer than you'd expect, so I'd resist adding oneOf or anyOf until a real tool forces the issue. If you do extend it, I'd start with $ref and additionalProperties: false, the two additions from Going further, and keep the validator pure while you do it. Purity is what lets you re-run it on a replay log.

How to read the code

code/main.py defines ToolRegistry, ToolRecord, ValidationError, and the validator functions (_walk plus the per-type checks _check_string, _check_object, and _check_array). The validator dispatches on schema["type"], or treats a schema with enum as an untyped enum check. Each type check appends ValidationError entries to a shared list, and _walk extends the path with one segment as it descends.

code/tests/test_registry.py covers registration, override, validation success, validation failure with paths, and every keyword in the subset.

Check: How to read the code0/6
1. With the db.get_user schema (type object, required id, id an integer), what does validate return for {"fields": ["id"]}?Apply
2. With the same db.get_user schema, where fields is an array of strings from ["id", "name", "email"], what does validate return for {"id": 1, "fields": ["id", "phone"]}?Apply
3. An array schema has items of type integer. What path does the error carry for the input [1, "x", 3]?Apply
4. A schema is an object with required ["a", "b"], and the input is {}. How many errors does validate return?Apply
5. A registry has tools a and b registered in that order. Then a is re-registered with override=True. What does names() return?Apply
6. register() raises "unsupported schema keywords: ['anyOf']" for a new tool. What is the right reading of this error?Analyze

Going further

Two extensions are worth adding once this lesson lands: $ref resolution against a local definitions block, and additionalProperties: false for strict shape. Both are small, and both tend to show up as the tool catalog grows past fifty tools. I left them out to keep the file under one read.

The next lesson (twenty-two) builds the JSON-RPC stdio transport that surfaces this registry to a model client. The one after (twenty-three) wraps both behind a dispatcher with timeouts and retries.

Warm-up0/3
1. A model is given a function-calling tool. What does the JSON schema attached to that tool tell the model?Warm-up
2. A tool call fails because the model sent a bad argument. Which feedback lets the model repair the call fastest?Warm-up
3. A web API rejects a request with a 400 error before running any business logic because a field has the wrong type. Why check input shape at the door?Warm-up
Check: Going further0/1
1. Your catalog has grown past fifty tools and many schemas repeat the same address object. Which extension fits the lesson's approach?Design

Code