Chapter 19
Week 5.4: Agno
Welcome to Week 5 - Agent Frameworks
Day 3: Agno
Same week, same five steps, a new framework. The whole idea this week is that once you know one agent framework, you basically know them all, so every day we build the same agent the same five steps and watch the idioms rhyme:
- Create the agent - give it a model and a system prompt.
- Run it - send a message, get a reply.
- Add tools - plain typed functions the agent can call.
- Add MCP - connect a tool server someone else wrote, wired the same way every time.
- Put it in a loop with a goal - hand it an objective and let it work, step after step, until the job is done.
Steps 1 and 2 are still just an LLM call. Tools and MCP give the agent things it can do. Step 5 is where it becomes an agent: the framework runs the loop on its own, choosing a tool, reading the result, choosing again, until the goal is met.
The running project is the same SQLite todo board from Day 1. A worker picks one goal off the board, plans its own steps, does the work with its agent loop, and ticks each step off. The board code (board.py) is the same file, byte for byte; only the framework around it changes.
Today is Agno, from the team formerly behind Phidata, who call it "the programming language for agentic software." Two things make it stand out. It is strikingly fast and lean to instantiate, and the same agent you write here drops unchanged into AgentOS, its own FastAPI runtime with sessions, tracing and a control-plane UI. We use the plain SDK and clock the instantiation speed at the end. Notice how little changes from the other frameworks; that is the point.
Setup
Two things this day needs, both already in place from earlier weeks:
- Node, for
npx(the filesystem MCP server runs over it). Check withnode --version. OPENAI_API_KEYin the repo-root.env. Today's frameworks use OpenAI'sgpt-5.4-mini, and that key has been in your.envsince Week 1.
Agno is part of the repo's environment, so a normal uv sync from the repo root installs everything. Open this notebook in Cursor and pick the repo's default Python 3.12.12 kernel, the same one you use every week, then run the cells top to bottom.
Warm the filesystem MCP server once now so the first run is fast, then stop it with Ctrl-C as soon as it prints that it is running:
npx -y @modelcontextprotocol/server-filesystem .import functools
import subprocess
from pathlib import Path
from dotenv import load_dotenv
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.tools.mcp import MCPTools
from mcp import StdioServerParameters
load_dotenv(override=True)Step 1: Create the agent
In Agno an agent is an Agent: a model plus instructions, its system prompt. We build the model once as an OpenAIChat pointed at gpt-5.4-mini, reading OPENAI_API_KEY from the environment, and reuse it. Agno has a synchronous path too, but because connecting MCP later makes the agent async, we await it throughout with arun so the whole notebook reads the same way.
MODEL = "gpt-5.4-mini"
model = OpenAIChat(id=MODEL)
agent = Agent(
model=model,
instructions="You are a concise, friendly assistant. Reply in a single short sentence.",
)Step 2: Run it
Send a message, await the reply, and print the result's .content. With no tools yet there is nothing to loop over, so the agent just answers. This is still only an LLM call.
result = await agent.arun(input="Say hello in Spanish.")
print(result.content)Our project this week: a SQLite todo board
The worker coordinates through the same tiny SQLite board as Day 1, the same board.py file: one file, one table, no server to run. A worker is handed one goal; to reach it, it writes its own step todos under that goal, ticks each one off as it goes, and closes the goal at the end. Under the hood the board is just a list of dicts (a goal has parent_id of None, a step points at its goal):
import board
board.reset_board()
board.add_goal("Read notes.txt, translate its contents into natural Spanish, and write the Spanish to spanish.txt.")
board.list_todos()show_board() prints that same data nicely, in the rich style from Week 1: each goal with its steps indented beneath, done tasks struck through in green and the one in progress in yellow. There are no steps yet; the worker writes its own when it plans.
board.show_board()Step 3: Add tools
A tool in Agno is a plain typed Python function with a docstring. Agno reads the type hints and the docstring and builds the schema for you, so there is nothing else to declare; an optional @tool decorator exists only for extras like caching. You hand the functions to the agent in its tools=[...] list.
We write three small board tools: show_todos to read the board, plan_steps to break a goal into steps, and complete_task to mark a todo done. Here we give a quick agent just two of them and ask what is on the board; watch it decide, on its own, to call show_todos before it answers. That decide, call, read, answer is the agent loop starting to turn. All three tools come together in step 5.
def show_todos() -> list[dict]:
"""List every todo on the board. A goal has parent_id None; a step has parent_id set to its goal's id."""
return board.list_todos()
def plan_steps(goal_id: int, steps: list[str]) -> dict:
"""Break a goal into an ordered checklist of steps on the board. Pass the goal's id and a short list of step descriptions."""
return {"goal_id": goal_id, "step_ids": [board.add_step(goal_id, step) for step in steps]}
def complete_task(task_id: int, result: str) -> dict:
"""Mark a todo (a step or the goal) with this id as done and record a short result summary."""
board.complete_todo(task_id, result)
return {"task_id": task_id, "status": "done"}board_agent = Agent(
model=model,
instructions="You help manage a shared todo board.",
tools=[show_todos, complete_task],
)result = await board_agent.arun(input="What is on the board right now, and what is its status?")
print(result.content)Step 4: Add MCP
MCP is just more tools: ones you did not write, connected over a small protocol. We give the agent the filesystem reference server, the same Node server every framework uses this week, scoped to a single workspace folder so it can only touch files in there. In Agno an MCP server is an MCPTools, an async context manager you open with async with and pass to the agent in the same tools=[...] list. We build it from StdioServerParameters so we can set the server's working directory (cwd) to the workspace, which makes the agent's file names resolve there.
Agno does not expose the server's stderr, so the first line of the next cell points its stdio client at the null device. That quiets the server's startup banner and lets it run from a Jupyter kernel on Windows, where the kernel's stderr has no real file descriptor. On Mac and Linux it simply keeps the output clean.
# Agno does not expose the MCP server's stderr, so point its stdio client at DEVNULL.
import agno.tools.mcp.mcp as agno_mcp
agno_mcp.stdio_client = functools.partial(agno_mcp.stdio_client, errlog=subprocess.DEVNULL)
workspace = Path("workspace").resolve() # the only folder the agent may touch
server = StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem", str(workspace)],
cwd=str(workspace),
)async with MCPTools(server_params=server, timeout_seconds=60) as filesystem:
file_agent = Agent(
model=model,
instructions="You can read and write files in your workspace. Use your tools to do what is asked.",
tools=[filesystem],
)
result = await file_agent.arun(input="Read notes.txt and summarize it in one short sentence.")
print(result.content)Step 5: Put it in a loop with a goal
Now the payoff. Give one agent all three board tools and the filesystem server, hand it the goal, and let it run. It plans its own steps on the board, works them with its file tools, ticks each one off, and closes the goal when the work is done. That is the agent loop running on its own: read, plan, act, check off, repeat. Watch the board fill with steps and then strike them through.
INSTRUCTIONS = """
You are a careful worker with a shared todo board and a set of file tools.
Take the pending goal and see it through. Begin by laying out a short plan: the handful of concrete steps the work itself breaks down into, added to the board under the goal. Then carry them out with your file tools, marking each step done as you finish it. Once the steps are all done, close the goal. Your files live in the single folder your tools are allowed to use.
"""
board.reset_board()
goal_id = board.add_goal("Read notes.txt, translate its contents into natural Spanish, and write the Spanish to spanish.txt.")
board.claim_todo(goal_id)
async with MCPTools(server_params=server, timeout_seconds=60) as filesystem:
worker = Agent(
model=model,
instructions=INSTRUCTIONS,
tools=[show_todos, plan_steps, complete_task, filesystem],
)
await worker.arun(input="Please work the pending goal on the board.")
board.show_board()Run the same worker from the terminal
Everything you just watched in step 5 is also packaged as a small script, agno_worker.py, sitting next to this notebook. It seeds the same goal, builds the same agent with the same three board tools and the filesystem MCP server, and runs the same loop, only from the command line instead of the kernel. Open a terminal in this folder and run it:
uv run agno_worker.pyYou will see the agent plan its steps, work the goal and tick each one off, then the finished board and the Spanish it wrote. This is the shape every worker takes on Day 5, where a Google ADK orchestrator launches one of these per framework as a parallel subprocess against a single shared board.
The one cool thing: how fast it starts
Agno's signature claim is speed. Creating an agent is close to free, which matters when a server spins up a fresh agent per request or a team spawns many at once. Let's just measure it: build a batch of agents and time it.
(your numbers will vary, but each agent lands in the low microseconds)import time
start = time.perf_counter()
agents = [Agent(model=model, tools=[show_todos]) for _ in range(1000)]
elapsed = time.perf_counter() - start
print(f"Built {len(agents)} agents in {elapsed * 1000:.1f} ms")
print(f"That is about {elapsed / len(agents) * 1_000_000:.1f} microseconds each.")The other half of the story is AgentOS: a handful of lines wraps your agents in a FastAPI app with sessions, streaming, tracing and a control-plane UI, so the same agent you just built can be served in production unchanged. And the same one-line swap as the rest of the week: Agno talks to any OpenAI-compatible endpoint through OpenAILike(id=..., base_url=..., api_key=...), with the tools, the MCP server and the board all unchanged.
