Skip to content

Building Agents

An agent is a class that wraps an LLM, a set of tools, and a conversation loop. This page covers everything from creating an agent to observing what it does at runtime.


Creating an agent

Subclass Agent and set everything up in __init__:

from exagent import Agent, tool

@tool
def my_tool(x: str) -> str:
    """Does something useful."""
    return x.upper()

class MyAgent(Agent):
    def __init__(self):
        self.system_description = "You are a helpful assistant."
        self.set_model("openai", "gpt-4.1-mini")
        self.add_tool(my_tool)
        super().__init__()  # always last

Order matters

set_model, add_tool, and load_system_skill must all be called before super().__init__(). The base class builds the system prompt during __init__, so anything registered afterwards is ignored.


run() — blocking

run() sends the prompt, drives the tool loop, and returns the final text as a string. Use this when you just want the answer.

agent = MyAgent()
answer = agent.run("What does 'hello' look like in uppercase?")
print(answer)  # HELLO

Parameters

agent.run(
    prompt,                   # str — the user message
    max_iterations=10,        # int — maximum tool-call rounds before stopping
    on_tool_call=None,        # callable — fires before each tool handler runs
    on_iteration=None,        # callable — fires after each model turn
)

stream() — live output

stream() uses the provider's streaming API and yields events as they arrive. Use this for CLIs, UIs, or anywhere you want live feedback.

for event in agent.stream("What does 'hello' look like in uppercase?"):
    if event["type"] == "text_delta":
        print(event["text"], end="", flush=True)
    elif event["type"] == "done":
        print()

Event types

type When it fires Extra keys
text_delta Each token as it streams text: str
tool_call Model finalises a tool request tool_call: ToolCall
tool_result Tool handler finishes id, name, content, is_error
done Loop complete text: str (full final text)

Multi-step tool chaining

The loop runs automatically. Each iteration sees the full conversation including all previous tool results, so tools can build on each other:

@tool
def find_user(email: str) -> dict:
    """Find a user by email and return their record."""
    return {"id": "u42", "name": "Alice"}

@tool
def list_orders(user_id: str) -> list:
    """List all orders for a user."""
    return [{"id": "o1", "total": 59.99}, {"id": "o2", "total": 12.00}]

@tool
def get_order(order_id: str) -> dict:
    """Get full details of an order."""
    return {"id": "o1", "status": "shipped", "eta": "tomorrow"}

class SupportAgent(Agent):
    def __init__(self):
        self.system_description = "You are a customer support assistant."
        self.set_model("openai", "gpt-4.1-mini")
        self.add_tools([find_user, list_orders, get_order])
        super().__init__()

agent = SupportAgent()
answer = agent.run("What is the status of alice@example.com's most recent order?")
# Agent calls: find_user → list_orders → get_order → replies

The model decides the chain. The library just executes each step and feeds results back.


Observability hooks

Both run() and stream() accept two optional callbacks.

on_tool_call

Fires once per tool call, just before the handler runs. Receives a ToolCall object.

def log_tool(tc):
    print(f"→ {tc.name} called with {tc.input}")

agent.run("...", on_tool_call=log_tool)

on_iteration

Fires at the end of each model turn. Receives the iteration number (1-indexed) and the full ProviderResponse.

def log_iteration(i, response):
    print(f"[turn {i}] stop_reason={response.stop_reason}, tools={len(response.tool_calls)}")

agent.run("...", on_iteration=log_iteration)

Conversation history

Every agent maintains chat_history — a list of messages in Anthropic canonical format. It accumulates across multiple run() / stream() calls, so the agent remembers previous turns.

agent.run("My name is Alice.")
agent.run("What is my name?")  # → "Your name is Alice."

To reset to a fresh conversation while keeping the system prompt:

agent.chat_history = agent.chat_history[:1]

Note

The /clear command in the interactive shell does exactly this.


max_iterations

If the model keeps requesting tools without stopping, the loop exits after max_iterations turns and returns whatever text the model produced last. The default is 10.

agent.run("...", max_iterations=3)

Next

Configuring providers →