Skip to content

Case Study: A Typed Tool Agent ​

This case study walks through the reference tool runner, a deterministic agent that answers tool calls. A model sends each call as JSON text. The program checks the arguments against a typed class, runs the tool, and returns a JSON result. Bad calls come back as errors that name the field that failed.

The full package is examples/agents/tool_runner. It has two files:

  • src/agent.au is the reusable tool layer.
  • src/main.au defines the tools and runs the program.

The Tool Layer ​

Aura
"""A small typed tool layer: tracing, tool specs, and typed dispatch."""

import json


public enum ToolError:
    """Why a tool call did not produce a result."""

    InvalidRequest(reason: str)
    UnknownTool(name: str)
    NotReady


public class ToolCall:
    """A model's request to run one tool."""

    public tool: str
    public args: json.Value


impl json.Codec for ToolCall


public type Fn[A, R] = Callable[def(args: A) -> R]
public type Decorator[A, R] = Callable[def(f: own Fn[A, R]) -> Fn[A, R]]


public def traced[A, R](name: own str) -> Decorator[A, R]:
    """Builds a decorator that prints a line each time the tool runs."""
    return Decorator[A, R](lambda [own name] own f: trace(name.clone(), f))


def trace[A, R](name: own str, f: own Fn[A, R]) -> Fn[A, R]:
    return Fn[A, R](lambda [own name, own f] args: trace_call(name, f, args))


def trace_call[A, R](name: str, f: Fn[A, R], args: A) -> R:
    print(f"call {name}")
    return f(args)


public class ToolSpec:
    """A tool's name and the JSON Schema of its arguments."""

    public name: str
    public schema: json.Value


public def tool[A: json.Codec](name: own str) -> ToolSpec:
    """Describes a tool whose arguments decode as `A`."""
    return ToolSpec(name=name, schema=json.schema[A]())


public def run[A: json.Codec, R: json.Codec](
    f: Fn[A, Result[R, ToolError]],
    args: own json.Value,
) -> Result[json.Value, ToolError]:
    """Decodes `args`, calls `f`, and encodes its result."""
    match own json.decode[A](args):
        case Ok(decoded):
            result = try f(decoded)
            return Ok(json.encode(result))
        case Err(error):
            return Err(ToolError.InvalidRequest(reason=f"args {error}"))
  • ToolCall decodes the outer call. Its args field is a json.Value, so the arguments pass through untouched until the tool decodes them.
  • traced is a generic decorator factory. @traced("double") takes the tool's name, and the decorated function fixes A and R.
  • tool pairs a name with json.schema[A](). The schema's description is the argument class's docstring.
  • run decodes the arguments as A, calls the tool, and encodes its result as JSON. It borrows the tool, so the decorated binding stays in place.

The Tools ​

Each tool takes one argument class and returns Result[Answer, ToolError]:

Aura
class IncrementArgs:
    """Adds a step to a number."""

    value: int64
    step: int64 = 1


impl json.Codec for IncrementArgs


@traced("increment")
def increment(args: IncrementArgs) -> Result[Answer, ToolError]:
    return Ok(Answer(tool="increment", value=args.value + args.step))

step has a default, so the schema lists only value as required, and a call may leave step out.

Dispatch ​

Aura has no mutable module state, so a decorator cannot register a tool. dispatch lists the tools by name and hands each call to agent.run:

Aura
def dispatch(call: own ToolCall) -> Result[json.Value, ToolError]:
    if call.tool == "double":
        return agent.run(double, call.args)
    if call.tool == "increment":
        return agent.run(increment, call.args)
    return Err(ToolError.UnknownTool(name=call.tool))

What It Prints ​

The program prints each tool's schema, then answers seven calls:

CallOutput
double with {"value": 21}call double, then result {"tool":"double","value":42}
increment with {"value": 42}call increment, then the result with 43
increment with {"value": 42, "step": 8}the result with 50
double with {"value": "x"}rejected args $.value: expected integer, found string
double with an extra keyrejected args $: unknown field `extra`
an unknown toolunknown tool missing
a call with no tool keyrejected $: missing field `tool`

After the calls it retries a connection with control.retry, streams events from a child task through a Queue, and closes its session on exit. Both backends print the same output, which the CLI tests pin in stdout.txt.

Where To Go Next ​

  • Typed Codecs covers derived codecs, decode errors, and schemas.
  • Decorators covers decorator order and generic decorators.

Aura 0.6.0 technical preview. Implementation baseline: 4d7e076de9ff6e2e056cdec10881d4bf421358de.