AETIUS

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 canonical type: "GENERATION"; every other span type becomes "SPAN".
  • span_parents (a list in Braintrust's real schema) becomes canonical parent_id, taking the first entry.
  • created becomes canonical start_time.
  • The reconstructed system's agent_name grouping key comes from the root span's own metadata.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:

Python
import agentstage
try:
    env = agentstage.from_braintrust(project_name="demo-braintrust-project")
except Exception as e:
    print(f"{type(e).__name__}: {e}")
Output
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:

Python
from examples.braintrust_fixtures import build_environment
env = build_environment()
print(env.list_systems())
Output
['invoice-agent', 'supervisor']

Reconstructing a system

The single-role case, for comparison against the Langfuse source page's own invoice-agent example:

Python
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()))
Output
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:

Python
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)
Output
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:

Python
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)
Output
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.