bitfab-py (imported as bitfab). Python ≥ 3.10.
Module Exports
openai-agents. The LangGraph and LangChain adapters need langchain-core or langgraph. The Claude Agent SDK adapter needs claude-agent-sdk.
Type Aliases
class Bitfab
__init__
Client properties
api_key resolves the configured string or callable. It does not apply the BITFAB_API_KEY fallback, and it never warns. capture_enabled is the effective state, so it resolves the fallback key too, the same way the first traced call would. Under strict=True, reading capture_enabled without a key raises RuntimeError. enabled warns once and returns the same value as capture_enabled.
Client environment variables
Transport environment variables
Commit ref environment variables
span
- When
capture_enabled=False, the wrapper runs the function without recording, except inside a replay item namedefaults to the function’s qualified name:Order.processfor a method,processfor a plain function or a closure (the enclosing function is dropped, since it is already the parent in the tree). If the function has no name, for example a lambda, it falls back totrace_function_key. The raw__name__still travels separately asfunction_namecapture_when="nested"records the span only when another Bitfab span is active. Without a parent, the decorated function runs normally and does not create a root trace. The default is"always". An unknown value warns once and falls back to that default- Nested spans propagate via
contextvars.ContextVar. This is safe acrossasyncio.gather, threads, and sync/async boundaries - Spans exist only where you create them. Nesting between them is automatic, but only among spans that exist. One wrapper around just the outermost function records a single-node trace. See Instrumentation
span()is the opt-in tracing surface.trace()andnode()are the opt-out surface. Aspan()entered beneath an activetrace()raisesMixedTracingError, aRuntimeErrorsubclass. Configure a function inside a subtree withnode()instead- Exceptions are recorded on the span, then re-raised
experiment_idis rarely set directly.replay()injects it through a replay context.test_run_idis a deprecated alias for it, and passing both with different values raisesValueErrorfinalizerecords a serializable view of a streaming result as the span output. The raw result is always returned to the caller unchanged. On an async generator,finalizereceives the list of yielded chunks. That list is assembled after iteration, once the caller has already streamed every chunk, so this is non-blocking and non-destructive. On a plain sync or async function,finalizereceives the return value instead and runs inline before the span is recorded, so a live single-consumer stream is blocked on and consumed. Prefer an async generator for streaming to avoid that.finalizemay beasyncon async or async-generator spans, and must be sync on sync spans. Afinalizethat raises records an error instead of crashing. Pair it withfinalizers.openai_chunksorfinalizers.anthropic_events. See Tracing streaming functions
trace
get_function(key).trace(...) are supported, and take the same options. The guides lead with the bound handle, which binds the key once for the root and its framework handlers. This form takes the key inline.
Returns: a decorator that returns a function with the same signature.
Semantics:
- The root span behaves exactly as
span(). Descendant spans are typed"function". They are named by their qualified name, for exampleOrder.process, with any<locals>.prefix stripped - First-party means the package directory containing the decorated function. That directory is found by walking up while
__init__.pyexists. Standard library code, site-packages, and Bitfab’s own code never record spans - Capture is scoped to the traced call.
sys.monitoringevents are installed on entry and removed on exit. Code outside a traced call is unaffected - Lambdas, generator expressions, comprehensions, module-level (
<module>) frames, and decorator wrappers are skipped. A decorator wrapper here means a function taking only*args, **kwargs.include_wrappers=Truerecords wrappers instead of skipping them - Skipping is transparent. A skipped wrapper never becomes a parent. The function it wraps parents to the real caller instead
- There is no cap on the total number of spans one trace root sends.
max_spanswas removed, and passing it raisesTypeError max_captured_subtree_spans(default 500) caps discovered calls sent with their inputs and outputs. It does not count declared nodes, nested trace roots, framework integration spans, or spans the loaded sim plan turned capture off for, so those keep recording past it. Spans sent without inputs and outputs only because the sim plan could not be read still count toward it. Past it, further discovered calls with capture on record nothingmax_depth(unbounded by default) caps how deep discovery goes and never applies to a declaration. Exceeding any bound stops recording without affecting the call itself, and logs a one-time warning per traced function, so a truncated trace is not mistaken for code that never ranget_function(key).trace(...)takes the samemax_captured_subtree_spansandmax_depthand resolves them the same waymax_captured_subtree_spansfalls back toBitfab(max_captured_subtree_spans=...), then to theBITFAB_MAX_CAPTURED_SUBTREE_SPANSenvironment variable, then to 500- The first root call that starts while the client’s first sim plan read is still running waits for that read, for at most five seconds. An async root waits without blocking the event loop, and a synchronous root called on an event loop thread does not wait at all (its spans are held until the read finishes). Once the first read has finished, whether it worked or not, no later root waits, and nested roots and child spans never wait
- Free-threaded builds warn once. Subtree capture is unverified on free-threaded builds. The root span is unaffected either way
self/clsare stripped from recorded inputs. Exceptions are recorded on the failing frame usingPY_UNWIND, then re-raised- Sync, async, and async-generator roots are all supported. Suspension does not close a span. A coroutine’s span spans its full lifetime
- On Python 3.11 and earlier, the root span is still recorded. A one-time warning explains that descendants were skipped
- A nested
trace()root starts its own independent trace. Its complete subtree also appears in every outertrace()capture. Each copy has separate span IDs, and the same structure the decorator would record alone. Every copy of a span also carries the contexts and prompt the call added, so each trace’s model and token totals include the calls that ran inside it. Two active roots double the span volume in the region they share. Anode()and the LangGraph integration’s spans keep their configured name, type,experiment_id, and finalized output in every enclosing copy, withfinalizerunning once; framework handler spans attach inside the innermost trace only. The outer trace’s span for the nested root carriesnested_trace_id,nested_trace_function_key, andnested_root_span_id, and the nested root span carriesenclosing_trace_id,enclosing_span_id, andenclosing_trace_function_key. Inside a replay item orseed_tracea nested root starts no trace of its own; the item’s trace records it as an ordinary descendant trace()andnode()are the opt-out tracing surface.span()is the opt-in surface. Atrace()root entered beneath an activespan()raisesMixedTracingError. The reverse raises too. Aspan()entered beneath atrace()also raises. The check only runs while tracing is active, meaning capture is on or the call is inside a replay item. The root span thatreplay("key", fn)wraps around an undecorated callable belongs to neither surface. Atrace()root called from that callable nests beneath it instead- Work dispatched to
ThreadPoolExecutor/threading.Threadis not captured, unless the callable is submitted throughcontextvars.copy_context().run, which carries the trace into the worker. An abandoned generator records no span - Async-generator capture is released between yielded items. This holds even when a caller stops consuming without calling
aclose() - Automatically discovered descendants are not replay mock targets unless configured with
node(mock_on_replay=True). Usenode()when a descendant needs trace-owned capture policy mock_on_replay_default=Truemakes replay mocking the default for configurednode()descendants undermock="marked". A node withmock_on_replay=Falseoverrides it. The trace option defaults toFalse. Unconfigured descendants remain non-mockable, because interpreter monitoring cannot short-circuit them
node
@client.trace. Its options intentionally mirror the applicable span() options. node() never creates a span or trace by itself. It inherits the enclosing trace function key.
Semantics:
- Outside an active
trace()call, the function runs normally with no capture or replay behavior. This does not hold when aspan()is active.node()belongs to the opt-out surface, so entering it beneath an opt-inspan()raisesMixedTracingError capture=True(default) lets the enclosing trace capture the node once, with the requestedname,type,experiment_id, andfinalizebehaviorcapture=Falseomits the node. Captured descendants attach to its nearest captured parenttypedefaults to"custom". On a generated BAML client method (b.ClassifyText) or awrap_bamlwrapper it defaults to"llm", and the node is that BAML call’s only span, carrying its prompt and usage. See BAML under a trace rootmock_on_replay=Truemakes a captured, configured descendant return its recorded output under the defaultmock="marked"strategy.Noneinherits the enclosing trace’smock_on_replay_default.Falseoverrides that default. Replaymock="all",mock="none", and mock overrides all behave exactly as they do forspan()capture=Falsewithmock_on_replay=TrueraisesValueError, because an uncaptured node has no recorded outputvarianttells apart calls sharing one span name, such as one function running several times at once on a thread pool. Pass a non-empty string, or a function that receives the call’s arguments (includingselffor a method) and returns a string orNone. Anything else raisesValueError. The function runs before each call, and only inside an activetrace(). If it raises or returns anything other than a non-empty string orNone, a one-time warning is logged and that call records no variant. The call itself always runs- The variant is sent as
varianton the span and kept in every enclosing trace’s copy. Replay matches a recorded output by name and variant before call order. A mocked call beneath a node with a variant only matches recordings beneath the same name and variant. Functions in between that set no variant do not affect matching, and recordings with no variants match exactly as before - Sync, async, and async-generator functions follow the corresponding
span()behavior. This includes the existing async-generator replay-mocking limitation - A function that is a
trace()root and also hasnode()settings, stacked in either order, records one span per root call: the root span, with the root’s own name and type. Inside another trace it appears once, as the linked copy of the nested trace. The node settings apply when the same function is reached through a separatenode()reference beneath a different root - A traced root, or a node, skips only its own call once. When its body calls its own undecorated function again, each of those calls is recorded, so recursion shows every level
- Requires the Python 3.12+ subtree capture used by
trace(). On earlier versions, the root still records. Configured descendants run normally and untraced
get_function
wrap_baml
Framework integration → see BAML framework guide for examples.
async wrapper with the same signature as method, plus a .collector attribute holding the most recent call’s BAML Collector (None before the first call or if baml-py is not installed). Pass on_collector to receive the Collector after each call.
Raises:
ValueErrorif form 1 is used withoutbaml_clientin constructorValueErrorif the method has no__name__
- If
baml-pyis not installed, the method is called directly without instrumentation - Otherwise calls
get_current_span().set_prompt(...)andget_current_span().add_context({...})with extracted metadata - Beneath a
trace()root the BAML call is already recorded as its ownllmspan, so the prompt and metadata land on that span and the enclosing span is left alone. See BAML under a trace root - Must be invoked inside a
@span-decorated function for the prompt/context to attach to anything
wrap_jev
Framework integration → see Jev framework guide for examples.
model, provider (typesafe), inputTokens, outputTokens, and durationMs into the span that is current when the call starts. fn may be sync or async; the wrapper matches it. Opens no span of its own. Beneath a trace() root, a call through typesafe_sdk is already recorded as its own llm span, so the wrapper records nothing more and node() on it configures that span. Also available on BitfabFunction with the same signature.
Raises: ValueError if fn has no __name__.
wrap_openrouter
Framework integration → see OpenRouter framework guide for examples.
provider (openrouter), model (the model OpenRouter routed to), upstreamProvider, inputTokens, outputTokens, cachedInputTokens, reasoningTokens, costUsd (OpenRouter’s usage.cost), and durationMs, into the span that is current when the call starts. Keys the response omits are left out. fn may be sync or async (such as llm.chat.completions.create); the wrapper matches it. Opens no span of its own. Beneath a trace() root, a chat.completions.create call on an openai client whose base URL is openrouter.ai is already recorded as its own llm span named Completions.create (or AsyncCompletions.create), so the wrapper records nothing more and node() on it configures that span. A call the OpenAI Agents SDK integration or the LangChain callback handler is already recording is not recorded a second time, and stream=True calls are not recorded. Also available on BitfabFunction with the same signature.
Raises: ValueError if fn has no __name__.
wrap_anthropic
Framework integration → see Anthropic framework guide for examples.
system and messages as the prompt, plus provider (anthropic), model, inputTokens (every prompt token, cache reads and cache writes included), cachedInputTokens (cache reads), outputTokens, and durationMs, into the span that is current when the call starts. Keys the response omits are left out. Bitfab works out the cost from the model and token counts. fn may be sync or async (such as anthropic.messages.create); the wrapper matches it. Opens no span of its own. Beneath a trace() root, a messages.create call on an anthropic client is already recorded as its own llm span named Messages.create (or AsyncMessages.create), so the wrapper records nothing more and node() on it configures that span. A call the OpenAI Agents SDK integration or the LangChain callback handler is already recording is not recorded a second time, and stream=True calls are not recorded. Also available on BitfabFunction with the same signature.
Raises: ValueError if fn has no __name__.
wrap_openai_responses
Framework integration → see OpenAI Responses API framework guide for examples.
instructions and input as the prompt, plus provider (openai), model, inputTokens, outputTokens, cachedInputTokens, reasoningTokens, costUsd (only when the response reports one), and durationMs, into the span that is current when the call starts. Keys the response omits are left out. Without costUsd, Bitfab works out the cost from the model and token counts. fn may be sync or async (such as llm.responses.create); the wrapper matches it. Opens no span of its own. Beneath a trace() root, a responses.create call on any openai client is already recorded as its own llm span named Responses.create (or AsyncResponses.create), so the wrapper records nothing more and node() on it configures that span. A call the OpenAI Agents SDK integration or the LangChain callback handler is already recording is not recorded a second time, and stream=True calls are not recorded. Also available on BitfabFunction with the same signature.
Raises: ValueError if fn has no __name__.
get_trace
DetachedTrace handle for annotating a trace after its root span has closed, from any process or thread.
Raises: ValueError if trace_id is not a canonical Bitfab trace ID.
Semantics:
- All methods on the returned handle are blocking, like
get_trace_span. Each one returns only once the server has applied the change - When
capture_enabled=False, methods return immediately without sending, except inside a replay item - The server returns 404 if no trace exists with that ID. The method raises rather than logging
get_trace_span
trace_id is the canonical Bitfab trace ID. Pass the span’s Bitfab id alone, or name and/or type. type is one of "llm", "agent", "function", "guardrail", "handoff", or "custom", and giving both name and type matches only spans with that name and that type. Name and type lookups return the last match by default. Numeric occurrences are zero-based in start-time order. Returns None when no trace or span matches.
ValueErrorwhenidis given withnameortype, or when none of the three is givenValueErrorwhentrace_idoridis not a valid Bitfab IDValueErrorwhennameis empty or not a stringValueErrorwhentypeis not one of the span types above, before any request is sentValueErrorwhenoccurrenceis not"first","last", or a non-negative integer.occurrence=Trueraises too, even thoughboolis a Pythonintsubtype
get_span
None when no span with that ID exists in the API key’s organization.
Raises: ValueError when span_id is not a string or is empty after stripping whitespace.
datasets
TypedDicts whose keys match the HTTP response (traceCount, addedTraceIds, and so on).
saveis an upsert keyed on(trace_function_key, name).createdisTruefor a new dataset, andFalsewhen an existing one was updated instead.description=Noneleaves an existing description untouched.listtakes an optional trace function key. Without it, every dataset in the organization is returned.updaterenames a dataset or changes its description, by id. Pass at least one ofname,description, andgolden; passing none raisesValueErrorwithout a request. A field left asNoneis untouched, anddescription=""clears the description. The dataset keeps its id, traces, and graders. A blank name, or a name another dataset under the same trace function already uses, raisesrequests.HTTPErrorwith a 400.Datasetcarriesid,traceFunctionKey,name,description,traceCount,graders([{"id", "name"}]),golden,createdAt, andupdatedAt.golden=Trueonsaveorupdatemarks the dataset as golden for the whole organization, a trusted reference set listed first in the app and inlist.golden=Falseunmarks it, andNoneleaves it as it is.goldenreadsFalsefrom an older server.list_tracesreturns{"datasetId", "traceIds"}, the same membership a replay withdataset_idselects.add_tracesandadd_graderseach accept 1 to 100 ids. They report partial acceptance rather than rejecting the whole call. Ids outside the organization, or under another trace function, come back inskippedTraceIdsorskippedGraderIds. Ids already present come back inalreadyPresentTraceIdsoralreadyAssignedGraderIds.remove_tracesnever deletes a trace, only its membership in the dataset. Ids that were not members come back innotPresentTraceIds.remove_gradersreportsnotAssignedGraderIdsthe same way.rerun_gradersre-scores every trace in the dataset.grader_idsdefaults to every assigned grader. An unassigned id rejects the call. It blocks until the run completes.rerun_graders_asyncis the same call as a coroutine, so await it later, hand it toasyncio.create_task, orgatherseveral datasets. Both raiseGraderRerunErrorwhen the run errored, with the run on.run, andGraderRerunTimeoutError(also aTimeoutError) when the run is still going aftertimeoutseconds. A timeout does not stop the run on Bitfab, so read it later withget_grader_rerun. A request that matches an in-flight run joins that run, andjoinedExistingisTruein that case.get_grader_rerunreturns the dataset’s active grader re-run, or the run named byrun_id. It returnsNonewhen nothing is active, or when the named run does not belong to this dataset.GraderRerunhasstatus(pending|running|completed|errored),graderIds,progress({completedTraces, totalTraces, graderCount}while running),result({tracesGraded, gradersRun}when completed), anderror.- A dataset id from another organization raises
requests.HTTPErrorwith a 404.
traces
search finds traces by the caller metadata your code stamped with set_metadata, by the name it set with set_name, or by the experiment that produced them. It returns a page of stable trace summaries matching every filter supplied. caller_metadata matches exactly and case-insensitively, with at most 10 pairs. name matches the name’s words in order, so it also finds a name that continues past the query, and it matches on word boundaries rather than inside a word. name_contains matches any part of the name, including inside a word, and must be at least 3 characters. Both are case-insensitive and capped at 256 characters. experiment_id returns only the replay traces that experiment produced. At least one of caller_metadata, name, name_contains, or experiment_id is required, so a search never returns unfiltered traces. The server defaults limit to 50 and caps it at 500; use the returned nextCursor for pagination.
An assertion says what should happen when a trace is replayed. Attach assertions to the ORIGINAL trace. get_assertions returns {"assertions": [...], "inheritedFrom": ..., "generation": ...}. generation reports the trace’s latest assertion generation, or None when generation never ran on that trace. Reading a replay that has no assertions of its own returns the nearest ancestor’s instead. That lookup resolves through the replay lineage, and names the ancestor trace in inheritedFrom. Writing assertions through a replay trace id is refused, and the error names the original to retry with. targetOnEvaluatedTrace narrows what the assertion checks on the trace under evaluation. It is either {"kind": "output"} or a span identified by name and occurrence. Omit it to check the whole trace. Targets are span names, never span ids, because a span id captured on the original resolves to nothing on the replay. Saving with an entry’s id edits that existing assertion, so two callers adding different assertions to one trace never overwrite each other.
The save returns once the text is saved as sent. Bitfab then checks each entry that carries assertion text against the writing rules in the background and, when it breaks one, rewrites it in place as one well-formed assertion, usually within a minute. Each returned assertion carries rewrite: its status, sentAssertion, changes, separateAssertions, unresolved, and error. A save returns pending for every entry being checked and not_checked for one skipped or sent without text. get_assertions returns each assertion with its latest rewrite, or None when no check ever ran on it, so read the result there once the check finishes: rewritten, unchanged, or check_failed (the check could not run, or had not finished 15 minutes after the save, and the text stays as sent). separateAssertions holds other claims the sent text bundled. They were not saved, so save each as its own assertion. Pass skip_guided_rewrite=True to save text exactly as sent with no check.
save_assertions writes one trace. save_assertions_all takes one update per trace and writes them all in a single request, so a publisher covering hundreds of traces makes one call. The singular delegates to it, so both go through the same route.
traceId. A batch takes up to 500 traces, up to 50 assertions per trace, and at most 1000 assertions in total. Passing an empty list writes nothing and sends no request.
Generate assertions
generate_assertions has Bitfab read one original trace and write draft assertions about what it actually did. Generation spends model calls, so the drafts are saved awaiting review and are not checked on replays until a person approves them in Bitfab.
guidance is free text that steers what the assertions are about, such as a root cause analysis of why the trace went wrong. It narrows the evidence and produces smaller assertions aimed at that failure, up to 10,000 characters. category_ids limits generation to those categories. Omit it to let Bitfab choose from every category in the organization. An id that is not one of the organization’s categories fails the call before any work starts.
A trace keeps one set of generated drafts. Calling again returns those drafts instead of generating twice, even when the new call passes different guidance or category_ids. Pass regenerate=True to start over. The previous generated drafts are archived once the new ones are saved. Assertions a person wrote are never replaced. A replay trace id is refused, and the error names the original to retry with.
A trace runs one generation at a time. Calling generate_assertions while one is running joins it and never starts a second one. Passing regenerate=True while one is running joins it too.
generate_assertions blocks until generation finishes and returns the drafts as a list of assertions. generate_assertions_async is the same call as a coroutine, in the style of Stripe’s _async methods, so the event loop stays free while Bitfab generates. Await it where you need the drafts, or hand it to asyncio.create_task or asyncio.gather to work on other things or start many traces at once. Both raise AssertionGenerationError when generation failed, with the reason in the message and the trace on .trace_id, and AssertionGenerationTimeoutError (also a TimeoutError) when it has not finished within timeout seconds. After a failure, pass regenerate=True to try again. A timeout does not stop generation on Bitfab.
To check on a generation from anywhere, call get_assertions and read generation. It carries status ("running", "completed", or "failed"), error, assertionIds (the drafts the latest completed generation saved, in order), startedAt, and finishedAt. It is None when generation never ran on that trace.
humanNote is people-only context attached to the assertion. Pass it on a save to write it, omit it on an edit to preserve what is stored, and pass None to clear it. It must never be used as evidence when judging a replay.
justification is the evidence for the assertion: one entry per span you relied on, each {"spanId": ..., "text": ...}, so the reason sits on the span a reviewer can open rather than in one paragraph over all of them. Cite spans on the assertion’s own trace. categoryJustification does the same for category_id, saying why that category fits. Omit either on an edit to preserve it. Changing the category clears categoryJustification, since it justified the old one.
assigneeEmail names the organization member who should review the assertion. The server resolves the address against that organization’s members and refuses one belonging to nobody in it, so a typo fails the call instead of quietly leaving the assertion unrouted. Omit it on an edit to preserve the current assignee, and pass None to leave it to nobody. Reads carry a nullable assignee with id, fullName, email, and imageUrl. Assigning routes an assertion rather than deciding it, so it never returns the assertion to "pending" the way an edit to the wording, criteria, target, or category does.
Reads carry one review decision in approvalState, approvedBy, and approvedAt. It approves both whether the assertion applies to its trace and whether its current category assignment is correct, using the two separate justifications as evidence. Only a person in Bitfab sets it. Editing assertion content or changing its category resets the decision.
Read and update a scenario
A scenario is one situation a trace function’s run was in: the trigger that started the run and the data available when it arrived, with a name and a description, built from one source trace. The trigger and data describe the situation only.actualOutcome can also record what one run in that situation actually did, never what it should have done. get_scenario reads the scenario built from a trace, and save_scenario updates it.
name, description, trigger, and data each replace the saved value whole, and fields left out are kept. append_data adds to the saved data without resending it: its history entries go after the saved ones and its missing items after the saved missing items. Appends made at the same time by different callers all land. Pass data or append_data, not both. Passing both, or none of the seven fields, raises ValueError before anything is sent.
actual_outcome is sent as actualOutcome. Bitfab never fills it in, so it holds only what a caller saved. Passing it replaces the saved value whole, passing None clears it, and leaving the argument out keeps it, so actual_outcome=None and not passing actual_outcome are different saves. It may be the only field sent. Its actions list what the run did in order, each with what it was given in details, what came back in result (or None), whether it failed, and when it happened in at (or None). Its source follows the same rule as a history entry’s source below, and may be left out.
category_ids is sent as categoryIds and files the scenario under any number of categories from categories. It replaces the scenario’s whole set of categories: leave it out to keep the saved set, and pass an empty list to clear it. An id outside the organization raises requests.HTTPError with a 400 naming the missing ids. Both methods return the scenario’s categories as CategorySummary entries sorted by title.
A history entry’s source names the span its text came from, on this trace or another trace linked to the same scenario. Leave it out or set it to None when the text is not in such a trace. A source naming a span on any other trace is refused. The server rebuilds triggerSummary and dataSummary from what is saved, and save_scenario returns the saved scenario. It only updates a scenario that already exists: a trace with no scenario raises requests.HTTPError with a 404, from either method.
spans
list returns the spans of one trace in the order they ran, without loading the full trace. No filter is required, so a call with only trace_id pages through every span:
It returns
{"spans", "nextCursor", "hasMore"}. Each span carries id, traceId, parentSpanId, name, type, startedAt, and endedAt, plus only the fields asked for. Pass nextCursor back as cursor to keep going; the method never fetches the next page on its own.
Raises: ValueError on a 404, which is any trace outside the organization, and on a 400 for an invalid request.
organization_members
assigneeEmail, which is refused when it belongs to nobody here. Membership reaches Bitfab through the identity provider’s mirror, so an empty list means nothing has been mirrored rather than that the organization is empty.
categories
save creates without an id and updates with one. The title is required. Omit description, or pass None, to preserve it on an update. Pass an empty string to clear it. list returns categories ordered by title. delete returns the deleted category and clears its assignments. Assertions, scenarios, and verdicts remain intact.
Pass category_id on entries in save_assertions or save_assertions_all to assign a category. Omit the field to preserve an existing assignment. Pass None to clear it. Assertion reads and saves return category_id and the nullable category summary. Inherited assertions include the same category metadata.
verdicts
save_agent_verdicts MCP tool writes. It runs from inside a replay process instead of a coding-agent session. verdict is "pass", "fail", or "abstained", and annotation is required for a pass or a fail. Key a replay verdict by original_trace_id plus the experiment_id it ran under. test_run_id is a deprecated alias for experiment_id. Passing both with different values raises ValueError. Add attempt when the experiment ran each trace more than once. Use skip for an attempt that crashed, was punctured, or fell back to a schema default. Also use skip for an assertion whose target could not be resolved. Recording a FAIL in either case would read as a behavior regression rather than a check that never ran. Pass annotation to skip to record why the verdict was withheld. It is stored on the withheld verdict and read back by get and get_all, and it is the only place a reader learns the difference between a verdict nobody could reach and a trace nobody looked at. A skip sent without annotation clears the note the previous verdict carried.
abstain records an abstention, the one-call shorthand for save(verdict="abstained") and the same write as {"verdict": "abstained"} in a save_all entry. An abstention is the judge’s third answer: the evidence it read does not show the part of the run the assertion checks, so nothing can confirm or deny it. It counts as a fail in every pass rate, since anything short of an evidenced pass is a fail, but it is stored and read back apart from a fail, so a reader sees judge coverage rather than the agent’s behavior. It is not a skip: a skip is for a check that never ran, an abstention is for a check that ran and found nothing to judge. Pass annotation to say what the evidence would have had to show. It reads back as verdict: "abstained", and the trace’s abstained count says how many of its failed verdicts are abstentions.
Every agent verdict scores one approved assertion. save, skip, and abstain require assertion_id, and save_all requires assertionId on every entry that is not an archive. Verdicts are only written on assertions, never on a whole trace. archive is the one write that may omit it, which clears an old whole-trace agent verdict. save_human and save_human_all require it too, enforced the same way.
Raises: ValueError from save, skip, and archive unless exactly one of trace_id or original_trace_id is given. save_all takes prebuilt VerdictUpdate entries and does not run that check. ValueError from save, save_all, skip, save_human, and save_human_all before any request when a verdict or skip has no assertion_id. ValueError from save and save_human when verdict is missing or unknown, and from save, save_all, and save_human when a pass or fail has no annotation, or one that is empty or only whitespace. ValueError from save and save_all when an abstention carries confidence or evidence, which only a pass or fail may carry, and from save_all for an entry with the deprecated abstain: False and no verdict. ValueError from save_human for "abstained", since a person never abstains. AssertionEvidenceError from generate_evidence when a candidate arrives without the fields AssertionVerdictEvidence declares, so a missing spanId surfaces at the call rather than at the first use.
get and get_all read by trace ID only. After writing a replay verdict, pass the returned outcome’s traceId to get to read it back.
A verdict may carry evidence, with one {"spanId": ..., "text": ...} entry per cited span on the evaluated trace. Omit it to preserve saved evidence. Pass None to clear it. get and get_all return the evidence with each whole-trace and per-assertion verdict.
generate_evidence generates assertion verdict evidence, the same call as the generate_verdict_evidence MCP tool. Pass the original or replay trace being judged; the assertion must belong to that trace or be inherited through its replay lineage. Each candidate comes from the selected trace and contains spanId, text, sourceField (the span field the quote was copied from), spanName, signature-only parameters (names and runtime types, never values), spanType, and isMocked. Save only the candidate’s { spanId, text } fields as verdict evidence; saved evidence remains on verdicts.get and verdicts.get_all.
save_human and save_human_all write the verdicts save_human_verdicts writes over MCP, recorded as written by a person. verdict is "pass" or "fail". Every entry names the assertion it scores with assertion_id ("assertionId" in a save_human_all entry). Use them only when a human decided the verdict, such as capturing a known production bug as a regression case. An agent’s own verdicts go through save. Verdicts have no approval step, so both kinds count as soon as they are written.
Like save_all, the batch is all-or-nothing. A trace outside the organization, a repeated target, or an assertion_id that is not active on its trace rejects the whole call and writes nothing, so a raised error never leaves part of the batch committed.
get and get_all read verdicts back. Each trace carries its effective verdict plus one row per scored assertion, keyed by the same assertion_id the write used, so a per-assertion verdict is verifiable per assertion rather than as a passed/failed tally. verdictStatus says whether a verdict was judged, skipped, or not judged yet, and source says whether a person or an agent wrote it. get returns None when the trace is not in this organization. One get_all call accepts up to 100 ids.
abstained counts how many of the trace’s failed verdicts are abstentions.
client.verdicts was client.labels, and these methods and types were named for labels. client.labels, generate_label_evidence, the label boolean in place of verdict, the Label* and TraceLabels types, and the label, labelStatus, and labelSource fields on what get returns still work for one more minor release and log a deprecation warning. An archive with no verdict to clear reports the action "no-active-verdict", the new name for "no-active-label", even when it is called through client.labels.graders
get_grader_verdicts MCP tool returns. Pass trace_ids to see every grader’s verdict on those traces, grader_id to see one grader’s most recent verdicts across traces, or both to narrow. This is the per-grader detail that verdicts.get does not carry, since that returns one grader-agnostic verdict per trace.
Raises: ValueError when neither trace_ids nor grader_id is given.
bitfab-replay, this loop runs in one of two hooks. The registry entry’s on_item_finish runs in the process that owns the run, and is the default place for it. ReplayConcurrency’s on_item_finish_in_child_process runs in the child that replayed the item, and is the only option when the judge needs state that run left in memory. Both fire once the attempt’s own trace has been flushed, which is when a lineage-keyed verdict row resolves and when the replay trace ID becomes available.
Reading the assertions, judging the replay, and writing the verdict back is the whole loop:
external: true, which Bitfab never runs because you evaluate traces yourself, in your own eval harness, CI, or review queue. Each verdict is stored as that grader’s own, so it counts in dataset and experiment pass rates exactly like a grader Bitfab ran, and get_verdicts reads it back. The same write as the save_grader_verdicts MCP tool. save_verdict_all takes up to 200 verdicts and is all-or-nothing: a grader Bitfab runs, an archived grader, a grader or trace outside the organization, a trace from another trace function, or two verdicts for the same grader and trace rejects the call and writes nothing. Saving again for the same grader and trace replaces the earlier verdict.
get_verdicts, save_verdict, and save_verdict_all were get_labels, save_label, and save_label_all, which still work for one more minor release and log a deprecation warning, as do the GraderLabel* types and the label, labelReason, and labelConfidence fields on each row.experiments
list pages newest first and returns {"experiments", "nextCursor", "hasMore"}; every filter is optional and they combine with AND, metadata (a dict[str, str]) matches by containment, and created_after / created_before take a datetime or an ISO 8601 string, a naive datetime being treated as UTC. get returns the experiment with its provenance (gitBranch, commitSha, baseSha, experimentSha, runBy, completedAt, metadata). get_rollup returns one {"experimentId", "totals", "rollup"} and get_rollup_all returns a list of them for up to 100 ids in one request, or [] with no request for an empty list.
totals counts the run’s replays by state: total counts one replay per trace per attempt, traces counts distinct traces, and judging counts traces, replays or originals, with an assertion verdict still being judged. awaitingVerdicts counts replays still waiting on a verdict. rollup carries the traces, verdicts, and checks tallies plus categories, one {"category", "verdicts"} row per category, and jitter. Each tally has passing, failing, fixed, regressed, stillPassing, stillFailing, originalWithoutVerdict (verdicts whose original has no verdict to compare against), unpaired, and classified; a trace or an assertion passes when at least 75 percent of its attempts passed, and passing / failing count every verdict, compared to the original or not, so passing / (passing + failing) is the pass rate. The earlier names awaitingLabels, labels, and originalUnlabeled are still sent beside them for one more minor release and are deprecated.
Raises: ValueError on a 404, which is any id outside the organization. A batch with one unknown id fails whole and names it.
Replay registry and command
Experimental: Add--cloud to any replay command to run the same replay on your own GitHub Actions. Every replay option works as it does locally, your registry is loaded only on the runner, and the command prints the replay’s progress and a short summary of its result and exits with its exit code. Every trace’s input and output are in the experiment, where the Bitfab MCP tools read them; add --cloud-full-output to print the whole result instead. It runs the Python helper that ships with the SDK, using the one workflow bitfab-replay --cloud-init writes. See Cloud replay for setup, snapshot review, and recovery commands.
ReplayRegistry.register stores the exact production callable, static per-function replay behavior, and an optional options_factory. It returns self, so calls chain. A @trace- or @span-decorated function supplies its key automatically. A plain handler root passes trace_function_key explicitly instead. **options accepts the same keyword arguments as replay(): limit, trace_ids, name, notes, metadata, max_concurrency, code_change_description, code_change_files, experiment_group_id, experiment_group_name, dataset_id, dataset_ids, grader_ids, only_with_assertions, skip_assertion_judging, mock, mock_override, adapt_inputs, db_branch, dry_run, attempts, concurrency, and on_item_finish. The child’s own grading hook is not among them. It is set on the ReplayConcurrency passed as concurrency, since only that object creates a child to run it in.
The factory receives JSON values from --params and --param. It can construct executable options such as mock_override. Values passed directly with --param override values loaded from a --params file. Unknown option names are rejected when the registry loads.
on_item_finish is the one lifecycle callback a registry entry can set. The command runs its own progress reporter first, then calls yours with the same finished item, which is where a replay verdict is written. It runs in the process that owns the run, so under primitive="process" it runs in the parent, once that item’s child has exited. It is also the only hook that fires for an item whose child died without producing a result. It is skipped under --dry-run, since nothing ran. A callback that raises is reported on stderr as on_item_finish failed and never fails the run.
To grade inside the child instead, or in addition, see on_item_finish_in_child_process on ReplayConcurrency.
ReplayRegistry.get(name) returns the stored ReplayRegistration, carrying client, fn, trace_function_key, options, and options_factory. .names lists registered command names in registration order. The package installs bitfab-replay. Run it as bitfab-replay --registry <path> <pipeline> [options]. The registry module must define registry. As soon as the server creates the experiment, the command prints [replay] Experiment <id>: <url> to stderr. If the run then fails with ReplayError, it prints Experiment <id>: <url> again just before the error message, so the experiment is never lost. A run that fails after its experiment was created also prints Resume with: bitfab-replay --registry <path> <pipeline> --resume <experiment_id>, the command that picks the experiment back up. Stopped with Ctrl-C or SIGTERM after the experiment was created, the command marks it interrupted, prints [replay] Experiment <id> interrupted. followed by the same Resume with: command, and exits. When Bitfab could not mark it interrupted, it prints [replay] Experiment <id> stopped, but Bitfab could not mark it interrupted. It will be marked interrupted within 15 minutes. A resume in the next two minutes may be refused as still running. followed by the same Resume with: command instead, and exits with code 130 for SIGINT or 143 for SIGTERM. An on_interrupt set on the registration is replaced by the command’s own.
bitfab-replay adds only the registry file’s own folder to the import path. A registry that imports the app by package name needs the app installed in the active environment, or PYTHONPATH=. set from the project root. When the registry cannot load one of its imports, the command raises ModuleNotFoundError with the original message followed by a note. The note says a registry belongs in the project that builds the traced function’s call and repeats the import path rule.
Raises:
ValueErrorfor an empty or duplicatenameValueErrorfor a non-callableoptions_factoryValueErrorfor a non-callableon_item_finishValueErrorfor a plainfnwith notrace_function_keyand no@traceor@spankey of its ownValueErrorfor an option name outside the accepted set above
A run whose selection matched no traces exits non-zero rather than reporting a clean run of zero items.
Under
--resume, the summary counts only the items that ran now as replayed. Items the earlier run finished print on their own line, as finished before resume.
--seed <cases.jsonl> runs each case once through the same registration and records the run, instead of replaying. Each line is a JSON object with an input list, plus optional kwargs, metadata, and session_id. A JSON array of those objects works too. A case’s input and kwargs are the call itself, recorded as-is. A case carrying expected is rejected, since the output is what the run produced. The registration already holds the client, the callable, and the trace function key, so the case runs against the exact function the later replay selects. The registration’s adapt_inputs is not run at seed time. It is a replay hook. A replay of the seeded trace applies it then, with the case’s metadata available on ctx.
Replay auto-capture environment variables
Replay memory throttle environment variables
These apply underprimitive="process", where memory_throttle decides when a child may launch.
primitive="process" re-execs bitfab-replay --registry once per work item. Each item runs in its own child interpreter. The environment for that interpreter is captured before the registry module’s own import-time side effects run. A child that runs longer than 40 minutes is killed. It is then recorded as a failed replay item. To grade an item inside the child that ran it, set on_item_finish_in_child_process on the ReplayConcurrency.
register_mock_override
register_mock_override(MockOverride(...)), register_mock_override(match, value), register_mock_override(trace_function_key, resolver_or_override), and register_mock_override(resolver). A keyed resolver runs only for child spans with that trace function key. A keyed MockOverride also applies its matcher. A global resolver can route on ctx.node.trace_function_key. Return NO_MOCK_OVERRIDE to continue to lower-priority overrides and the base mock strategy. None remains a valid mocked output. Per-call overrides take precedence over registered overrides.
Raises:
ValueErrorwhen a keyed call’s second argument is neither aMockOverridenor a callable resolverValueErrorwhen aMockOverrideis passed together with a secondvalueargumentValueErrorwhen a bare, non-callablematch_or_overrideis given with novalue
clear_mock_overrides
replay
skip_assertion_judging=True to turn judging off, since each judgement costs model calls. Replays from SDK versions released before judging became the default never ask for it, so they stay unjudged. only_with_assertions narrows whatever limit, trace_ids or dataset_ids selected to the traces that carry at least one approved assertion, so a run measuring assertion outcomes does not pay to re-execute traces nothing can grade. The server applies it as part of selection, which is what makes it compose with limit: limit=10, only_with_assertions=True is the ten most recent traces that HAVE approved assertions, not the ten most recent filtered down to however few do. Neither an archived assertion nor one still awaiting review counts: a replay is only judged against assertions a person has approved. A selection that narrows to nothing replays nothing (the CLI exits non-zero, as it does for any empty selection).
mock_override accepts a MockOverride, a global resolver, or a list of either. The first matching entry whose value is not NO_MOCK_OVERRIDE wins. Per-call overrides run before registered overrides. Both take precedence over mock.
experimental_selective_replay accepts {"must_run": [{"trace_function_key": "checkout", "span_name": "price"}]}. It requires mock="marked" and rejects overrides. See selective replay for reuse declarations, limits, and coverage reports.
on_item_start fires when a worker begins processing an item, before replay setup or customer code runs. It carries type="started", running lifecycle totals, and the historical trace and span identity.
Pair it with on_item_finish. on_item_finish fires exactly once per item, as that item finishes. It carries running totals (completed, total, succeeded, errored), experiment_id, and the required finished item. test_run_id is a deprecated alias carrying the same value. It never represents whole-run completion.
The finished item carries these fields:
trace_id: the server replaytraces.id, read back off the ingest response and surfaced as the item finishes. Its trace is flushed on finish.trace_idisNoneonly if that flush could not confirm delivery in timeoriginal_trace_id: the original historical trace being replayed.source_trace_idis a deprecated alias for the same valueinput,result,original_output,error,trace_error, andreplay_errorduration_ms: this replay’s own durationoriginal_duration_ms,original_tokens, andoriginal_model: describe the trace being replayedtokens,model,db_snapshot_ref, anddb_branch_timings
on_progress callback receives the same per-item events. It may additionally receive its legacy, item-less terminal complete event. It is ignored when on_item_finish is also provided.
Pass the ready-made report_replay_progress reporter as both on_item_start and on_item_finish. The Bitfab plugin uses its stderr events to identify in-flight traces and to write finished per-item result files. Each event carries the experiment ID as experimentId, and as the deprecated test_run_id for older plugins.
on_experiment_start fires once, as soon as the server has created the experiment and before any item runs. It receives a ReplayExperimentStart with experiment_id and experiment_url, so the experiment link is known even if the run later fails.
on_interrupt fires when SIGINT (Ctrl-C) or SIGTERM stops the run after the experiment was created, once the experiment is marked interrupted. It receives a ReplayInterrupt, whose marked_interrupted is False when the request to mark it failed. When it is set, replay prints nothing itself. See the interrupt notes under resume below.
replay(decorated_fn) reads the trace function key from the @trace or @span decorator. It raises if decorated_fn is undecorated.
replay("key", fn) takes an explicit key plus any plain callable. The SDK wraps fn in a span under that key internally. This explicit-key form is how handler-instrumented workflows replay: LangGraph, LangChain, the Claude Agent SDK, and OpenAI Agents. Those workflows have no decorated root in the app.
A legacy keyword form, replay(fn=decorated_fn), is also preserved. fn_or_key used to be positionally named fn, so this spelling still works. Prefer replay(decorated_fn) in new code.
When replay auto-wraps a plain callable, it passes a recorded dict root input, for example a LangGraph state, as a single positional argument. This matches the TypeScript SDK. A decorated function always gets the keyword-args splat instead, whether or not a redundant matching key is also passed. An explicit key that contradicts the decorator’s own key raises.
max_concurrency=None means unlimited. max_concurrency=1 means sequential. When trace_ids is passed, limit is ignored, with a warning, because the explicit ID list already determines how many traces replay.
attempts (1 to 100, default 1) replays each selected trace that many times inside the same experiment. Attempts run attempt-major. Each attempt is its own replay trace, with its own correlation id, verdict, tokens, and cost. ReplayItem.attempt reports which attempt an item is.
concurrency is the options-object form of the same controls: ReplayConcurrency(attempts=..., primitive=..., max_concurrency=...). Passing concurrency alongside the attempts or max_concurrency arguments raises, rather than merging the two.
primitive="async"is the default. It runs every work item as a coroutine in one process.max_concurrencydefaults to 10primitive="process"runs each work item in its own child interpreter.max_concurrencydefaults to 4, andNoneis refused. Only this primitive acceptson_item_finish_in_child_processandmemory_throttle
bitfab-replay --registry. A bare replay() call raises instead, because it holds a live function object with no re-exec target.
process_launcher is the internal seam bitfab-replay --registry uses to supply that re-exec mechanism. Its type is not exported. Calling replay() directly should never pass it.
db_branch=True requests a DB branch per replay item, sized however the mirror project is sized. False, or omitting db_branch, leaves branching off. Each bounded worker resolves its own branch, including a separate branch per attempt. get_current_replay_branch() exposes that branch inside fn.
db_branch also accepts a mapping to tune the branch, for example db_branch={"min_cu": 2, "max_cu": 2, "warmup_sql": "SELECT 1;"}. Every key in that mapping is optional.
min_cuandmax_cuare the compute’s autoscaling floor and ceiling, in Neon Compute Units (0.25 to 56). Equal values pin a fixed size, up to 56, which keeps items comparable. An autoscaling range may not span more than 8 CU, and may not exceed a ceiling of 16 CUwarmup_sqlis appended to the branch’s readiness check. It runs beforefnsees the branch, so its time is not charged to the replayed call. Invalid warm-up SQL fails the branch, rather than quietly handing back a cold one
db_branch’s keys entirely and the branch keeps the mirror project’s own defaults.
Async-generator spans are not mockable. If mock="all", mock="marked" selects one, or a mock_override matches, the item errors before the real generator is iterated.
A trace replays only when its root span has serializable inputs. It also replays when it was instrumented through a framework handler, whose recorded root input is serializable. If the original inputs were stubbed as non-serializable at capture time, the trace cannot be replayed.
dry_run=True resolves every item’s inputs through selection, deserialization, and adapt_inputs. It then stops without calling fn. Each item reports the exact (args, kwargs) the function would have received. This is the cheap way to check that recorded inputs still fit the current signature. The persistence barrier is skipped, since a run that executed nothing produces no traces.
resume picks a stopped experiment back up by its experiment ID. The server returns the experiment’s saved trace selection, its attempt count, and which trace and attempt pairs already finished. Only the unfinished pairs run, and they record into the same experiment. A pair counts as unfinished when its trace never finished, including one Bitfab marked failed only because the stopped run went quiet. A pair that errored in the earlier run stays finished.
- The experiment fixed its selection when it started. So
resumeraisesValueErrorwhen combined withtrace_ids,limit,dataset_id,dataset_ids,attempts,grader_ids,only_with_assertions, ordry_run - Database snapshots follow the experiment’s stored
db_branchsetting, including its compute size and warm-up. Adb_branchpassed withresumethat disagrees is ignored, with a logged warning - The finished pairs come back first in
items, withcarried_over=True. They carryoriginal_trace_id,attempt, and the replaytrace_id. Their input, result, outputs, timings, and tokens are empty orNone. Theirerroris set when the pair errored in the earlier run. The lifecycle callbacks do not fire for them, but the running totals count them - A trace that was deleted after the experiment started is skipped, with a logged warning naming it
- Database branches the stopped run still held are released before the resume starts. A resume fails if Bitfab cannot release them. Try it again in that case
force=True skips the last two refusals, never the others. Passing force=True without resume raises ValueError. An interrupted experiment is accepted right away, without the two-minute wait, and set back to pending for the resumed run.
Once the experiment is created, a replay that stops before it finishes marks the experiment interrupted through POST /api/sdk/replay/interrupt, waiting up to five seconds for an answer. A failed request (an error status, a timeout, or no connection) is logged at debug level and never raised. Only a pending experiment moves, so a completed one stays completed.
- On SIGINT or SIGTERM, replay marks the experiment interrupted, then calls
on_interrupt, or prints[replay] Experiment <id> interrupted.to stderr when it is not set. When the request failed it prints[replay] Experiment <id> stopped, but Bitfab could not mark it interrupted. It will be marked interrupted within 15 minutes. A resume in the next two minutes may be refused as still running.instead, since the experiment stays pending until the server marks it. An exception raised byon_interruptis logged, exceptSystemExit, which ends the process. The signal then carries on as it would have without Bitfab. SIGINT raisesKeyboardInterrupt, a handler installed before the run is called, and SIGTERM with no handler ends the process. Handlers are installed only when replay runs on the main thread, restored just before the final request that completes and grades the experiment, so a signal arriving during it ends the process as it normally would and leaves the experiment to Bitfab’s own 15-minute check, and restored when the run ends - When the run raises any other exception before it finishes, replay marks the experiment interrupted and re-raises it.
on_interruptis not called - A replay killed before it can mark itself, for example by SIGKILL, is marked interrupted by the server once it has gone 15 minutes with no replay activity. The server checks every five minutes. A running replay sends a heartbeat about once a minute, so only a process that stops sending it is marked interrupted
- An interrupted experiment is not graded. Resume it with
resume, or mark it finished with Mark finished on the experiment page orsave_experimentwithstatus: "completed", which grades the traces that already finished
seed_trace
fn once and records the execution as an original trace. It returns the trace ID, for use with replay(trace_ids=[...]). Capture stays off. This records exactly one call, with the same semantics capture-on would give it: a root span, a first-party subtree under the trace decorator’s bounds, and no mocking. The recorded input is the real call. The output is what the run produced. The trace lands under trace_function_key, with ingestion_type: seeded. replay selects it like any other trace. Each replay of it links back as original_trace_id.
fn resolves exactly as it does for replay. A decorated function records under its own key. That key must match trace_function_key. A plain callable is wrapped under trace_function_key instead. An exception is recorded on the root span. The trace still persists. The exception is re-raised. A call that records nothing, for example because no API key resolved or fn is a generator, raises instead of returning an ID that replay could never find.
metadata is stored on the trace. It is handed to a later replay’s adapt_inputs hook as ctx["metadata"]. This lets a case’s provenance, such as its id, suite, or source row, ride with the trace instead of through the recorded inputs. If the traced function also emits its own trace through an integration, the caller’s metadata is merged onto that export and wins on any shared key, so the provenance recorded here is what replay reads back. name is the trace’s title and a searchable field. Put the case’s own label there, such as a ticket id or a dataset row name, so the seeded trace can be found by it.
Call it from synchronous code. An async def fn runs to completion on a fresh event loop. Calling it from inside an already-running loop raises. With trace_across_threads=True, spans from worker threads inside the call nest under the seeded root.
A seeded trace carries no database pin. db_branch refuses it as a result. It has a full recorded subtree, though. Replay mocking works on it exactly as it does for a captured trace.
reseed_trace
fn once exactly as seed_trace would, then asks Bitfab to adopt that run under trace_id. Returns {"trace_id", "previous_run_trace_id"}. The trace keeps its id, verdicts, assertions, dataset membership, name, and metadata. The previous run is kept as its own trace with reseedOfTraceId pointing back at trace_id. Nothing is mocked and no experiment is created.
fn resolves as it does for replay and seed_trace, and trace_function_key must match the trace’s own key. A run that raises is recorded but not adopted, and the exception is re-raised. Bitfab rejects a run from another function, one that already belongs to a dataset, or a trace that is itself a previous run. Graders on the datasets holding the trace are re-queued, and default replay selection skips previous runs.
reseed_from_registry
{"pipeline", "traceFunctionKey", "reseeded": [{"traceId", "previousRunTraceId"}]}. The installed bitfab-seed --registry <path> <pipeline> --from-trace <id>[,<id>...] command calls it.
seed_from_registry
input list, plus optional kwargs, metadata, and session_id. input and kwargs are the call itself, recorded as-is. The registration’s adapt_inputs is a replay hook. It is not run at seed time. A seeded trace is therefore never adapted twice. Returns {"pipeline", "traceFunctionKey", "traceIds"}.
call
env_vars.
Raises: ValueError when the function lookup fails. An execution error re-raises whatever exception the BAML call itself raised, not necessarily ValueError.
Framework Integrations
Handlers returned by these methods plug into each framework’s callback, processor, or hook surface. They emit Bitfab spans automatically. They also reuse the owningBitfab client’s lazy OTel worker, so
Bitfab.close() releases the decorator and the framework transport together.
Directly constructed LangGraph or Claude handlers are the exception. They own
their own transport, and expose close(timeout=30.0) plus a context manager.
For usage examples and semantics, see the per-framework guides.
All client-owned adapters honor capture_enabled. With client capture off, their framework callbacks and wrappers continue to run but send nothing; the client also skips the internal trace normally sent by local call() execution. Replay and seed_trace remain explicit recording operations. Directly constructed handlers with no usable API key are inert.
get_langgraph_callback_handler
BaseCallbackHandler. Pass via config={"callbacks": [handler]} when invoking. The handler-created root is replayable from the framework input. A separate @span root is only needed for meaningful surrounding application work. Retriever calls are captured too, as function-type spans carrying the query and returned documents. See LangGraph framework guide.
Aliased as get_langchain_callback_handler(trace_function_key) for plain LangChain projects. The returned handler and its behavior are identical. The handler class is also exported as BitfabLangChainCallbackHandler.
get_langgraph_integration
wrap_tool_call and awrap_tool_call directly to LangGraph’s native ToolNode constructor. For an agent built with LangChain’s create_agent or Deep Agents’ create_deep_agent, pass integration.middleware in middleware=[...] instead; it routes each tool call through the same hooks. Using middleware requires langchain 1.0+.
instrument(graph) returns a traced copy of the graph of the same type (a subclass of the graph’s class), so it can be called directly, nested as a node in another graph, or exported to LangGraph Platform, and isinstance checks still pass. The original graph is not modified. Every way of running the copy, invoke, ainvoke, stream, astream, batch, abatch, stream_events, astream_events, and pipes or retries built on it, adds callback_handler to the run config, preserving invocation-time config and any existing callbacks, and records a replayable root span carrying only the graph input, with the final graph state as its output. A stream’s root span stays open until the stream is exhausted or closed. Copies made with with_config() or copy() stay traced, and every other attribute, such as get_state(), is the graph’s own. When the graph runs inside a trace for the same trace function, as it does during replay or as a nested graph, no second root span is opened.
Replay a recorded run through the method production calls, for example bitfab.replay(key, graph.invoke) or bitfab.replay(key, graph.astream). A streamed replay is drained inside the replay root, and its result is the final graph state. When the graph has a checkpointer and the run has a thread_id, the thread’s checkpoint at the start of the run is recorded on the root span, and replay restores it into an in-memory checkpointer, so the replayed run sees the same history and resumes the same interrupt without writing to the graph’s own checkpointer. Channels LangGraph stores as deltas, such as the message history of create_agent and create_deep_agent agents, are recorded with their full value. Inside a trace() root, with the same trace function key or another one, the checkpoint is recorded on that trace’s root span under the integration’s key, so several agents in one trace keep their own threads; when one agent runs more than once in a trace, its first run’s checkpoint is recorded and later replayed runs continue the same in-memory thread. Every run of the graph during a replay uses an in-memory checkpointer, even when no checkpoint was recorded. A Command run input is recorded and restored as a Command. The earlier create_invoker(graph) and create_async_invoker(graph) are deprecated: they log a one-time warning and return instrument(graph).invoke and instrument(graph).ainvoke.
create_agent(...) and create_deep_agent(...) are drop-ins for LangChain’s create_agent and Deep Agents’ create_deep_agent. They pass every argument through, add middleware to middleware=[...] (once, even if it is already there), and return the built agent traced by instrument(). create_deep_agent also adds middleware to each subagent given as a spec; subagents given as a compiled runnable or a remote graph_id pass through unchanged. They require langchain 1.0+ and deepagents respectively; install bitfab-py[deepagents] (Python 3.11+) for Deep Agents. A missing package raises an ImportError naming what to install.
A tool call that reaches wrap_tool_call, awrap_tool_call, or middleware outside a trace for this key, for example because the graph was invoked directly instead of through instrument(), runs normally and is not recorded as a replayable span. The SDK logs a one-time warning naming the fix.
Use the lower-level callback_handler and wrap_invoke(fn) when meaningful application work around the graph invocation needs to live inside the trace.
Under an active trace() subtree, the integration’s tool and invoke spans behave as node() calls instead of opening opt-in spans. This gives them nearest-frame parenting, span budgets, and mock_on_replay_default inheritance. A wrapped graph can therefore run inside opt-out tracing without raising MixedTracingError.
Integration-managed tools are marked for replay mocking by default. Recorded ToolMessage and Command results are reconstructed with the current tool-call ID. An expected tool output that is missing during replay fails closed, instead of running the live tool.
Calls are matched by tool name and occurrence order. Repeated concurrent calls to the same tool are not yet recommended, as a result.
Install the langgraph extra. See the LangGraph framework guide and current limitations.
function spans. They cannot be mocked on replay. Graph state a tool writes while it runs, such as a file saved by a LangChain Deep Agents state backend, is recorded with that tool call and written again when replay returns the recorded result. This relies on LangGraph’s internal state-write function; if a LangGraph version stops exposing it to tool calls, the SDK logs a one-time warning and those writes are not recorded.
get_deep_agents_integration
get_langgraph_integration, named for LangChain Deep Agents. Build the agent with its create_deep_agent(...), which takes Deep Agents’ arguments, so every tool call of the main agent and of the subagents you define, including write_todos, the file tools, and task, is recorded as a replayable span. Subagent runs and their tool calls nest under the task call that started them. A mocked task call returns the subagent’s recorded answer without running the subagent. Requires deepagents, which brings langchain. See the Deep Agents framework guide.
get_openai_tracing_processor
openai-agents. Registering it with agents.add_trace_processor does require it. It captures agent internals. Pair it with get_openai_agent_handler for a replayable root. See OpenAI Agents framework guide.
get_openai_agent_handler
wrap_run(agent, input, **run_kwargs) is a drop-in for Runner.run. It records a keyed, replayable root span carrying the run input, and the tracing processor’s spans nest underneath it. For streamed runs, wrap_run_streamed(agent, input, **run_kwargs) is an async-generator drop-in for Runner.run_streamed. Iterate it to consume the same stream events while the run is traced. bitfab.replay() re-runs the non-streaming wrap_run, not this async generator. Both methods skip opening their own root span when already inside an enclosing Bitfab span, such as a replay auto-wrap or the caller’s own @span. In that case they run the call directly, so the tracing processor nests under the existing root instead of doubling it. See OpenAI Agents framework guide.
get_claude_agent_handler
instrument_options(options), wrap_response(stream, input=...), and wrap_query(stream, input=...) for the Claude Agent SDK. Pass input=prompt to the wrap call to record a replayable root span. When a Bitfab span is already active, no second root opens. The handler’s spans nest under the existing one instead. See Claude Agent SDK framework guide.
wrap_baml
See the BAML framework guide for examples. Full signature under wrap_baml above.
class BitfabFunction
Returned from client.get_function(key).
Bitfab instance, using the bound trace_function_key. The guides lead with this form for a trace root, because the key is bound once and the root, its spans, and its framework handlers all read it from here. trace() and span() take exactly the options their client-level counterparts take, and preserve the client methods’ subtree capture, replay, nesting, and mixed-surface behavior. The get_*_handler methods reuse the same bound key, so a trace root and its framework handler need no repeated string. The experimental get_langgraph_integration() also binds the key. Pass its sync and async hooks directly to LangGraph’s native ToolNode. Then wrap the compiled graph with instrument() and call invoke, ainvoke, stream, or astream on it. The experimental get_deep_agents_integration() binds the key the same way; build the agent with its create_deep_agent(...). wrap_baml is the exception. It opens no span and uses no key. It enriches the current span instead. Call it inside a function that is already wrapped by this handle’s trace() or span(). wrap_jev, wrap_openrouter, wrap_anthropic, and wrap_openai_responses work the same way.
Module Functions
finalizers
finalize= helpers for streaming spans. openai_chunks assembles OpenAI chat-completion chunks into text, finish_reason, usage, and tool_calls. usage is present only when the request set stream_options={"include_usage": True}. anthropic_events assembles Anthropic stream events into text, stop_reason, and usage. Both helpers are duck-typed. Both tolerate malformed or unfamiliar events. Neither requires the provider package at runtime.
get_current_span()
id and trace_id return "". The other methods do nothing. Never raises.
get_current_trace()
get_current_replay_branch()
None outside a replay item. It also returns None for an item whose source trace carried no DB snapshot reference. That is the fallback path: url = branch.database_url if branch else os.environ["DATABASE_URL"].
The value object is immutable, and scoped to one item. It is built from the replay ContextVar, so concurrent items each see their own branch. Reading database_url marks the trace as having used the branch, reported as accessed. The other attributes inspect the branch without exposing the connection string, and deliberately do not mark it as accessed. repr() redacts the URL.
Every field the service puts on the lease is copied onto the branch, under its snake_case name. A field added server-side is therefore readable before you upgrade the SDK. database_url is the sole exception. It is the credential, and the only member that may mark the branch as accessed.
flush_traces(timeout: float = 30.0)
timeout seconds. Returns True when the queued exports completed
successfully within that deadline. Returns False when delivery failed, or the
flush timed out. Records still held awaiting a simulation plan also make it return
False. Ready records are still flushed when other records remain held.
close() also returns False if held records cannot be delivered before shutdown.
Use it before process exit in short-lived scripts.
Bitfab.close(timeout: float = 30.0)
Flushes pending requests and permanently shuts down this client’s private OTel
transports, within one total deadline. The method is idempotent. It returns
False if delivery or shutdown misses the deadline. Bitfab also supports
with Bitfab(...) as client:, which calls close() on context exit. Use
either form when a long-running process creates transient clients. Shared
clients may otherwise remain open until the process-wide exit hook runs.
Classes (Context Handles)
CurrentSpan
CurrentTrace
set_session_id groups traces from the same user session. Unlike the other setters, it does not validate that session_id is a non-empty string.
set_name sets the trace’s title in Bitfab. This is a searchable and filterable field, stored on the trace’s name column. A trace with no name set is titled by its trace function key instead. Empty strings are ignored.
set_metadata shallow-merges searchable exact-match metadata, with later keys winning. It accepts up to 50 keys; keys use 1-40 letters, numbers, _, ., :, or -, and scalar values are at most 500 characters. Filters match exact key/value pairs case-insensitively.
add_context accumulates entries.
drop flags the trace to be dropped. Once flagged, spans that complete afterward are not uploaded at all. The flag rides out on the completion payload. At completion, the server scrubs any payloads that already raced out, meaning the trace itself, its external trace, and its sibling spans. It deletes the archived S3 objects. It marks the trace dropped instead of completed, keeping only a skeleton audit row. drop is a no-op outside a span, and never raises.
TraceSearchResult
client.traces.search. tokens has the TokenUsage shape. nextCursor is None on the last page; pass it back as cursor to fetch the next one.
DetachedTrace
client.get_trace(trace_id), where trace_id is the canonical Bitfab trace ID. Its methods send to the server immediately, and block until it responds. A later read therefore always observes the write. They raise if the server rejects the update. They are silent no-ops if the client is disabled, or if input validation fails. Validation otherwise mirrors CurrentTrace, with one exception. DetachedTrace.set_session_id does validate a non-empty string, unlike CurrentTrace.set_session_id.
TypedDicts & Dataclasses
AllowedEnvVars
CapturedSpan
get_trace_span and get_span. SpanOccurrence is Literal["first", "last"] | int.
ReplayConcurrency
attempts must be between 1 and 100. Async mode accepts max_concurrency=None for unlimited work. Process mode requires a positive bound instead. Passing concurrency= together with the legacy attempts= or max_concurrency= parameters raises, rather than choosing one over the other.
memory_throttle holds a child back until the machine has memory for it, so a run on a loaded laptop finishes with fewer children in parallel instead of being killed by the operating system. Free memory must cover every running child’s remaining growth, plus the new child, plus a reserve, and the operating system must not report critical memory pressure. On macOS that is the critical level of kern.memorystatus_vm_pressure_level. On Linux it is every task stalled on memory for 10% or more of the last ten seconds, read from the container’s memory.pressure file or /proc/pressure/memory. Swap reading near full does not hold a child back by itself, because macOS grows swap to match what it uses. Where pressure cannot be read, swap at 85% used stands in for it. Each running child is charged its full expected peak rather than its current resident size, because a child that started a moment ago has not grown yet. That per-child estimate starts from a default and then calibrates in both directions off the resident size actually measured for children that have finished.
It only ever holds launches back. It never raises concurrency above max_concurrency, and it never holds back the only child in flight, so a machine under lasting memory pressure still finishes the run one item at a time. Where memory cannot be read it admits normally.
It defaults to True under primitive="process" and False under primitive="async". Passing memory_throttle=True alongside primitive="async" raises, because every async item shares one process and there is nothing to hold back. Lower max_concurrency there instead.
on_item_finish_in_child_process is the child’s own item-finish hook. It runs in the child interpreter that replayed the item, which is the only place the replayed run’s in-memory state still exists, after that item’s spans are confirmed delivered so item["trace_id"] is in hand and after its result file is written, and before that child exits.
ReplayItemFinishProgress, so one callback can serve the registry hook and this one.
child_delivery_timeout is how many seconds a child waits for its spans to reach Bitfab. It bounds two waits. The first runs before on_item_finish_in_child_process: the child flushes the item’s spans to read back item["trace_id"], and if that flush does not confirm delivery, it asks Bitfab whether the trace is stored for up to the same number of seconds again. The second runs after the hook: the child flushes whatever is still queued, including spans the hook itself recorded, before it exits. Raise it when a large trace uploads slowly and items come back with trace_id set to None or with spans missing. It defaults to 30.0 under primitive="process", must be a positive number, and is refused under primitive="async", since no child process exists there.
A child reports span delivery failures, such as a rejected or failed upload, delivery not confirmed within child_delivery_timeout, or a replay Bitfab never confirmed (the same [bitfab] replay ... capture incomplete. warning, which leaves the item without an error), as [replay] span delivery problem for <trace>#<attempt>: ... on the command’s own stderr. Repeated messages are counted instead of repeated.
on_item_finish, and setting both is the expected shape.
Each fires exactly once per item, and the child’s runs first. The parent only learns an item exists once its child has exited, and the child runs its hook before exiting, so a slow judge in the child delays that item’s progress event in the parent and never the reverse. Cross-item state belongs in the parent hook or in the returned
ReplayResult, since each child sees only its own item.
It is refused under any primitive other than process, since no child process exists to run it in. It is skipped under --dry-run. A callback that raises is reported as on_item_finish_in_child_process failed, naming the trace and attempt, instead of failing the item. The message is written to the child’s stderr and forwarded to the command’s own stderr, so a judge that throws is visible even though a successful item’s child log is never read, and the child writes its result file before running the hook. A child that dies before reaching the hook is still reported to the registry’s on_item_finish in the parent, which is where a crashed attempt is recorded as skipped.
TokenUsage
ReplayItem.tokens and ReplayItem.original_tokens, of their ReplayProgressItem equivalents, and of TraceSearchEntry.tokens. Not exported from bitfab. Referenced here only for the field’s type.
ReplayItem
ReplayResult
CodeChangeFile
replay(code_change_files=[...]) or read back from a BITFAB_CODE_CHANGE_PATH file. path is relative to the repo root, or any consistently used root.
AdaptContext
adapt_inputs. original_trace_id is the Bitfab trace ID of the trace being replayed. It lets a table-driven adapter look up per-trace adapted inputs. original_span_id is the external span ID the recorded inputs were read from. metadata is the original trace’s stored metadata, meaning whatever seed_trace or get_current_trace().set_metadata recorded on it. It is empty when the trace carries none. When the traced function also emits its own trace through an integration that exports trace metadata, the caller’s metadata is merged with that export and wins on any shared key, so an integration no longer replaces the provenance the caller stored. Keys the integration set are kept alongside it, so this can carry keys the caller never wrote. Requires v0.52.3 or later. This lets an adapter read a seeded case’s provenance without it being smuggled through the recorded inputs.
Replay lifecycle progress
ReplayProgressItem mirrors ReplayItem. It makes the fields that are unavailable during execution optional. ReplayProgress is the deprecated compatibility shape. It additionally supports a terminal type="complete" event, with an optional result.
Each finished item’s ingestion_type identifies its source as captured or seeded. It matches the final item’s value and is None against older servers.
DbSnapshotRef
ReplayItem.db_snapshot_ref carries. sdkWallClockBeforeFn is the ISO wall-clock timestamp the SDK observed immediately before invoking the wrapped function. The server-side resolver uses that timestamp as the snapshot instant. No provider is captured here. The provider is resolved later, at replay time. It uses camelCase keys, since the ref goes to the server as-is.
CommitRef
commit_ref on every root trace completion. sha is the commit. branch is the checked-out branch, or None when detached or unknown. dirty is True when the working tree had uncommitted or untracked changes, False when it was clean, and None when the SDK could not tell, which is always the case when the ref came from environment variables rather than git. remote is the origin URL reduced to host/owner/repo with any credentials removed, so a CI checkout token never reaches the trace. root_sha is the repository’s first commit, so two checkouts of the same repository match even without a remote.
Resolution order is BITFAB_COMMIT_SHA, then the deploy platform’s build variables, then git in the process’s working directory (see Commit ref environment variables). Environment resolution is synchronous and free. The git path runs once per process on a background thread with a two second timeout per command, so it never sits on the thread that ran the traced function. A trace that completes before it lands ships without a commit_ref, and a process with neither variables nor a repository never sends one. The result, including a negative one, is memoized for the life of the process. Set BITFAB_DISABLE_COMMIT_REF to opt the process out entirely.
DbBranchLease
db_snapshot_ref. It is carried on the replay context with camelCase keys, because it arrives straight off the wire. neonBranchId is the literal Neon branch id. snapshotTimestamp is the instant the branch was pinned to. It is echoed back in db_snapshot_usage on the replayed trace’s completion. Inside a replayed function, read the branch through get_current_replay_branch() instead of this raw lease. That accessor returns a ReplayBranch.
DbBranchOptions
client.replay(db_branch={...}), to tune how each item’s branch is sized and warmed. Every key is optional. db_branch=True branches with the mirror project’s own sizing instead. min_cu is the autoscaling floor, in Neon Compute Units (0.25 to 56). max_cu is the ceiling. Equal to min_cu, it pins a fixed size. Otherwise, a later item can run against an already-scaled-up endpoint and post a better number for the same code. warmup_sql is appended to the branch’s readiness check. It runs before fn sees the branch, so its time is not charged to the replayed call. Invalid SQL fails the branch, rather than silently leaving it cold.
MockOverride
(match, value) pair. The first override whose match returns True wins for a given span. A MockOverrideResolver is the keyless form. It runs for every child span. It returns NO_MOCK_OVERRIDE to decline a given span. mock_override accepts either form, or a list of them.
MockOverrideCtx
MockValue callable, and to a resolver. inputs and kwargs are the live positional and keyword arguments passed to the wrapped function on this run. This lets an override compute from what the changed code actually asked for. get_original_output is synchronous. It returns this span’s original recorded output, deserialized. It is memoized for the replay item. It raises if the span has no recorded counterpart in the replayed trace.
SpanNodeMeta
MockOverride match predicate. It carries no output payload. Matching runs on structural metadata only. span_name is the resolved name: the name option, else the function’s qualified name (Order.process for a method), falling back to trace_function_key. original_span_id is this span’s id in the original trace. It is None when the live span has no recorded counterpart, such as a span the changed code newly introduced.
MockValue
MockOverrideCtx and returns the value. None is a legitimate mocked output. Returning it substitutes None, rather than declining. Return NO_MOCK_OVERRIDE to decline instead.
A trace error means the replayed function started and raised. A replay error means Bitfab could not invoke it at all, for example because database warmup, input loading, or mock preparation failed. A replay whose delivery Bitfab cannot confirm in time is a logged warning, [bitfab] replay <original trace id>#<attempt> could not confirm all spans reached Bitfab within <N>s. The run continues; Bitfab will show this replay as capture incomplete., never a failure. If no completed item’s trace persisted at all, or finalization fails, replay() raises ReplayError. Its items, experiment_id, experiment_url, and cause preserve the partial result and the original whole-run exception. test_run_id and test_run_url are deprecated aliases carrying the same values.
If database branch resolution fails, replay_error is a DbBranchReplayError. It carries code, the server message, original_trace_id, and an optional cause. Resolver codes such as branch_create_failed, snapshot_from_replaced_origin, invalid_snapshot_ref, seeded_trace_has_no_snapshot, and internal_error remain available in memory, in progress events, in result files, and in ReplayError.items. (seeded_trace_has_no_snapshot means the source was seeded, so it pinned no database instant.) HTTP, timeout, and network failures while requesting a lease use lease_request_failed instead, with the original client exception as cause.
serialize_replay_result
trace_error and replay_error. That includes DbBranchReplayError.code, original_trace_id, and nested cause. Use it for direct-run stdout, instead of json.dumps(..., default=str). That alternative reduces exceptions to plain strings.