Skip to content

Agent

AG2 ships a TelemetryMiddleware that emits OpenTelemetry spans for a single agent's work — its turns, LLM calls, tool executions, and human-in-the-loop interactions.

The middleware follows the OpenTelemetry GenAI Semantic Conventions, so traces can be exported to any compatible backend — Jaeger, Grafana Tempo, Datadog, Honeycomb, Langfuse, Arize AX, Arize Phoenix, and others.

Tip

To trace the interactions between agents in a network — the hub's dispatch, channels, agent lifecycles, and tasks — see Network Telemetry. The two compose: an agent's invoke_agent span links back to the network envelope that triggered it.

Installation#

pip install "ag2[openai,tracing]"

Quick Start#

from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor, ConsoleSpanExporter

from ag2 import Agent
from ag2.config import OpenAIConfig
from ag2.middleware.builtin import TelemetryMiddleware

# 1. Configure OpenTelemetry
resource = Resource.create(attributes={"service.name": "ag2-quickstart"})
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(tracer_provider)

# 2. Create agent with telemetry middleware
agent = Agent(
    "assistant",
    prompt="You are a helpful assistant.",
    config=OpenAIConfig(model="gpt-4o-mini"),
    middleware=[
        TelemetryMiddleware(
            tracer_provider=tracer_provider,
            agent_name="assistant",
        ),
    ],
)

# 3. Run -- spans are emitted automatically
import asyncio
reply = asyncio.run(agent.ask("What is the capital of France?"))

Trace Hierarchy#

Each ask() call produces a root span with child spans for LLM calls, tool executions, and human input:

invoke_agent assistant
  |-- chat gpt-4o-mini              # LLM API call
  |-- execute_tool get_weather      # tool execution
  |-- chat gpt-4o-mini              # LLM call after tool result
  +-- await_human_input assistant   # human-in-the-loop

Span Types#

Every span includes an ag2.span.type attribute:

ag2.span.type Operation name Triggered by
agent invoke_agent on_turn -- wraps the full agent turn
llm chat on_llm_call -- each LLM API call
tool execute_tool on_tool_execution -- each tool invocation
human_input await_human_input on_human_input -- human-in-the-loop

Semantic Attributes#

Spans carry standard OpenTelemetry GenAI attributes:

Attribute Span types Description
gen_ai.operation.name All Operation: invoke_agent, chat, execute_tool, await_human_input
gen_ai.agent.name agent, human_input Agent name
gen_ai.provider.name agent, llm LLM provider (e.g. openai, anthropic) -- auto-detected
gen_ai.request.model agent, llm Model name (e.g. gpt-4o-mini) -- auto-detected
gen_ai.response.model llm Resolved model name from response
gen_ai.response.finish_reasons llm Finish reasons (e.g. ["stop"], ["tool_calls"])
gen_ai.usage.input_tokens llm Prompt token count
gen_ai.usage.output_tokens llm Completion token count
gen_ai.usage.cache_creation_input_tokens llm Tokens used to create prompt cache (Anthropic)
gen_ai.usage.cache_read_input_tokens llm Tokens read from prompt cache (Anthropic, OpenAI, Gemini)
gen_ai.tool.name tool Tool function name
gen_ai.tool.call.id tool Tool call ID
gen_ai.tool.type tool Tool type (always function)

Content Capture#

By default, message content, tool arguments, and results are included in spans. To disable content capture for privacy-sensitive environments:

1
2
3
4
5
TelemetryMiddleware(
    tracer_provider=tracer_provider,
    agent_name="assistant",
    capture_content=False,  # omits messages, tool args, and results
)

When content capture is enabled (the default), spans include these additional attributes:

Attribute Span type Content
gen_ai.input.messages llm JSON request messages (see below)
gen_ai.output.messages llm JSON response messages (see below)
gen_ai.tool.call.arguments tool Tool call arguments (JSON)
gen_ai.tool.call.result tool Tool execution result (see below)
ag2.tool.call.result.truncated tool Present and true when the result was truncated
ag2.human_input.prompt human_input Prompt shown to human
ag2.human_input.response human_input Human's response

