Skip to content

ekon15/braintrust-nested-spans

Repository files navigation

Braintrust Agent Tracing: Nested Spans Solution

Problem: Subagent spans appearing flat instead of nested as children of parent spans in Braintrust.

Solution: Proper OpenTelemetry context management for hierarchical span relationships.


Quick Start

# 1. Set your API key
echo "BRAINTRUST_API_KEY=sk-your-key-here" > .env

# 2. Run the demo (uv handles everything automatically)
uv run test_child_spans.py

# Or use the automated script
./run_demo_uv.sh

First run: Installs dependencies in ~2 seconds View traces: https://www.braintrust.dev


The Problem

When orchestrator agents create subagents, spans often appear flat:

orchestrator
agent.search          ← Should be nested
agent.lineage         ← Should be nested
agent.analysis        ← Should be nested

Instead of the desired nested hierarchy:

orchestrator
  ├── agent.search
  │   └── tool.search_database
  ├── agent.lineage
  │   └── llm.anthropic_call
  └── agent.analysis
      ├── tool.fetch_data
      └── llm.synthesis

The Solution

Use OpenTelemetry context management to create proper parent-child relationships:

from opentelemetry import trace

# Get parent context
parent_context = trace.set_span_in_context(parent_span)

# Create child with explicit parent
with tracer.start_as_current_span("child", context=parent_context):
    # Child work here
    pass

Complete Example

with tracer.start_as_current_span("orchestrator") as orch_span:
    # Capture orchestrator context
    orch_context = trace.set_span_in_context(orch_span)

    # Subagent properly nested under orchestrator
    with tracer.start_as_current_span("agent.search", context=orch_context) as agent_span:
        agent_context = trace.set_span_in_context(agent_span)

        # Tool nested under agent
        with tracer.start_as_current_span("tool.query", context=agent_context):
            # Tool execution
            pass

Production-Ready Wrapper

The BraintrustAgentTracer class provides a clean API:

from anthropic_agent_wrapper import BraintrustAgentTracer

tracer = BraintrustAgentTracer(project_name="my-project")

# Simple, clean nesting
with tracer.orchestrator_span("main_agent") as orch_span:
    with tracer.subagent_span("search", orch_span) as agent_span:
        with tracer.tool_span("query_db", agent_span):
            # Your tool logic
            pass
        with tracer.llm_span(agent_span, model="claude-3-5-sonnet-20241022"):
            # Your LLM call
            pass

Setup Options

Option 1: uv (Recommended - Fast!)

# Install uv (if needed)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Set API key
echo "BRAINTRUST_API_KEY=sk-..." > .env

# Run (uv handles deps automatically!)
uv run test_child_spans.py
uv run anthropic_agent_wrapper.py

Performance: Installs 28 packages in 15ms, ~100x faster than pip

Option 2: pip (Traditional)

# Create virtual environment
python3 -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate

# Install dependencies
pip install -r requirements.txt

# Set API key
echo "BRAINTRUST_API_KEY=sk-..." > .env

# Run demos
python test_child_spans.py
python anthropic_agent_wrapper.py

Get your API key: https://www.braintrust.dev/app/settings


What's Included

Demo Scripts

test_child_spans.py - Comprehensive demonstration:

  • Problem: Flat spans (incorrect nesting)
  • Solution: Nested spans (proper hierarchy)
  • Wrapper: Custom integration example

anthropic_agent_wrapper.py - Production code:

  • BraintrustAgentTracer class (drop-in solution)
  • Complete integration examples
  • Ready for your codebase

Documentation

SPAN_HIERARCHY.md - Visual guide with ASCII diagrams showing flat vs nested spans


OpenTelemetry Attributes

Follow the Gen AI semantic conventions:

# Agent span
span.set_attribute("gen_ai.agent.name", "agent_name")
span.set_attribute("gen_ai.operation.name", "agent")

# Tool span
span.set_attribute("gen_ai.tool.name", "tool_name")
span.set_attribute("gen_ai.operation.name", "tool")

# LLM span
span.set_attribute("gen_ai.system", "anthropic")
span.set_attribute("gen_ai.request.model", "claude-3-5-sonnet-20241022")
span.set_attribute("gen_ai.operation.name", "chat")

Integration with Anthropic Agent SDK

The BraintrustAgentTracer class is designed to work alongside the Anthropic Agent SDK:

from anthropic_agent_wrapper import BraintrustAgentTracer

class MyAgent:
    def __init__(self):
        self.tracer = BraintrustAgentTracer(project_name="my-project")

    def run(self, task):
        with self.tracer.orchestrator_span("main") as orch_span:
            # Run subagents
            result1 = self._run_subagent("search", orch_span, task)
            result2 = self._run_subagent("analyze", orch_span, result1)
            return result2

    def _run_subagent(self, name, parent_span, input_data):
        with self.tracer.subagent_span(name, parent_span) as agent_span:
            # Tool execution
            with self.tracer.tool_span("execute", agent_span):
                result = self._execute_tool(input_data)

            # LLM call
            with self.tracer.llm_span(agent_span):
                response = self._call_llm(result)

            return response

Troubleshooting

"BRAINTRUST_API_KEY not set"

  • Create .env file in the project root
  • Add: BRAINTRUST_API_KEY=sk-...
  • Verify the file is in the same directory as the scripts

"Failed to export spans"

  • Check API key is correct (starts with sk-)
  • Verify internet connection
  • Ensure api.braintrust.dev is accessible

"command not found: uv"

# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# Restart your shell

Spans still appearing flat

  • Verify you're passing the context parameter to start_as_current_span()
  • Use trace.set_span_in_context() to capture parent context
  • Ensure parent span is still active when creating children

Key Concepts

The Golden Rule

Every child span MUST be created with its parent's context.

Three steps:

  1. Capture: parent_context = trace.set_span_in_context(parent_span)
  2. Pass: start_as_current_span("child", context=parent_context)
  3. Repeat: Do this for every level of nesting

Why Context Management Matters

Without explicit context:

# ❌ WRONG: Creates siblings
with tracer.start_as_current_span("parent"):
    pass
with tracer.start_as_current_span("child"):  # Not a child!
    pass

With explicit context:

# ✅ CORRECT: Creates parent-child
with tracer.start_as_current_span("parent") as parent_span:
    parent_ctx = trace.set_span_in_context(parent_span)
    with tracer.start_as_current_span("child", context=parent_ctx):  # Now a child!
        pass

Resources


Python: 3.9+ | Status: Production Ready | Tested: 2026-01-23

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors