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.
To reset to a fresh conversation while keeping the system prompt:
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.