Your first agent
A tool, an agent, a reply — in about ten lines.
A tool is a decorated async function. An agent is a prompt, a model and a list of tools. There is no third concept.
import httpx
from agentino import Agent, tool
@tool
async def failed_runs(since_hours: int = 24) -> str:
"""Jobs that failed in the last N hours, newest first."""
async with httpx.AsyncClient(timeout=10) as http:
r = await http.get(f"{CI}/api/runs", params={"status": "failed", "since_hours": since_hours})
r.raise_for_status()
return r.text
agent = Agent(
instructions="You are the on-call desk. Lead with the answer, then the evidence.",
tools=[failed_runs],
)
print(await agent.run("Anything break overnight?"))
# → "Three failures, all in the nightly export — a statement timeout at 02:14,
# 02:47 and 03:31. Everything else passed."
That is the whole API. The framework handles the model round-trip, tool dispatch, retries and pulling the final text out of the response.
What the decorator reads
@tool builds the schema the model sees from the function itself — nothing is
declared twice:
- the function name becomes the tool name
- the docstring becomes the description, and its
Args:section describes each parameter - the type hints become the JSON schema
So the function above is offered to the model as
failed_runs(since_hours: integer) with the description "Jobs that failed
in the last N hours, newest first." Rename the function and the tool renames
itself.
What happens on .run()
- Your instructions and the user's message go to the model along with the tool schemas.
- If the model asks for a tool, the runtime calls it, appends the result, and goes back to the model.
- That repeats until the model answers in prose or
max_turnsis reached. - You get the final text.
Failures inside a tool come back to the model as an error result rather than raising, so one broken lookup does not end the turn.
Synchronous callers
The core is async all the way down, and there is deliberately no sync wrapper — one would either block an event loop that is already running or hide the fact that a turn takes seconds. From synchronous code, drive it yourself:
import asyncio
print(asyncio.run(agent.run("Anything break overnight?")))
Runner is the higher-level entry point when you are working from a config
file rather than an Agent you built in Python:
from agentino import Runner, load_config
runner = Runner(load_config("agents.yml"))
print(await runner.one_shot("Review PR #42"))
load_agents is the same loader when you want the agents themselves rather
than a runner around them.