2026-04-20 · 8 min read

Tracing Agno Agents: Observability for Python Multi-Agent Pipelines

Agno (formerly phidata) is a Python-native multi-agent framework built around Agent and Team primitives. When a team routes to the wrong member agent, a tool call fails silently, or an agent run returns a low-quality response, you need trace visibility to diagnose what happened. Here's how to instrument Agno agents and teams with Nexus.

What Agno is

Agno (formerly phidata) is a Python-native framework for building multi-agent systems. Its core abstractions are Agent (a single LLM-powered agent with tools and instructions) and Team (a coordinator that routes tasks across multiple member agents). Agno agents are lightweight by design: you give an agent a model, a list of tools, and instructions — then call agent.run().

That simplicity introduces a few observability blind spots:

Tracing a basic Agno agent run

Install the SDK and wrap your agent calls with Nexus traces:

pip install agno nexus-sdk
import os
import time
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.tools.duckduckgo import DuckDuckGoTools
from nexus_sdk import NexusClient

nexus = NexusClient(api_key=os.environ["NEXUS_API_KEY"])

agent = Agent(
    model=OpenAIChat(id="gpt-4o"),
    tools=[DuckDuckGoTools()],
    instructions="You are a concise research assistant. Summarize findings in 2-3 sentences.",
    markdown=False,
)

def run_with_tracing(query: str, user_id: str) -> str:
    trace = nexus.start_trace({
        "agent_id": "agno-research-agent",
        "name": f"agno: {query[:60]}",
        "status": "running",
        "started_at": nexus.now(),
        "metadata": {
            "user_id": user_id,
            "query": query[:200],
            "environment": os.environ.get("APP_ENV", "dev"),
        },
    })
    trace_id = trace["trace_id"]

    t0 = time.time()
    try:
        response = agent.run(query, stream=False)
        elapsed_ms = int((time.time() - t0) * 1000)

        nexus.end_trace(trace_id, {
            "status": "success",
            "latency_ms": elapsed_ms,
            "metadata": {
                "output_length": len(response.content) if response.content else 0,
            },
        })
        return response.content or ""

    except Exception as e:
        nexus.end_trace(trace_id, {
            "status": "error",
            "latency_ms": int((time.time() - t0) * 1000),
            "error": str(e),
        })
        raise

Each call to run_with_tracing() creates one trace in Nexus. The trace captures the query, user ID, total latency, and success or error status.

Tracing tool calls

Agno tools are Python callables passed to the tools list. The cleanest way to trace them is to wrap each tool function with a span before passing it to the agent:

import os
import time
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from nexus_sdk import NexusClient

nexus = NexusClient(api_key=os.environ["NEXUS_API_KEY"])

def make_traced_search(trace_id: str):
    """Returns a search function that emits a Nexus span on each call."""
    def search_web(query: str) -> str:
        """Search the web for current information about a topic."""
        t0 = time.time()
        try:
            from duckduckgo_search import DDGS
            with DDGS() as ddgs:
                results = list(ddgs.text(query, max_results=5))
            output = "\n".join(r["body"] for r in results)
            nexus.add_span(trace_id, {
                "name": "tool:search_web",
                "status": "success",
                "latency_ms": int((time.time() - t0) * 1000),
                "metadata": {
                    "query": query[:120],
                    "result_count": len(results),
                },
            })
            return output
        except Exception as e:
            nexus.add_span(trace_id, {
                "name": "tool:search_web",
                "status": "error",
                "latency_ms": int((time.time() - t0) * 1000),
                "error": str(e),
            })
            raise
    return search_web

def run_agent_with_tool_tracing(query: str, user_id: str) -> str:
    trace = nexus.start_trace({
        "agent_id": "agno-research-agent",
        "name": f"agno: {query[:60]}",
        "status": "running",
        "started_at": nexus.now(),
        "metadata": {"user_id": user_id, "query": query[:200]},
    })
    trace_id = trace["trace_id"]

    # Build agent with traced tool for this request
    agent = Agent(
        model=OpenAIChat(id="gpt-4o"),
        tools=[make_traced_search(trace_id)],
        instructions="You are a concise research assistant.",
        markdown=False,
    )

    t0 = time.time()
    try:
        response = agent.run(query, stream=False)
        elapsed_ms = int((time.time() - t0) * 1000)
        nexus.end_trace(trace_id, {
            "status": "success",
            "latency_ms": elapsed_ms,
        })
        return response.content or ""
    except Exception as e:
        nexus.end_trace(trace_id, {
            "status": "error",
            "latency_ms": int((time.time() - t0) * 1000),
            "error": str(e),
        })
        raise

Building the agent per request (with the traced tool injected) is the reliable pattern — it ties each tool call span back to its parent trace without needing global state.

Tracing Agno Team hierarchies

Agno’s Team class lets you compose multiple agents under a coordinator. The coordinator decides which member agent handles each subtask. To trace this hierarchy, emit one span per member agent invocation alongside the parent trace:

import os
import time
from agno.agent import Agent
from agno.team.team import Team
from agno.models.openai import OpenAIChat
from agno.tools.duckduckgo import DuckDuckGoTools
from agno.tools.yfinance import YFinanceTools
from nexus_sdk import NexusClient

nexus = NexusClient(api_key=os.environ["NEXUS_API_KEY"])

def run_team_with_tracing(task: str, user_id: str) -> str:
    trace = nexus.start_trace({
        "agent_id": "agno-research-team",
        "name": f"team: {task[:60]}",
        "status": "running",
        "started_at": nexus.now(),
        "metadata": {
            "user_id": user_id,
            "task": task[:200],
            "team_size": 2,
        },
    })
    trace_id = trace["trace_id"]

    def make_web_agent():
        def search(query: str) -> str:
            """Search the web for recent news and information."""
            t0 = time.time()
            try:
                from duckduckgo_search import DDGS
                with DDGS() as ddgs:
                    results = list(ddgs.text(query, max_results=5))
                output = "\n".join(r["body"] for r in results)
                nexus.add_span(trace_id, {
                    "name": "agent:WebSearchAgent:tool:search",
                    "status": "success",
                    "latency_ms": int((time.time() - t0) * 1000),
                    "metadata": {"query": query[:120], "result_count": len(results)},
                })
                return output
            except Exception as e:
                nexus.add_span(trace_id, {
                    "name": "agent:WebSearchAgent:tool:search",
                    "status": "error",
                    "latency_ms": int((time.time() - t0) * 1000),
                    "error": str(e),
                })
                raise

        return Agent(
            name="WebSearchAgent",
            model=OpenAIChat(id="gpt-4o"),
            tools=[search],
            instructions="Search the web for relevant, up-to-date information.",
            markdown=False,
        )

    def make_analyst_agent():
        return Agent(
            name="AnalystAgent",
            model=OpenAIChat(id="gpt-4o"),
            tools=[YFinanceTools(stock_price=True, company_news=True)],
            instructions="Analyze data and provide structured insights.",
            markdown=False,
        )

    team = Team(
        name="ResearchTeam",
        agents=[make_web_agent(), make_analyst_agent()],
        model=OpenAIChat(id="gpt-4o"),
        instructions="Coordinate agents to research and analyze. Delegate to the right agent for each subtask.",
        markdown=False,
    )

    t0 = time.time()
    t_coord = time.time()
    try:
        response = team.run(task, stream=False)
        elapsed_ms = int((time.time() - t0) * 1000)
        coord_ms = int((time.time() - t_coord) * 1000)

        nexus.add_span(trace_id, {
            "name": "coordinator:route_and_aggregate",
            "status": "success",
            "latency_ms": coord_ms,
            "metadata": {
                "output_length": len(response.content) if response.content else 0,
            },
        })
        nexus.end_trace(trace_id, {
            "status": "success",
            "latency_ms": elapsed_ms,
        })
        return response.content or ""

    except Exception as e:
        nexus.end_trace(trace_id, {
            "status": "error",
            "latency_ms": int((time.time() - t0) * 1000),
            "error": str(e),
        })
        raise

With this pattern, each trace shows the full team run in the top-level span plus individual tool call spans named agent:WebSearchAgent:tool:search. That naming convention makes it easy to filter by agent or tool name in the Nexus UI.

Async agents

Agno supports async execution via agent.arun(). The tracing pattern is the same — just use await:

import asyncio
import os
import time
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from nexus_sdk import NexusClient

nexus = NexusClient(api_key=os.environ["NEXUS_API_KEY"])

agent = Agent(
    model=OpenAIChat(id="gpt-4o"),
    instructions="You are a concise assistant.",
    markdown=False,
)

async def run_async_with_tracing(query: str, user_id: str) -> str:
    trace = nexus.start_trace({
        "agent_id": "agno-async-agent",
        "name": f"agno-async: {query[:60]}",
        "status": "running",
        "started_at": nexus.now(),
        "metadata": {"user_id": user_id, "query": query[:200]},
    })
    trace_id = trace["trace_id"]

    t0 = time.time()
    try:
        response = await agent.arun(query, stream=False)
        elapsed_ms = int((time.time() - t0) * 1000)
        nexus.end_trace(trace_id, {
            "status": "success",
            "latency_ms": elapsed_ms,
            "metadata": {
                "output_length": len(response.content) if response.content else 0,
            },
        })
        return response.content or ""
    except Exception as e:
        nexus.end_trace(trace_id, {
            "status": "error",
            "latency_ms": int((time.time() - t0) * 1000),
            "error": str(e),
        })
        raise

What to watch for in production

Once traces are flowing from your Agno agents, three failure patterns appear most often:

Next steps

Agno is gaining adoption quickly as a lightweight alternative to heavier agent frameworks — its Team primitive in particular makes it easy to compose specialist agents without complex orchestration logic. The tracing approach above works with any Agno model provider (OpenAI, Anthropic, Groq, Gemini) since it wraps the run boundary rather than patching internal SDK calls. Sign up for a free Nexus account to start capturing traces from your Agno agents today.

Add observability to Agno agents

Free tier, no credit card required. Full trace visibility in under 5 minutes.