gen_ai.input.messages is the conversation history AG2 assembled for that call, not just the latest user message, serialised as OpenAI-style chat messages: user for text inputs, assistant for earlier model replies (with tool_calls when the model requested tools), tool with a tool_call_id for each tool result, and a user turn carrying the summary once history has been compacted. Tool results render the same way as gen_ai.tool.call.result below, including the max_tool_result_chars cap. Non-text user inputs (images, audio, documents) are omitted.

The history is recorded as AG2 assembled it, not as a byte-exact copy of the provider payload. Each provider repairs a degraded history differently — after a partial write or a compaction that splits a tool turn, one may drop a tool call whose result is missing while another keeps it — so the span may differ from the request in those cases. It reports what AG2 held, which is what you need to diagnose the degradation itself.

gen_ai.output.messages is the model's reply for that call, recorded whether the model answered with text, tool calls, or both. On a tool-calling turn the first chat span's output therefore carries the tool call, and the next chat span's input carries that tool call together with its result.

The tool result is always recorded as a string:

  • text results verbatim,
  • structured results (dict, Pydantic model, list) as JSON,
  • parts given by URL as the URL,
  • multi-part results as the above joined with newlines.

Binary and provider-file parts (inline images, audio, documents) have no text and are omitted.

Results longer than max_tool_result_chars (default 8192) end with ...[truncated] and carry ag2.tool.call.result.truncated; traces reconstructed by ag2.eval expose that flag as metadata["truncated"] on the result's text part. Raise the cap per middleware instance to keep large structured results parseable, or pass None to disable truncation (large results may then exceed your backend's attribute or payload limits):

TelemetryMiddleware(capture_content=True, max_tool_result_chars=32_000)

Warning

With capture_content=True, message content, tool arguments, and human input will appear in your tracing backend. Ensure your backend has appropriate access controls.

Custom Span Attributes#

Use span_attributes to stamp custom key-value pairs onto spans the middleware emits. This is useful for routing or filtering traces by tenant, environment, deployment, or any other label your backend supports. Every span will carry these attributes.

1
2
3
4
5
6
7
8
TelemetryMiddleware(
    tracer_provider=tracer_provider,
    agent_name="assistant",
    span_attributes={
        "deployment": "production",
        "ag2.org.id": "org-abc123",
    },
)

Tip

This is the right place to add tenant or organization identifiers when your tracing backend filters traces by span-level labels (for example, Google Cloud Trace label filters or Datadog tags).

Note

If a key in span_attributes collides with an intrinsic attribute set by the middleware (such as ag2.span.type or gen_ai.usage.input_tokens), the middleware's value always wins.

Configuration#

TelemetryMiddleware accepts:

Parameter Type Default Description
tracer_provider TracerProvider \| None Global provider OpenTelemetry TracerProvider
capture_content bool True Include message/tool content in spans
agent_name str \| None "unknown" Agent name for span attributes
provider_name str \| None None LLM provider name (auto-detected from response if not set)
model_name str \| None None Model name (auto-detected from response if not set)
span_attributes dict[str, str] \| None None Extra key-value pairs stamped onto every span (tenant IDs, environment tags, etc.)

Tool Execution Example#

from ag2 import Agent
from ag2.config import OpenAIConfig
from ag2.middleware.builtin import TelemetryMiddleware
from ag2.tools import tool

@tool
def get_weather(city: str) -> str:
    """Get weather information for a city."""
    return f"Sunny, 72F in {city}"

agent = Agent(
    "weather_agent",
    prompt="Use the get_weather tool to answer weather questions.",
    config=OpenAIConfig(model="gpt-4o-mini"),
    tools=[get_weather],
    middleware=[
        TelemetryMiddleware(
            tracer_provider=tracer_provider,
            agent_name="weather_agent",
        ),
    ],
)

Backend Integration#

Since TelemetryMiddleware emits standard OpenTelemetry spans, any OTLP-compatible backend works — Grafana Tempo, Jaeger, Datadog, Honeycomb, Langfuse, Arize AX, Arize Phoenix, and others. Swap the ConsoleSpanExporter from the Quick Start above for an OTLPSpanExporter pointed at your backend (or a local OpenTelemetry Collector); nothing else about the instrumentation changes.

For Arize users, Arize AX provides a managed production path for tracing, evaluating, and monitoring AG2 agents, while Arize Phoenix is the open-source path for local development or self-hosted inspection. The same traces can support follow-on workflows such as agent evaluation and LLM evaluation.