Problem: Subagent spans appearing flat instead of nested as children of parent spans in Braintrust.
Solution: Proper OpenTelemetry context management for hierarchical span relationships.
# 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.shFirst run: Installs dependencies in ~2 seconds View traces: https://www.braintrust.dev
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
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
passwith 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
passThe 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# 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.pyPerformance: Installs 28 packages in 15ms, ~100x faster than pip
# 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.pyGet your API key: https://www.braintrust.dev/app/settings
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:
BraintrustAgentTracerclass (drop-in solution)- Complete integration examples
- Ready for your codebase
SPAN_HIERARCHY.md - Visual guide with ASCII diagrams showing flat vs nested spans
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")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- Create
.envfile in the project root - Add:
BRAINTRUST_API_KEY=sk-... - Verify the file is in the same directory as the scripts
- Check API key is correct (starts with
sk-) - Verify internet connection
- Ensure
api.braintrust.devis accessible
# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# Restart your shell- Verify you're passing the
contextparameter tostart_as_current_span() - Use
trace.set_span_in_context()to capture parent context - Ensure parent span is still active when creating children
Every child span MUST be created with its parent's context.
Three steps:
- Capture:
parent_context = trace.set_span_in_context(parent_span) - Pass:
start_as_current_span("child", context=parent_context) - Repeat: Do this for every level of nesting
Without explicit context:
# ❌ WRONG: Creates siblings
with tracer.start_as_current_span("parent"):
pass
with tracer.start_as_current_span("child"): # Not a child!
passWith 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- OpenTelemetry Gen AI Conventions: https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/
- Braintrust OTEL Integration: https://www.braintrust.dev/docs/integrations/sdk-integrations/opentelemetry
- Braintrust Dashboard: https://www.braintrust.dev
Python: 3.9+ | Status: Production Ready | Tested: 2026-01-23