Braintrust source
No live Braintrust project was available while writing this page (no BRAINTRUST_API_KEY in this environment), so every example below runs against a local fixture (examples/braintrust_fixtures.py) shaped like a real Braintrust BTQL response instead. That fixture's field names (span_attributes.type, span_attributes.name, span_parents, is_root, metadata, input, output, created) were confirmed against the actual installed braintrust SDK's own source, not assumed from docs. What was not independently re-verified in this repository is the live shape of a real account's data end to end. See agentstage/sources/braintrust.py's module docstring for the exact, itemized list of what's source-verified versus what would still need a live pull to confirm. Treat this page's fixture the same way the Langfuse source page treats its own: real code, run for real, against a stand-in for a project this environment could not reach.
What makes Braintrust different from Langfuse as a source
Langfuse's trace.get() returns one nested object per trace: a trace-level wrapper with an observations list inside it. Braintrust's BTQL has no such wrapper. A project's logs are a single flat stream of spans, and "one trace" is reconstructed by grouping spans that share a root_span_id. Connecting to Braintrust means: list the most recent root spans (is_root = true, sorted by created descending), then for each one, fetch every span sharing its root_span_id.
agentstage.sources.braintrust.normalize_trace() is where that assembly happens, and where the flat span list becomes the same canonical shape agentstage.sources.langfuse.normalize_trace() produces (see the core concepts page and agentstage/sources/base.py):
- A span's
span_attributes.type == "llm"becomes canonicaltype: "GENERATION"; every other span type becomes"SPAN". span_parents(a list in Braintrust's real schema) becomes canonicalparent_id, taking the first entry.createdbecomes canonicalstart_time.- The reconstructed system's
agent_namegrouping key comes from the root span's ownmetadata.agent_name, since there is no separate trace-level object to carry it the way Langfuse's trace wrapper does. One consequence worth knowing: because the root span often tags its own role identity the same way, a single-role system's one role can end up named after that identity ("invoice-agent"below) rather than a generic placeholder, unlike this library's fallback role name for a Langfuse-sourced system with no per-observation role signal at all.
Judge and eval exclusion
Braintrust's own scorer executions carry span_attributes.type == "score" (a real, documented value of the SDK's SpanTypeAttribute enum, not a name-substring guess). Separately, the specific account this port's investigation was run against tags its own judge agent's spans with metadata.agent_name containing "Judge", a real signal for that project, not a Braintrust-wide guarantee. normalize_trace() drops both kinds of span before anything downstream, including the agent_name lookup itself, ever sees them. Unlike Aetius's own reconstruction work, which correctly left Braintrust judge exclusion out of scope entirely, agentstage's reconstruction/tools.py would otherwise fold a scorer's or judge's own tool calls straight into another role's ToolBehaviorProfile, real contamination of exactly the kind this library exists to avoid.
Connecting to a Braintrust project
agentstage.from_braintrust is the real entry point. Called with no credentials available, it fails loudly:
import agentstage
try:
env = agentstage.from_braintrust(project_name="demo-braintrust-project")
except Exception as e:
print(f"{type(e).__name__}: {e}")
RuntimeError: missing required Braintrust credentials: api_key (pass explicitly or set BRAINTRUST_API_KEY)
from_braintrust takes api_key and org_name explicitly, or reads BRAINTRUST_API_KEY / BRAINTRUST_ORG_NAME from the environment. org_name is only needed for an account belonging to more than one org; auth is API-key-only otherwise, the org is auto-discovered from the key. Unlike Langfuse, there is no base-URL credential: the API host comes from the org's own metadata during login. project_name is a human-readable project name, not an opaque id, since Braintrust's own BTQL project_logs() function takes the name directly.
The rest of this page uses the local fixture's Environment instead, built the same way, backed by BraintrustLocalTraceSource instead of BraintrustSource:
from examples.braintrust_fixtures import build_environment
env = build_environment()
print(env.list_systems())
['invoice-agent', 'supervisor']
Reconstructing a system
The single-role case, for comparison against the Langfuse source page's own invoice-agent example:
invoice_system = env.reconstruct("invoice-agent")
print(invoice_system.orchestration.pattern)
print([r.role for r in invoice_system.roles])
role = invoice_system.role("invoice-agent")
print(role.system_prompt_source)
print(role.system_prompt)
print(list(invoice_system.tools.keys()))
single_agent
['invoice-agent']
observed
You are an invoicing support assistant for a Braintrust-logged deployment. Look up invoices accurately.
['lookup_invoice']
The role is named invoice-agent, not the generic single-role fallback name, exactly the naming difference described above: this fixture's root span carries its own metadata.agent_name, so that becomes the resolved role identity as well as the system's grouping key.
A multi-role system whose raw traces also contain a real scorer span and a judge-tagged span, both excluded before reconstruction ever runs:
team_system = env.reconstruct("supervisor")
print(team_system.orchestration.pattern)
print([r.role for r in team_system.roles])
print(list(team_system.tools.keys()))
print("judge_verdict" in team_system.tools)
supervisor = team_system.role("supervisor")
print(supervisor.system_prompt_source)
print(supervisor.system_prompt)
researcher = team_system.role("researcher")
print(researcher.tools)
print(team_system.orchestration.entry_role, team_system.orchestration.terminal_roles)
supervisor_delegates
['researcher', 'supervisor']
['search_docs']
False
observed
You are the support team lead. Delegate research to the researcher and reply with what they find.
['search_docs']
supervisor ['researcher']
team_system.tools holds exactly one tool, search_docs, the researcher's real one. The fixture's raw span list for this trace also contains a score-typed span and a span tagged metadata.agent_name = "Judge Agent" whose own tool call was named judge_verdict. "judge_verdict" in team_system.tools is False. Nothing about either of those spans reached reconstruction at all.
Everything after this point is the same as any other source
Replay matching, ToolSpec, OrchestrationGraph, and every framework adapter work identically on a Braintrust-reconstructed system, because by the time env.reconstruct(...) returns, it's the same canonical shape a Langfuse-reconstructed system is:
from agentstage.replay.matcher import replay_tool_call
lookup_invoice = invoice_system.tools["lookup_invoice"]
result = replay_tool_call("lookup_invoice", {"invoice_id": "inv_3001"}, lookup_invoice.behavior)
print(result)
MatchResult(tool_name='lookup_invoice', arguments={'invoice_id': 'inv_3001'}, source='replay', threshold=0.5, response={'status': 'paid', 'amount': 145.0}, similarity=1.0, closest_candidate=None, closest_similarity=None)
See the core concepts page for what reconstruct() returns in full, how replay matching and its threshold configuration work, and the three framework adapters.