Skip to slide
Chapter 6 · Tools and Tool Execution
39 / 142

CHAPTER 06 · Tools and Tool Execution · 3 / 5

Code MVP: a tool registry and a shell tool

"""
chapter 06: tools and tool execution.
A tool registry with schema validation, plus a shell tool that returns a
STRUCTURED, TRUNCATED result (exit code, timing, head+tail of output).
This is the agent's hands; the capstone plugs Chapter 7's approvals in front.
"""
import subprocess, time
from dataclasses import dataclass
from typing import Callable

# --- Truncation: keep the head and tail, elide the noisy middle ---
def truncate_output(text: str, head: int = 50, tail: int = 50) -> str:
    lines = text.splitlines()
    if len(lines) <= head + tail:
        return text
    omitted = len(lines) - head - tail
    return "\n".join(lines[:head] + [f"... ({omitted} lines omitted) ..."] + lines[-tail:])

# --- A structured result the model can actually reason about ---
def format_result(exit_code: int, output: str, seconds: float) -> str:
    total_lines = len(output.splitlines())
    return (f"Exit code: {exit_code}\n"
            f"Wall time: {seconds:.2f} seconds\n"
            f"Total output lines: {total_lines}\n"
            f"Output:\n{truncate_output(output)}")

@dataclass
class Tool:
    name: str
    description: str
    parameters: dict          # JSON schema (Chapter 3)
    handler: Callable         # the function that does the work

class ToolRegistry:
    def __init__(self):
        self._tools: dict[str, Tool] = {}

    def register(self, tool: Tool):
        self._tools[tool.name] = tool

    def schemas(self) -> list:
        """The 'tools' field for the prompt (Chapter 3)."""
        return [{"type": "function", "name": t.name,
                 "description": t.description, "parameters": t.parameters}
                for t in self._tools.values()]

    def validate(self, name: str, args: dict) -> str | None:
        """Cheap schema check: required keys present? Returns an error or None."""
        tool = self._tools.get(name)
        if tool is None:
            return f"Unknown tool: {name}"
        required = tool.parameters.get("required", [])
        missing = [k for k in required if k not in args]
        return f"Missing required args: {missing}" if missing else None

    def call(self, name: str, args: dict) -> str:
        error = self.validate(name, args)           # step 1 & 2: parse + validate
        if error:
            return f"Exit code: 1\nError: {error}"
        # step 3 (permissions) is inserted here by Chapter 7.
        return self._tools[name].handler(**args)    # step 4: execute

# --- The shell tool itself: execute, capture, time, format ---
def shell_handler(command: str, timeout_ms: int = 10000) -> str:
    start = time.time()
    try:
        proc = subprocess.run(command, shell=True, capture_output=True,
                              text=True, timeout=timeout_ms / 1000)
        combined = proc.stdout + proc.stderr            # step 5: capture
        return format_result(proc.returncode, combined, time.time() - start)
    except subprocess.TimeoutExpired:
        return format_result(124, "Command timed out", time.time() - start)

if __name__ == "__main__":
    registry = ToolRegistry()
    registry.register(Tool(
        name="shell", description="Run a shell command",
        parameters={"type": "object",
                    "properties": {"command": {"type": "string"},
                                   "timeout_ms": {"type": "number"}},
                    "required": ["command"]},
        handler=shell_handler))

    print(registry.call("shell", {"command": "echo hi && seq 1 200"}))
    print("---")
    print(registry.call("shell", {}))   # missing required arg -> clean error

Run it and you will see a structured result with an exit code, a wall time, and the 200-line output neatly truncated to its head and tail. Call it with bad arguments and you get a clean validation error instead of a crash. That robustness is the whole point.

← → arrow keys work too