AI Agent Beginner Tutorial: 06 - Tool Systems

Move scattered functions into a tool registry and handle schemas, permissions, result limits, timeouts, bounded retries, stable errors, and business idempotency by side-effect level. A failure matrix verifies every recovery policy.

Contents7 sections

Risks added when ordinary functions enter an agent

The fake model and behavioral tests now fix the search-read-finish path as a baseline. This makes it safe to replace dispatch_tool() with one tool registry without changing behavior.

search_local_sources() is simple in isolation. Inside an agent loop, the model may repeat requests or send oversized arguments, while the tool may time out or return excessive content. Write tools add a harder case: the client observes a timeout after the server already completed the write, so a retry duplicates the side effect.

A tool registry places one execution boundary around every tool.

TEXT
Model request
   │
   ▼
Schema check ─▶ Authorization ─▶ Timeout/concurrency ─▶ Handler
                                                       │
                                                       ▼
Model result ◀─ Stable error ◀─ Truncate/redact ◀─ Raw result

Label side effects first

LevelExampleAutomatic retry
readSearch, read documentBounded retry for transient faults
reversible_writeSave an overwritable draftRetry with idempotency and status lookup
irreversible_writeSend message, make paymentNo default retry; enter approval

A timeout means the caller does not know the outcome. It does not prove that the operation failed. Reads can retry, while writes must query execution state first.

Keep descriptions and policy together in ToolSpec

PYTHON
from dataclasses import dataclass
from typing import Any, Callable

@dataclass(frozen=True)
class ToolSpec:
    name: str
    description: str
    effect: str
    timeout_seconds: float
    max_attempts: int
    max_output_chars: int
    handler: Callable[..., Any]

class ToolRegistry:
    def __init__(self) -> None:
        self._tools: dict[str, ToolSpec] = {}

    def register(self, spec: ToolSpec) -> None:
        if spec.name in self._tools:
            raise ValueError("duplicate_tool")
        self._tools[spec.name] = spec

    def get(self, name: str) -> ToolSpec | None:
        return self._tools.get(name)

The model-visible description, runtime policy, and handler come from the same registry entry, preventing schema changes from drifting away from the executor.

Normalize invocation results

PYTHON
def invoke_tool(registry: ToolRegistry, name: str, arguments: dict) -> dict:
    spec = registry.get(name)
    if spec is None:
        return {"ok": False, "error": "unknown_tool", "retryable": False}
    try:
        value = spec.handler(**arguments)
        return {"ok": True, "data": str(value)[:spec.max_output_chars]}
    except PermissionError:
        return {"ok": False, "error": "forbidden", "retryable": False}
    except (ConnectionError, TimeoutError):
        return {"ok": False, "error": "transient_failure", "retryable": True}
    except TypeError:
        return {"ok": False, "error": "invalid_arguments", "retryable": False}
    except Exception:
        return {"ok": False, "error": "tool_failure", "retryable": False}

The model receives only stable error codes and retryable. Exception stacks stay in service logs. Even retryable=True remains subject to max_attempts and the overall task budget.

Give write tools business idempotency keys

For saving a brief, an idempotency key can combine task_id + path + content_hash. Repeating the same business intent returns the first result without writing again.

PYTHON
class IdempotencyStore:
    def __init__(self) -> None:
        self._results: dict[str, dict] = {}

    def run_once(self, key: str, operation: Callable[[], dict]) -> dict:
        if key in self._results:
            return self._results[key]
        result = operation()
        if result.get("ok"):
            self._results[key] = result
        return result

A network request ID identifies one transport attempt rather than business intent. When deduplication must survive process restarts, store idempotency results in a database and commit them with output-file state.

Test from a failure matrix

FailureRead toolWrite toolExpected result
Missing argumentDo not executeDo not executeinvalid_arguments
Transient disconnectRetry at most twiceQuery status firstCount against budget
Authorization denialDo not retryDo not retryforbidden
Timeout with unknown outcomeMay query againNever rewrite blindlyoutcome_unknown
Oversized outputTruncateTruncate receiptRecord original size

Tests inspect the handler's actual call count. Asserting only the final error code cannot reveal that the executor wrote twice.

What changes after the reliable tool layer

The agent loop no longer calls arbitrary functions directly. Every action passes through the registry, making errors and budgets visible in one place. Chapter 7 keeps this registry, replaces demonstration data with a searchable corpus, and connects search_local_sources and get_document to one RAG pipeline.

REFERENCES

References

  1. 01Function Calling | OpenAI API
  2. 02Production Best Practices | OpenAI API
  3. 03LLM06:2025 Excessive Agency | OWASP GenAI Security Project

Series

AI AGENT Beginner's Guide

Next step

Continue with related topics

Continue along the same topic.

Browse latest news