First agent with tools¶
In ~10 minutes you'll build an agent that uses three tools — built-in and custom — to answer a multi-step question.
If you haven't done Getting started, do that first.
What we're building¶
An assistant agent that can answer questions involving the current date, basic math, and counting characters. The agent decides which tools to call.
agent.run("If today is the start of a 30-day trial, when does it end? Also, how many 'a's in 'Madagascar'?")
The model will call get_current_date_and_time, calculate, and letter_counter in whatever order it decides, then synthesise the answer.
1. Set up the model client¶
Any tool-capable model works. To check: client.is_tool_using_model.
2. Use built-in tools¶
aimu.tools.builtin ships a set of ready-made tools. They're grouped by domain so you don't have to remember individual names:
from aimu.tools import builtin
builtin.web # [get_weather, get_webpage, search, wikipedia]
builtin.fs # [list_directory, read_file]
builtin.compute # [calculate]
builtin.misc # [echo, get_current_date_and_time]
For this tutorial we want compute (for math) and misc (for the date):
from aimu.agents import Agent
from aimu.tools import builtin
agent = Agent(client, "You are a helpful assistant.", tools=builtin.compute + builtin.misc)
print(agent.run("What is 17 * 23?"))
# 17 * 23 = 391.
The agent's loop dispatched calculate("17 * 23") and reported the result.
3. Add a custom tool¶
Now declare a tool the built-ins don't cover:
from aimu.tools import tool
@tool
def letter_counter(word: str, letter: str) -> int:
"""Count occurrences of a letter in a word."""
return word.lower().count(letter.lower())
Three things to notice:
- Type hints are required for every parameter (or a default value). Missing both raises
ToolSignatureError. - The first paragraph of the docstring becomes the tool description. Be specific — this is what the model reads to decide whether to call it.
- The function is unchanged —
letter_counter("hi", "i")still works directly. The decorator only attaches an OpenAI-format spec atfunc.__tool_spec__.
Add it to the agent's tools:
agent = Agent(
client,
"You are a helpful assistant.",
tools=builtin.compute + builtin.misc + [letter_counter],
)
print(agent.run(
"If today is the start of a 30-day trial, when does it end? "
"Also, how many 'a's are in 'Madagascar'?"
))
The model will:
- Call
get_current_date_and_time()to learn today's date. - Call
calculate("...")to add 30 days. - Call
letter_counter(word="Madagascar", letter="a")→ 4. - Return a synthesised answer.
4. Inspect what happened¶
The agent records the full conversation in client.messages. Each tool call appears as an assistant message with tool_calls, followed by a tool message with the result.
for msg in client.messages:
role = msg["role"]
if "tool_calls" in msg:
for tc in msg["tool_calls"]:
print(f" → {tc['function']['name']}({tc['function']['arguments']})")
elif role == "tool":
print(f" ← {msg['name']}: {msg['content']}")
elif msg.get("content"):
print(f"[{role}] {msg['content'][:80]}")
This is OpenAI's message format — there's no proprietary wrapper.
5. Stream the agent¶
Same iterator API as client.chat(stream=True), but each StreamChunk carries the agent name and loop iteration:
for chunk in agent.run("count r's in strawberry and 2+2", stream=True):
if chunk.is_tool_call():
name = chunk.content["name"]
args = chunk.content["arguments"]
print(f"\n[tool: {name}({args!r})]")
elif chunk.is_text():
print(chunk.content, end="")
See how-to: stream output for full streaming patterns.
What's next¶
You now know:
- The three tool routes:
builtin.<group>for ready-made tools,@toolfor custom Python functions,MCPClientfor cross-process tools (how-to: use MCP tools). - The agent loop: keep calling
chat()until the model stops calling tools. - How to inspect the message trail.
Up next: Workflows — when you want the orchestration fixed in code rather than directed by the LLM.
See also¶
- How-to: add a custom tool — full
@toolsignature rules - How-to: build an orchestrator — multi-agent pattern when one model dispatches to others