@bitfab/sdk. Dual ESM/CJS. Node.js ≥ 18 and modern browsers.
Module Exports
Constants
class Bitfab
new Bitfab(config: BitfabConfig)
captureEnabled
BITFAB_API_KEY fallback, and the capture switch, including the BITFAB_CAPTURE_ENABLED fallback. It is false when capture was turned off by either, or no key resolves. Under strict: true, reading it without a key throws BitfabError instead.
span / withSpan
- Returns a wrapper whether or not capture is on. With
captureEnabled: false, the wrapper runs the function untraced. It sends nothing, except inside a replay item or the one callseedTraceruns - Span
namedefaults to the function’s qualified name, resolved per call:Order.processwhen the function runs as a method of anOrderinstance (orOrder.buildfor a static method),processfor a plain function, thentraceFunctionKeywhen the function is anonymous. The rawfn.namestill travels separately asfunction_name - Span
typedefaults to"custom" experimentIdlinks the span to an experiment. If the span starts a trace, that trace is linked too.testRunIdis a deprecated alias with the same meaning, and passing both with different values throwscaptureWhen: "nested"records the span only when another Bitfab span is active. Without a parent, the wrapped function runs normally. It does not create a root trace. The default is"always". An unknown value warns once and falls back to that default- Input arguments are serialized via superjson. This preserves type information
- The return value is serialized as output, whether it is a plain value or a resolved Promise
- A value that fails to serialize is stubbed instead of dropping the span. A warning notes it may not be replayable. A span whose encoded payload still exceeds the per-request byte ceiling ships anyway, with its largest fields stubbed first and its identifying fields stubbed last. This way an oversized span degrades instead of vanishing
SpanOptions.finalize?: (result) => unknown | Promise<unknown>records a serializable view of a non-serializable result, such as a live stream. The raw result is returned to the caller unchanged.await finalize(result)is recorded as the span output instead. It runs in the background. A throwingfinalizerecords an error instead of crashing. For an async-generator result, it receives the array of yielded values once iteration completes, and its return is recorded instead of{ yielded, return }. Pair it with the exportedfinalizers.aiSdkfor the Vercel AI SDK, orfinalizers.readableStream- An async-generator span remains open until iteration completes, returns early, or throws. A nested span created inside the generator body inherits that span. To include spans created by the consumer in the same trace, wrap the controller that owns the iteration in an outer root span
- A thrown error is recorded on the span, in the
erroranderror_source: "code"fields. It is then re-thrown - Spans nest automatically, via
AsyncLocalStorageon Node or a module-level stack as a browser fallback - Spans exist only where you create them. Nesting is automatic, but only between spans that exist. One wrapper around the outermost function records a single-node trace. See Instrumentation
withSpan()is the opt-in tracing surface.trace()/withTrace()andnode()/withNode()are the opt-out surface. AwithSpan()entered beneath an active subtree trace throwsMixedTracingError. Configure a discovered call withnode()/withNode()instead- The browser fallback does not isolate concurrent async chains.
Promise.allwith independentwithSpancalls may see the wrong parent
withSpan.
Requirements and semantics:
- Requires TypeScript 5.0 or newer, and a compiler or transpiler that supports the standard ECMAScript decorator transform
experimentalDecoratorsmust be disabled or omitted. The legacy decorator transform is not supportedemitDecoratorMetadatamust be disabled or omitted. It is not compatible with standard decorators- Supports instance, static, and private methods
- Preserves the method receiver, arguments, return type, errors, nesting, and all
SpanOptions - Within opt-in tracing, prefer
withSpanto the optionalspan()decorator because it supports every TypeScript version and callable shape. For new workflows on a supported build path, start with opt-outwithTrace/traceand configure discovered calls withwithNode/node
trace / withTrace
@bitfab/sdk/transform build adapter into the application. With it, every discovered first-party call beneath the root records its full inputs, output, and thrown error by default. Without it, only the normal rich root span is recorded, and the first root to finish prints one [bitfab] warning naming the root and pointing at the adapter steps.
The root records its own function once. The transform still instruments a function passed to withTrace(), in the same file or imported from another one, and the root skips only the call it is already recording. Every other call to that function is recorded: a direct call inside another root appears as a discovered call, and withNode() configuration on the same function still applies when it runs beneath another root.
TraceOptions controls the root name and type. The root name defaults to the function’s qualified name, resolved per call (Pipeline.run for a decorated method), then the trace function key when the function is anonymous. It also controls maxDepth (default 30), maxCapturedSubtreeSpans (default 500), qualified or simple exclude names, and includeWrappers (default false). An excluded call is omitted. Its captured descendants attach to the nearest captured parent instead.
There is no cap on the total number of spans one trace root sends. maxSpans was removed from TraceOptions, so passing it is a type error.
maxCapturedSubtreeSpans (default 500) caps discovered calls sent with their inputs and outputs. It does not count calls configured with node() or withNode(), nested trace roots, or calls the loaded sim plan turned capture off for, so those keep recording past it. Calls 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 nothing.
Hitting a limit warns once, and when the root finishes the trace carries the flat metadata keys bitfab.truncated_by (the limits that fired, in the order max_depth, max_captured_subtree_spans) and bitfab.dropped_spans (how many calls were not recorded), so truncated traces can be filtered.
A top-level root whose function is declared async waits, for at most five seconds, when it starts while the client’s first sim plan read is still running, so its spans record with the plan applied. Once that first read has finished or the wait has run out, no root waits again. Synchronous roots, functions that return a promise without being declared async, and nested roots never wait. If that first read fails, spans are sent without their inputs and outputs until a read succeeds, except each trace’s root span, spans recorded by a framework integration, calls configured with node() or withNode(), and nested trace roots. Those spans still count toward maxCapturedSubtreeSpans.
A subtree root entered beneath another subtree root starts its own independent trace while every enclosing root keeps recording. The nested root’s function and its whole subtree appear in each enclosing trace with separate span IDs and the shape that root 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. Each root applies its own maxDepth, maxCapturedSubtreeSpans, and explicit exclusions to its copy. node() and withNode() configuration applies in every copy, including experimentId and the finalized output, with finalize running once per call. Framework integration spans attach inside the innermost trace only. The enclosing trace’s span for the nested root carries nested_trace_id, nested_trace_function_key, and nested_root_span_id. The nested root span carries enclosing_trace_id, enclosing_span_id, and enclosing_trace_function_key. Inside replay() or seedTrace() a nested root starts no trace of its own and the item’s trace records it as an ordinary descendant.
Set mockOnReplayDefault: true on the trace options to make replay mocking the default for every automatically captured node. This applies under the default mock: "marked" strategy. A configured node with mockOnReplay: false overrides that default and runs live. The option defaults to false. It does not change mock: "all" behavior.
Span inclusion is opt-out. Every eligible discovered call is recorded with rich content by default. exclude and node({ capture: false }) omit calls explicitly. A sim-plan capture-off decision stops a node’s spans from being sent, while the calls beneath them are still recorded. Calls configured with node() or withNode() ignore the sim plan and keep their inputs and outputs.
trace() and node() are the opt-out tracing surface. withSpan() is the opt-in surface. A subtree root entered beneath an active withSpan() throws MixedTracingError. So does a withSpan() entered beneath a subtree root. Framework integration spans never trip the check. The LangGraph, OpenAI Agents, and Vercel AI SDK wrappers open their spans on the surrounding surface, and the callback-based handlers emit spans directly. The root span that replay() and seedTrace() wrap around an undecorated callable belongs to neither surface, so a subtree root called from that callable nests beneath it.
node / withNode
trace() or withTrace() call. It never creates a span or trace by itself. It inherits the enclosing trace function key.
Semantics:
- Outside an active subtree trace, the method runs normally with no capture or replay behavior. Beneath an active
withSpan()with no enclosing trace, it throwsMixedTracingErrorinstead.node()belongs to the opt-out surface, so it has no trace to configure there capture: true(the default) keeps the call captured, with its inputs, output, and errorsname,type,experimentId,mockOnReplay, andfinalizehave the same behavior as theirSpanOptionscounterparts- Under a trace with
mockOnReplayDefault: true, an omitted node policy inherits the trace default. SettingmockOnReplay: falseon the node keeps it live instead, under the default"marked"replay strategy capture: falseomits the call. Its captured descendants attach to the nearest captured parent insteadcapture: falsewithmockOnReplay: truethrowsBitfabError, because an uncaptured call has no recorded output- Requires a compatible
@bitfab/sdk/transformintegration to discover the method. Standard and legacy method decorator output are accepted when the transform runs before decorators are lowered
node(). The wrapped function remains eligible for the automatic subtree transform, including when the same function is also passed to withTrace() or withSpan(). A withTrace() root wrapped around a withNode() function, in either order, records one span per call: the root, with the root’s name and type. The node settings apply when the function runs as a node beneath a different root. Without an active trace() or withTrace() call, it runs normally and never creates a span or trace. Beneath an active withSpan(), it throws MixedTracingError instead.
The function must have a stable name (a referenced function binding or named function expression). Passing an anonymous inline function throws BitfabError, because the transform could not safely bind its configuration to one discovered call.
On a generated BAML client call, pass the bound method (b.ClassifyText.bind(b)) or a wrapBAML wrapper. The options then apply to that BAML call’s one llm span, and an omitted type stays "llm". See BAML under a trace root.
getFunction
BitfabFunction bound to traceFunctionKey.
wrapBAML
Framework integration → see BAML framework guide for examples.
WrappedBamlFn, an async function with a .collector property set after each call.
Throws:
BitfabErrorif form 1 is used withoutbamlClientin constructorBitfabErrorif the method has no.name
wrapJev
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. Opens no span of its own. Beneath a withTrace() root, a call through @typesafe-ai/sdk is already recorded as its own llm span, so the wrapper records nothing more and withNode() on it configures that span.
Throws:
BitfabErrorif the method has no.name(bind the client’s method:jev.systemOne.bind(jev))
- If
@boundaryml/bamlcannot be loaded, the method is called directly,onCollectoris not called,.collectorisnull, and a one-time[bitfab]warning is logged. In bundled server code (Next.js with Turbopack, for example),import "@bitfab/sdk/baml"once so the SDK can load it - Otherwise, it creates a BAML
Collector, calls the method through a tracked client, then:- Calls
getCurrentSpan().setPrompt(...)with the rendered messages as JSON - Calls
getCurrentSpan().addContext({ model, provider, inputTokens, outputTokens, durationMs })
- Calls
- The
onCollectorcallback fires after each invocation. Errors in the callback are swallowed - Beneath a
withTrace()/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. Passing the wrapper towithNode()configures that one span. See BAML under a trace root
wrapOpenRouter
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. Opens no span of its own. Beneath a withTrace() root, each .chat.completions.create(...) call in code compiled by the build transform, on a client whose base URL is openrouter.ai, is already recorded as its own llm span named Completions.create, so the wrapper records nothing more and withNode() on it configures that span. The client’s own APIPromise is handed back from those calls, so .withResponse() keeps working. stream: true calls are not recorded.
Throws:
BitfabErrorif the method has no.name(bind the client’s method:openai.chat.completions.create.bind(openai.chat.completions))
wrapAnthropic
Framework integration → see Anthropic framework guide for examples.
system and messages (when the first argument is the request) 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. Opens no span of its own. Beneath a withTrace() root, each .messages.create(...) call in code compiled by the build transform is already recorded as its own llm span named Messages.create, so the wrapper records nothing more and withNode() on it configures that span. The client’s own APIPromise is handed back from those calls, so .withResponse() keeps working. stream: true calls are not recorded.
Throws:
BitfabErrorif the method has no.name(bind the client’s method:anthropic.messages.create.bind(anthropic.messages))
wrapOpenAIResponses
Framework integration → see OpenAI Responses API framework guide for examples.
instructions and input (when the first argument is the request) 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. Opens no span of its own. Beneath a withTrace() root, each .responses.create(...) call in code compiled by the build transform, on any openai client, is already recorded as its own llm span named Responses.create, so the wrapper records nothing more and withNode() on it configures that span. The client’s own APIPromise is handed back from those calls, so .withResponse() keeps working. stream: true calls are not recorded.
Throws:
BitfabErrorif the method has no.name(bind the client’s method:openai.responses.create.bind(openai.responses))
getTrace
DetachedTrace handle for annotating a trace after its root span has closed, from any process or thread.
Throws: BitfabError if traceId is not a canonical Bitfab trace ID.
Semantics:
- All methods on the returned handle block (return
Promise<void>), resolving once the server has applied the change - When capture is off outside a replay item or a
seedTracecall, methods returnPromise.resolve()immediately - The server returns 404 if no trace exists with that ID. The returned promise rejects with a
BitfabErrorin that case, rather than logging it
getTraceSpan
traceId is the canonical Bitfab trace ID. lookup is either { id }, using the span’s Bitfab ID, or { name?, type?, occurrence? } with at least one of name or type. type is one of "llm", "agent", "function", "guardrail", "handoff", or "custom". When both name and type are given, a span must match both. occurrence is "first" | "last" | number. It defaults to "last". Numeric occurrences are zero-based in start-time order. An id combined with name or type does not compile, and is refused at runtime with Provide id, or name and/or type before any request. An unknown type is refused the same way. Returns null when no trace or span matches.
getSpan
null when no span with that ID is in the API key’s organization.
Throws: BitfabError if spanId is empty.
datasets
saveis an upsert keyed on(traceFunctionKey, name).createdistruefor a new dataset andfalsewhen an existing one was updated. An omitteddescriptionleaves the existing one untouched.listtakes an optionaltraceFunctionKey. Without it, every dataset in the organization is returned.updaterenames a dataset or changes its description in place and resolves to the updated dataset, which keeps its id, traces, and graders.UpdateDatasetParamsis{ name?: string, description?: string, golden?: boolean }. An omitted field is left untouched and an emptydescriptionclears it. Passing none of them rejects withBitfabErrorbefore any request. A blank name, or a name another dataset under the same trace function already uses, rejects with a 400BitfabError.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 that is listed first in the app, inlist_datasets, and inlist.golden: falseunmarks it, and omitting it leaves it as it is. Every dataset carriesgolden, read asfalsefrom an older server.listTracesreturns{ datasetId, traceIds }, the same membership a replay withdatasetIdselects.addTracesandaddGradersaccept 1 to 100 ids. They report partial acceptance instead of rejecting the whole call. An id outside the organization, or under another trace function, comes back inskippedTraceIdsorskippedGraderIds. An id already present comes back inalreadyPresentTraceIdsoralreadyAssignedGraderIds.removeTracesnever deletes a trace, only its membership in the dataset. Ids that were not members come back innotPresentTraceIds.removeGradersreportsnotAssignedGraderIdsthe same way.rerunGradersre-scores every trace in the dataset.graderIdsdefaults to every assigned grader. An unassigned id rejects the call. The promise resolves once the run completes. Hold it and await it when you need the result, since nothing blocks while it waits. It rejects withGraderRerunErrorwhen the run errored, carrying the run, and withGraderRerunTimeoutErrorwhen the run is still going aftertimeoutMs(default 90,000). A timeout does not stop the run on Bitfab, so read it later withgetGraderRerun. A request matching an in-flight run joins it, reported asjoinedExisting: true.GraderRerunhasstatus(pending|running|completed|errored),graderIds,progress({ completedTraces, totalTraces, graderCount }while running),result({ tracesGraded, gradersRun }when completed), anderror.- A dataset id from another organization rejects with a 404
BitfabError.
traces
search finds traces by the caller metadata your code stamped with setMetadata, by the name it set with setName, or by the experiment that produced them.
callerMetadata 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. nameContains 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. experimentId returns only the replay traces that experiment produced. At least one of callerMetadata, name, nameContains, or experimentId is required, so a search never returns unfiltered traces. TypeScript states that requirement in the type, so a call with no filter does not compile. limit defaults to 50 and is capped at 500; use the returned nextCursor for pagination.
An assertion says what should happen when a trace is replayed.
getAssertionsreturns{ assertions, inheritedFrom, generation }.generationreports the trace’s latest assertion generation, ornullwhen generation never ran on that trace. Attach assertions to the ORIGINAL trace. Reading a replay that has none of its own returns the nearest ancestor’s assertions instead, resolved through the replay lineage. That ancestor trace is named ininheritedFrom.inheritedFromisnullwhen the assertions are the trace’s own. Writing assertions through a replay trace id is refused, and the error names the original to retry with.- The common case is to omit
targetOnEvaluatedTrace. Omitting it checks the whole trace. Set it to narrow the check instead, to either{ kind: "output" }or{ kind: "span", name, occurrence? }. - Targets are span names, never span ids, because an id captured on the original resolves to nothing on the replay.
occurrenceaccepts"first","last"(the default), or a 0-based index for a span the trace calls more than once. assertion,passCriteria, andfailCriteriaare the same three fieldssave_gradertakes, so an assertion that proves out across many traces is promoted into a grader by copying them.humanNoteis 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 passnullto clear it. It must never be used as evidence when judging a replay.- Passing an entry’s
idedits that assertion. Omitting it adds a new one instead. This way two callers adding different assertions to one trace never overwrite each other.archiveAssertionshides rows from every read. It keeps them for audit. saveAssertions’sourcedefaults to"agent"when omitted, recording that a coding agent, not a person, authored the assertion.justificationis 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.categoryJustificationdoes the same forcategory_id, saying why that category fits. Omit either on an edit to preserve it. Changing the category clearscategoryJustification, since it justified the old one.assigneeEmailnames 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 passnullto leave it to nobody. Reads carry a nullableassigneewithid,fullName,email, andimageUrl. 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, andapprovedAt. 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. - The save returns once the text is saved as sent. Bitfab then checks each entry that carries
assertiontext against the writing rules in the background and, when the text breaks one, rewrites it in place as one well-formed assertion, usually within a minute. Each returned assertion carriesrewrite, whosestatusispendingfor an entry being checked andnot_checkedfor one that is not. Read the result later withgetAssertions: each row carries its latestrewrite, withstatus(pending,rewritten,unchanged, orcheck_failed, which also covers a check unfinished 15 minutes after the save),sentAssertion,changes,separateAssertions,unresolved, anderror, ornullwhen no check ever ran on it.separateAssertionsholds other claims the sent text bundled. They were not saved, so save each as its own assertion. PassskipGuidedRewrite: trueto save text exactly as sent, with no check. saveAssertionswrites one trace.saveAssertionsAlltakes 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.- The server writes the whole batch in one transaction, so a rejected batch writes nothing and there is no half-saved state to reconcile. Results come back as one flat array covering every trace, each row carrying its own
traceId. A batch takes up to 500 traces, up to 50 assertions per trace, and at most 1000 assertions in total. Passing an emptyupdatesarray writes nothing and sends no request.
Generate assertions
generateAssertions 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.
guidanceis 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.categoryIdslimits 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
guidanceorcategoryIds. Passregenerate: trueto start over. The previous generated drafts are archived once the new ones are saved. Assertions a person wrote are never replaced. - A trace runs one generation at a time. Calling
generateAssertionswhile one is running joins it and never starts a second one. Passingregenerate: truewhile one is running joins it too. - The promise resolves with the drafts once generation finishes. Hold it and await it when you need the drafts, or start several traces at once and
Promise.allthem. It rejects withAssertionGenerationErrorwhen generation failed, carryingtraceId. Passregenerate: trueto try again. It rejects withAssertionGenerationTimeoutErrorwhen generation has not finished withintimeoutMs(default 600,000). A timeout does not stop generation on Bitfab. - To check on a generation from anywhere, call
getAssertionsand readgeneration. It carriesstatus("running","completed", or"failed"),error,assertionIds(the drafts the latest completed generation saved, in order),startedAt, andfinishedAt. It isnullwhen generation never ran on that trace. - A replay trace id is refused, and the error names the original to retry with.
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. It can also record what the run actually did there, as one example, but never what the run should have done.getScenario reads the scenario built from a trace, and saveScenario updates it.
name,description,trigger, anddataeach replace the saved value whole. Fields left out are kept.appendDataadds to the saved data without resending it: itshistoryentries go after the saved ones and itsmissingitems after the saved missing items. Appends made at the same time by different callers all land.- Send
dataorappendData, not both. Passing both, or nothing besidestraceId, is a type error. actualOutcomeis what the run actually did: a shortsummaryand theactionsit took, each with its arguments indetails, what came back inresult, whether itfailed, and when it happened inat. It is one example of a response, never what the run should have done. Bitfab never fills it in, so it is only ever what a caller saved. It replaces the saved value whole,nullclears it, and leaving it out keeps it. A save that sends onlyactualOutcome,nullincluded, is valid.actualOutcome.sourcenames the span the outcome was read from, on a trace linked to the scenario. Leave it out or set it tonullwhen it was not read from one. Every scenario read back carriesactualOutcome,nullwhen none was saved.categoryIdsfiles the scenario under any number of categories, by id fromclient.categories. It replaces the whole set: leave it out to keep the current set, and pass an empty array to clear it. An id outside the API key’s organization fails the save with a 400BitfabErrornaming the missing ids. Every scenario read back carriescategories, each aCategorySummary, sorted by title.- A history entry’s
sourcenames the span its text came from, on this trace or another trace linked to the same scenario. Leave it out or set it tonullwhen the text is not in such a trace. Asourcenaming a span on any other trace is refused. - The server rebuilds
triggerSummaryanddataSummaryfrom what is saved, and the call resolves with the saved scenario. saveScenarioonly updates a scenario that already exists. A trace with no scenario rejects with a 404BitfabError, from either method.
spans
list reads one trace’s spans in the order they ran, one page at a time. traceId is the canonical Bitfab trace ID and is checked before any request. No filter is required: spanType matches the span type and spanName the exact span name, and omitting both pages through every span. Each span carries its parent and end time, plus only the fields listed in fields, and none by default; a requested field with no content comes back as null. limit defaults to 50 and is capped at 200, or at 50 when fields is set. Pass nextCursor back as cursor to continue. A trace outside the organization is rejected with a 404 BitfabError.
organizationMembers
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 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 and verdicts remain intact.
Pass category_id on entries in saveAssertions or saveAssertionsAll to assign a category. Omit the field to preserve an existing assignment. Pass null 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, from inside a replay process rather than a coding-agent session.
These were named client.labels (with generateLabelEvidence), graders.getLabels, saveLabel, and saveLabelAll, with Label* and GraderLabel* types and a label: boolean write field. The old names are deprecated and still work for one minor: each is the same client, method, or type, a label sent to a write is sent as verdict, and the old response fields (label, labelStatus, labelSource, labelReason, labelConfidence) are still filled. The old names return the new action value no-active-verdict: an archive that finds nothing to clear reports no-active-verdict, never no-active-label, even when called through client.labels, because it is the same client. The deprecated abstain: true write field is sent as verdict: "abstained". Each deprecated write field warns once per process. An entry carrying both verdict and label or abstain throws BitfabError before any request, and so does an entry with abstain: false and no verdict. A skip or an archive that also carries verdict, label, or abstain is a type error. An abstained verdict that carries confidence or evidence, and an entry that carries both the deprecated label and abstain, throw a BitfabError before any request.
- Key a direct verdict by
traceId. Key a replay verdict byoriginalTraceIdplus theexperimentIdit ran under, addingattemptwhen the experiment ran each trace more than once. verdictis"pass","fail", or"abstained". A pass or fail takes a requiredannotationand optionalconfidenceandevidence. An abstention takes an optionalannotation.- Every agent verdict scores one approved assertion, so
save,saveAll, andskiprequireassertionId. Leaving it out is a type error, and at runtime the call throwsBitfabErrorbefore any request. Verdicts are only written on assertions, never on a whole trace.archiveis the one write that may omit it, which clears an old whole-trace agent verdict.saveHumanandsaveHumanAllrequire it too, enforced the same way. skip(target, experimentId?)withholds a verdict.archive(target, experimentId?)clears a previous one.VerdictTargettakes eithertraceIdororiginalTraceIdwith optionalattempt, plus a requiredassertionId.ArchiveVerdictTargetis the same withassertionIdoptional. Batch updates can still use{ skip: true }and{ archive: true }. Skip is the right answer for an attempt that crashed, was punctured, or fell back to a schema default. It is also right for an assertion whose target could not be resolved.- A withheld verdict takes an
annotationsaying why, stored on it and returned bygetandgetAll. Without one, a skip reads the same whether nobody could decide or nobody looked, and a skip sent without an annotation clears the note the previous verdict carried. abstain(target, experimentId?)records an abstention, the same write as{ verdict: "abstained" }in a batch. 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. Passannotationto say what the evidence would have had to show. It reads back asverdict: "abstained", and the trace’sabstainedcount says how many of its failed verdicts are abstentions.VerdictOutcome.keyechoes the id you addressed the verdict by, so a caller can verify persistence without ever holding a server-generated replay trace id.getandgetAllread by trace ID only. After writing a replay verdict, pass the returnedVerdictOutcome.traceIdtogetto 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. Passnullto clear it.getandgetAllreturn the evidence with each whole-trace and per-assertion verdict. generateEvidencegenerates assertion verdict evidence, the same call as thegenerate_verdict_evidenceMCP 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 containsspanId,text,sourceField(the span field the quote was copied from),spanName, signature-onlyparameters(names and runtime types, never values),spanType, andisMocked. Save only the candidate’s{ spanId, text }fields as verdict evidence; saved evidence remains onverdicts.getandverdicts.getAll.
saveHuman and saveHumanAll write the verdicts save_human_verdicts writes over MCP, recorded as written by a person. Every entry names the assertion it scores with assertionId: leaving it out is a type error, and at runtime the call throws BitfabError before any request. 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. A person passes or fails, never abstains. Verdicts have no approval step, so both kinds count as soon as they are written.
Like saveAll, the batch is all-or-nothing. A trace outside the organization, a repeated target, or an assertionId that is not active on its trace rejects the whole call and writes nothing, so a thrown error never leaves part of the batch committed.
get and getAll read verdicts back. Each trace carries its effective verdict plus one row per scored assertion, keyed by the same assertionId the write used, so a per-assertion verdict is verifiable per assertion rather than as a passed/failed tally. get returns null when the trace is not in this organization. One getAll call accepts up to 100 ids.
graders
get_grader_verdicts MCP tool returns. Pass traceIds to see every grader’s verdict on those traces, graderId to see one grader’s most recent verdicts across traces, or both to narrow. Passing neither raises. This is the per-grader detail that verdicts.get does not carry, since that returns one grader-agnostic verdict per trace.
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 getVerdicts reads it back. The same write as the save_grader_verdicts MCP tool. A grader passes or fails, never abstains. saveVerdictAll 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.
experiments
list pages newest first; every filter is optional and they combine with AND, and an unfiltered call with limit: 1 returns the latest run. metadata matches by containment. get, getRollup, and getRollupAll reject an id outside the organization with a 404 BitfabError; getRollupAll fails the whole batch and names the unknown ids. getRollupAll([]) returns [] without a request, and any other batch is one request.
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.
defineReplayRegistry preserves the registry’s inferred TypeScript type. Each entry contains a client, the exact production fn, an optional traceFunctionKey for a plain handler root, static options, and an optional optionsFactory({ params }). A withTrace- or withSpan-wrapped function supplies its key automatically. The factory receives JSON values from --params and --param. It can construct executable options such as mockOverride. Direct parameters override file values. Registry options exclude lifecycle callbacks, because the installed command owns progress reporting.
Entries are checked when the registry loads. An entry with any field other than client, fn, traceFunctionKey, options, and optionsFactory logs a one-time warning that names the field. The field is ignored. A later SDK release will reject unknown fields. Replay options such as adaptInputs and mock go inside options.
A traced function bound with .bind(thisArg) keeps its trace function key. A decorated method therefore registers as instance.method.bind(instance) with no traceFunctionKey, and replay records it as the root. SDK versions 0.64.1 and earlier drop the key on .bind() and nest the replay under a second root with the same name.
When the registry cannot load one of its imports, bitfab-replay keeps the original error message and adds a note after it. The note says a registry belongs in the package that builds the traced function’s call, and that bitfab-replay runs from that package’s directory.
The package installs bitfab-replay. Run bitfab-replay --registry <path> <pipeline> [options]. The command loads .ts registries directly and loads @bitfab/sdk/transform/register itself, so replays record the calls beneath each withTrace root. If your application already preloads both loaders, put tsx before the transform; it is not applied twice. If the transform cannot load, the command warns once and records each replay’s root span only.
When assertion-only selection and a limit narrow an explicit ID list or dataset, the command reads approved assertions in selection order until the bound is filled. An assertion still awaiting review does not make a trace eligible. Inherited assertions do not make a replay trace eligible. A failed assertion read aborts selection. The server revalidates the pinned IDs, so assertions changed concurrently can still reduce the final count.
Command values override overlapping scalar defaults without removing unrelated executable options. The command wires both lifecycle callbacks to
reportReplayProgress, so each @@bitfab:progress line carries experimentId, plus the deprecated testRunId with the same value. Right after the experiment starts, the command prints [replay] Experiment <id>: <url> to stderr. If replay then fails, it prints Experiment <id>: <url> again just before the error message, so the experiment ID is never lost. After that experiment line, a failed run also prints Resume with: bitfab-replay --registry <path> <pipeline> --resume <experimentId>, 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 onInterrupt set in the registry is replaced by the command’s own. It writes the human summary to stderr. It writes the full serialized ReplayResult to stdout. A run whose selection matched no traces exits non-zero, rather than reporting a clean run of zero items.
The stderr summary counts items by their source’s ingestionType. A captured item’s recorded output is a previous run, so it counts as Same or Changed. A seeded item’s is the value the case expected, so it counts as Matched expected or Missed expected. One run can replay both. Both pairs print when both are present. An item with an error counts under Errors instead.
Under --resume, the summary counts only the items that ran now as replayed. Items the earlier run finished print on their own line, as carried over from the earlier run.
--seed <cases.jsonl> seeds cases through the same registration instead of replaying. Each line is a JSON object with an input array plus optional expected, metadata, and sessionId. A JSON array of those objects works too. The registration already holds the client, the callable, and the trace function key. Because of that, a seeded case is written against the exact function the later replay selects. A case that cannot supply the function’s required arguments is rejected at seed time instead.
Add --run to run each case once through the registered function and record the execution, rather than writing the case directly. The output is then what the run produced, so a case carrying expected is rejected. The registration’s adaptInputs is a replay hook. It is not applied at seed time, so a seeded trace is never adapted twice.
seedTrace
replay({ traceIds: [...] }). The recorded root span carries input as its input and expected as its output, so replay reports each item against the value you expected rather than against a previous run.
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. spanName labels the root span only.
Use it to turn a corpus you already hold, such as a dataset export, a spreadsheet, or hand-written cases, into traces. Passing fn checks the case against the function’s required argument count, so a case that could never run fails here instead of at replay. Omit it when the seeding script cannot import the callable.
A trace seeded from a case has no child spans and no database pin. Because of that, replay mocking has nothing recorded to substitute. dbBranch refuses it for the same reason. Supply mockOverride at replay time for calls that must not run.
Seeding by running once
fn once. Records the execution as an original trace. Returns the trace ID for use with replay({ traceIds: [...] }). Capture stays off. This records exactly one call, with the same semantics capture-on would give it: a root span, a first-party subtree bounded by the enclosing withTrace root, and no mocking. The recorded input is the real call. The output is what the run produced. There is no expected to pass as a result. The trace lands under traceFunctionKey, with ingestion_type: seeded. replay selects it. Each replay links back to it as originalTraceId.
fn resolves exactly as it does for replay. A withTrace- or withSpan-wrapped function records under its own key, and that key must match traceFunctionKey. A plain callable is wrapped under the key given here instead. An exception is recorded on the root span. The trace still persists. The exception is re-thrown. A call that records nothing, because no API key resolved, rejects rather than returning an ID replay could never find.
metadata is stored on the trace. It is handed to a later replay’s adaptInputs hook as ctx.metadata. This way a case’s provenance rides 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.
Unlike a trace seeded from a case, this one has a full recorded subtree, so replay mocking works as it does for a captured trace. It still carries no database pin, so dbBranch refuses it.
reseedTrace
fn once exactly as seedTrace would, then asks Bitfab to adopt that run under traceId. The trace keeps its id, verdicts, assertions, dataset membership, name, and metadata. The previous run is kept as its own trace, previousRunTraceId, with reseedOfTraceId pointing back at traceId. Nothing is mocked and no experiment is created.
fn resolves as it does for replay and seedTrace, and traceFunctionKey must match the trace’s own key. A run that throws is recorded but not adopted, and the error is re-thrown. 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.
reseedFromRegistry
bitfab-seed --registry <path> <pipeline> --from-trace <id>[,<id>...] command calls it; bitfab-seed --cases <cases.jsonl> [--run] is the home of what bitfab-replay --seed does, and that spelling still works.
seedFromRegistry
{ pipeline, traceFunctionKey, traceIds }. With { run: true }, each case is run once through the registered function. The execution is recorded instead of the case, so a case carrying expected is rejected.
registerMockOverride
{ match, value }, both the key and matcher must match. A global resolver can route on ctx.node.traceFunctionKey. Return NO_MOCK_OVERRIDE to continue to the next override and then the replay’s base mock strategy. A synchronous span requires a synchronous resolver result, including a synchronous sentinel decline. A mixed-tree global resolver can return the sentinel synchronously for sync keys, and a Promise for async keys. undefined and null remain valid mocked outputs. Per-call overrides take precedence over registered overrides.
clearMockOverrides
replay
Returns:
{ items, experimentId, experimentUrl, attempts }, plus the deprecated aliases testRunId and testRunUrl with the same values. See ReplayResult.
Notes:
- A trace replays only when its root span has serializable inputs. It can also replay when it was instrumented through a framework handler. That handler’s recorded root input is serializable. If the original inputs were stubbed as non-serializable at capture time, the trace cannot be replayed
fnmay already be awithTrace- orwithSpan-wrapped function. That function carries its trace function key and is used as-is.fnmay instead be a plain callable.replay()wraps that callable under the key automatically. Either way, new spans link to the experiment via async context. Don’t wrap an already-wrapped function in a fresh closure. The closure has no trace function key, soreplay()wraps the closure as the root, and the inner wrapped function records a second root, nesting a duplicate- Inputs are deserialized from historical spans and passed positionally
- When
codeChangeFilesis omitted,replay()auto-captures the code change. By default it captures a rename-aware diff of uncommitted working-tree changes (including untracked files) againstHEAD. When the working tree is clean, it diffs against the branch yours was cut from instead (the parent branch in a stack, otherwise the point where you split from the default branch:origin/HEAD, elseorigin/main,origin/master,main, ormaster), so changes you already committed still show. The diff is bounded to 60 files, 500,000 bytes per file, and 2,000,000 bytes total. SetBITFAB_CODE_CHANGE_PATHto point at a JSON file withdescriptionand/orfilesinstead, and that file wins over the git diff.BITFAB_CODE_CHANGE_BASEdiffs against the merge-base with that ref instead (for exampleorigin/main), which includes committed branch changes.BITFAB_DISABLE_CODE_CHANGE_CAPTUREopts the whole process out of auto-capture. PassingcodeChangeFiles: nullopts out one call instead - When the Bitfab plugin sets
BITFAB_REPLAY_RESULT_PATH,replay()writes the fullReplayResult, usingserializeReplayResult, to that file after the run completes. A caller does not have to hand-parse stdout as a result. A write failure is logged. It never throws resumepicks 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
resumethrows aBitfabErrorwhen combined withtraceIds,limit,datasetId,datasetIds,attempts,graderIds,onlyWithAssertions, ordryRun - Database snapshots follow the experiment’s stored
dbBranchsetting, including its compute size and warm-up. AdbBranchpassed withresumethat disagrees is ignored, with a console warning - The finished pairs come back first in
items, withcarriedOver: true. They carryoriginalTraceId,attempt, and the replaytraceId. Their input, result, outputs, timings, and tokens are empty ornull. Theirerroris set when the pair errored in the earlier run.onItemStartandonItemFinishdo not fire for them, but the running totals count them - A trace that was deleted after the experiment started is skipped, with a console warning naming it
- The server refuses a resume, with a 400 and a message saying why, when the experiment is already completed or failed, was started before resume was available, or was started as an experimental selective replay. It also refuses when the experiment was started, resumed, or received replay results in the last two minutes, since the run that started it may still be running. It also refuses when the code running now differs from the code the experiment started from, either in the working tree it ran or in its code change files. When the working tree differs, the message names the commit and tree the experiment started from (which includes any uncommitted changes it ran with), so you can return your working tree to that state your own way and resume again.
force: trueskips the last two refusals, never the others. An interrupted experiment is accepted right away, without the two-minute wait, and set back topendingfor the resumed run - 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
- Once the experiment is created, a replay that stops before it finishes marks the experiment
interruptedthroughPOST /api/sdk/replay/interrupt, waiting up to five seconds for an answer. A failed request (an error status, a timeout, or no connection) is never thrown. Only apendingexperiment moves, so a completed one stays completed - On SIGINT or SIGTERM in Node,
replay()marks the experiment interrupted, then callsonInterruptand leaves the rest to it. The run keeps going if the callback returns, so exit from it to stop. WithoutonInterrupt, it prints[replay] Experiment <id> interrupted.to stderr, or[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.when the request failed, then re-sends the signal so the process ends as it normally would. When you installed your own handler for that signal, it is left to your handler instead. The listeners are removed 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 they are removed when the run ends - When the run throws before it finishes,
replay()marks the experiment interrupted and rethrows the same error.onInterruptis 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
call
envVars.
Throws: BitfabError on lookup failure or execution error.
Framework Integrations
Handlers returned by these methods are framework-native adapters. They plug into each framework’s own callback, processor, or hook surface, and emit Bitfab spans automatically. They reuse the owningBitfab client’s lazy OTel worker, so client.close() releases the withSpan and framework transport together. Directly constructed handlers own their transport and expose close(timeoutMs?) instead. For usage examples and semantics, see the per-framework guides. Signatures here are canonical.
All client-owned adapters honor captureEnabled. 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 seedTrace remain explicit recording operations. Directly constructed handlers with no usable API key are inert.
getLangGraphCallbackHandler
config.callbacks when invoking a graph or chain. Root framework invocations are registered immediately as pending external traces. They are completed when the root callback ends. The handler-created root is replayable from the framework input, so a separate withSpan root is only needed for meaningful surrounding application work. See LangGraph framework guide.
Aliased as getLangChainCallbackHandler(traceFunctionKey) for plain LangChain projects. The returned handler and behavior are identical. The handler class is also exported as BitfabLangChainCallbackHandler.
getLangGraphIntegration
wrapTools(tools) wraps each tool’s public invoke() boundary, for a hand-built ToolNode. For an agent built with LangChain’s createAgent or Deep Agents’ createDeepAgent, pass integration.middleware in middleware: [...] instead; it routes each tool call through the same replay boundary. await integration.createAgent(params) and await integration.createDeepAgent(params) are drop-ins for those two builders: they pass every parameter through, add middleware once (and, for createDeepAgent, to each subagent given as a spec; compiled runnable and remote graphId subagents pass through unchanged), and return the agent traced by instrument(). They are async because they load langchain or deepagents, both optional SDK peers. Their result is typed as BitfabLangGraphAgent, since the SDK cannot reference an optional package’s types; pass the framework’s own agent type as the type argument (for example createDeepAgent<DeepAgent>(...), with DeepAgent from deepagents) to keep its exact types, or build the agent yourself and pass it to instrument(), which returns the type it was given.
instrument(graph) returns the same graph (a proxy, so instanceof still holds and it can be nested as a node or exported to LangGraph Platform) with every way of running it traced: invoke, stream, streamEvents, and batch. Each adds callbackHandler, records a replayable root span carrying only the graph input, and records the final graph state as the root’s output. A stream’s root span stays open until the stream finishes. withConfig() returns the reconfigured graph still traced, and every other property 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 (input) => graph.invoke(input) or (input) => graph.stream(input); 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 MemorySaver lent to the run through LangGraph’s run config, so the replayed run sees the same history and resumes the same interrupt without writing to the graph’s own checkpointer. Inside a withTrace() 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. A tool call that reaches middleware outside a trace for this key, for example because the agent was invoked directly, runs normally and is not recorded as a replayable span; the SDK logs a one-time warning naming the fix, and the callback handler records the call as a function span. Tool spans recorded through middleware or wrapTools() attach under the graph’s tools step that ran them.
instrument() adds callbackHandler through LangGraph’s public withConfig() API, preserving invocation-time config and existing callbacks. The earlier createInvoker(graph) is deprecated: it logs a one-time warning and does the same as instrument(graph).invoke. Use the lower-level callbackHandler and wrapInvoke(fn) when meaningful application work around the graph invocation belongs inside the trace. 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, so repeated concurrent calls to the same tool are not yet recommended. Install @langchain/core and @langchain/langgraph. Both are optional SDK peers. See the LangGraph framework guide and current limitations.
getDeepAgentsIntegration
getLangGraphIntegration, named for LangChain Deep Agents. Build the agent with its createDeepAgent(params), which takes Deep Agents’ own parameters, 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. Install deepagents and langchain. See the Deep Agents framework guide.
getRivetIntegration
withTrace() root and returns a replay-aware proxy through getContext() inside it. bind(tracedFn) adapts the traced function into a Rivet handler whose first argument is the live context; withContext(context, callback) is the lower-level equivalent. Neither method creates a trace or span.
The proxy snapshots pre-turn actor or workflow state on the root and records fixed database, KV, queue, schedule, cron, run-wakeup, persistence, broadcast, sleep, destroy, and public workflow primitive calls as inherited child spans. Replay invokes the exact traced root with no live actor, hydrates the snapshot, and serves recorded outputs only for paths explicitly listed in mockOnReplay. Database transaction callbacks and queue iterators rerun through their inner effect proxies. A replay-mocked workflow primitive returns its recorded output without invoking its callback. Overrides receive callback values from the current replay call in inputs; callbacks are not serialized. The default is empty. Missing fixtures, missing selected effect occurrences, unselected effects, and live resources without virtual-handle semantics fail closed. See the Rivet framework guide.
getOpenAiTracingProcessor
@openai/agents’ addTraceProcessor. This keeps the SDK’s default OpenAI exporter. setTraceProcessors replaces it instead. The processor captures agent internals. Pair it with getOpenAiAgentHandler for a replayable root. See OpenAI Agents framework guide.
getOpenAiAgentHandler
wrapRun(agent, input, options?) is a drop-in for @openai/agents’ run(). It records a keyed, replayable root span carrying the run input. The tracing processor’s spans nest underneath it. Called from inside an already-active span, it runs run() directly instead of opening a second root. Pass { stream: true } to options for a streamed run instead. The result is handed back immediately. The final output is recorded on the span once the stream drains. First-byte latency is untouched as a result. See OpenAI Agents framework guide.
getClaudeAgentHandler
instrumentOptions(options), wrapResponse(stream, opts?), and wrapQuery(stream, opts?) for the Claude Agent SDK. Wrap query()’s async iterator with wrapQuery. wrapResponse exists for naming symmetry with the Python SDK’s wrapper around ClaudeSDKClient.receiveResponse(). TypeScript has no equivalent of that method to wrap directly. Pass { input: prompt } to the wrap call to record a replayable root span. See Claude Agent SDK framework guide.
getVercelAiMiddleware
wrapLanguageModel, then use the wrapped model with generateText / streamText / generateObject / streamObject. Every call is captured as a keyed llm span carrying the call parameters as input. Streaming is captured without disturbing the live stream. See Vercel AI SDK framework guide.
The middleware’s structural types are exported for callers that wrap or inspect it. All three are open ([key: string]: unknown), so provider-specific fields pass through untouched:
BitfabVercelAiHandler is the class the middleware is built from, exported for direct construction with { traceFunctionKey, withSpan }.
wrapBAML
See the BAML framework guide for examples. Full signature under wrapBAML above.
wrapOpenRouter
See the OpenRouter framework guide for examples. Full signature under wrapOpenRouter above.
wrapAnthropic
See the Anthropic framework guide for examples. Full signature under wrapAnthropic above.
wrapOpenAIResponses
See the OpenAI Responses API framework guide for examples. Full signature under wrapOpenAIResponses above.
class BitfabFunction
Fluent wrapper binding a traceFunctionKey. Obtained via client.getFunction(key).
span / withSpan
client.withSpan(boundKey, optionsOrFn, maybeFn).
client.span(boundKey, options). The same standard-transform requirements as Bitfab#span apply. Within the opt-in surface, prefer withSpan for TypeScript 4.x projects and non-method callables. For new workflows on a supported build path, start with opt-out withTrace / trace and withNode / node.
trace / withTrace
client.withTrace(boundKey, ...) or client.trace(boundKey, options). The guides lead with this form for a trace root, because the key is bound once and the root, its spans, and its framework integrations all read it from here. The client-level methods stay fully supported, and both forms have the same subtree capture, replay, nesting, transform, and mixed-surface behavior.
getVercelAiMiddleware
client.getVercelAiMiddleware(boundKey), reusing the bound key so a withSpan root and the middleware share it. See Nesting with core tracing.
getClaudeAgentHandler
client.getClaudeAgentHandler(boundKey), reusing the bound key so a withSpan root and the handler share it. See Nesting with core tracing.
getLangGraphCallbackHandler
client.getLangGraphCallbackHandler(boundKey), reusing the bound key so a withSpan root and the handler share it. See Nesting with core tracing.
getLangChainCallbackHandler
Alias of getLangGraphCallbackHandler. LangChain and LangGraph share one callback system.
getLangGraphIntegration
client.getLangGraphIntegration(boundKey, options). Pass its wrapped tools to the graph, then wrap the compiled graph with instrument(graph) to get the callback-configured, replayable entry point. For a LangChain agent, build it with await integration.createAgent(params).
getDeepAgentsIntegration
client.getDeepAgentsIntegration(boundKey, options). Build the agent with await integration.createDeepAgent(params).
getRivetIntegration
client.getRivetIntegration(boundKey, options). Use the returned adapter with this handle’s withTrace() root. The adapter binds context and records inherited effect spans but never creates another root.
wrapBAML
Identical signature and semantics to Bitfab#wrapBAML. Unlike the getXHandler() methods above, it does not use the bound key. It opens no span of its own. It enriches the current span instead, so call it inside a function wrapped by this handle’s withSpan.
wrapJev
Identical signature and semantics to Bitfab#wrapJev. Like wrapBAML, it does not use the bound key and opens no span of its own.
wrapOpenRouter
Identical signature and semantics to Bitfab#wrapOpenRouter. Like wrapJev, it does not use the bound key and opens no span of its own.
wrapAnthropic
Identical signature and semantics to Bitfab#wrapAnthropic. Like wrapJev, it does not use the bound key and opens no span of its own.
wrapOpenAIResponses
Identical signature and semantics to Bitfab#wrapOpenAIResponses. Like wrapJev, it does not use the bound key and opens no span of its own.
class BitfabError
Extends Error.
status is the HTTP status code, present only on that non-2xx response failure. It is absent for network failures, timeouts, and an error returned inside an otherwise-successful response body. retryAfterMs carries the server’s Retry-After header in milliseconds, when it sent one. It is present only alongside status. Never thrown for transport errors on withSpan paths. Those are swallowed.
class MixedTracingError
Extends Error.
withSpan, span) and opt-out tracing (withTrace, trace, node, withNode) meet in one call stack. The message names which surface was entered inside which and how to resolve it. Unlike other tracing setup failures, it is not swallowed. The wrapped function does not run untraced, because a wiring mistake the check exists to report would otherwise be invisible.
class BitfabLangGraphCallbackHandler
Duck-types LangChain’s callback handler interface without importing @langchain/core. Obtained via client.getLangGraphCallbackHandler(key). No direct instantiation needed for normal use. It records chain, LLM, tool, and retriever roots as pending traces on start, then completes them on root end. Full callback surface documented in LangGraph framework guide.
class BitfabOpenAITracingProcessor
Implements the OpenAI Agents SDK TracingProcessor interface. Obtained via client.getOpenAiTracingProcessor(). No direct instantiation needed for normal use. See OpenAI Agents framework guide.
class BitfabOpenAIAgentHandler
Run wrapper for the OpenAI Agents SDK. Obtained via client.getOpenAiAgentHandler(key). Exposes wrapRun(agent, input, options?), a drop-in for run() that records a keyed, replayable root span. See OpenAI Agents framework guide.
class BitfabClaudeAgentHandler
Handler for the Claude Agent SDK. Obtained via client.getClaudeAgentHandler(key). Exposes instrumentOptions, wrapResponse, and wrapQuery. See Claude Agent SDK framework guide for method signatures and usage.
Functions
finalizers
SpanOptions.finalize helpers. aiSdk awaits the Vercel AI SDK’s text, usage, finish reason, tool calls, and tool results, without consuming its live stream. It records { text, usage, finishReason, toolCalls, toolResults }. The recorded usage prefers the result’s totalUsage over its usage field, when both resolve. Rejected or absent fields become undefined. readableStream tees a stream. It passes the caller’s live branch to onLive. It records the other branch as chunks. Because a ReadableStream is single-consumer, the caller must use the branch received by onLive.
getCurrentSpan()
traceId is "".
getCurrentTrace()
flushTraces(timeoutMs?: number)
timeoutMs: 5000. Use before process.exit() in short-lived scripts.
Returns true when queued exports completed successfully within the deadline. Returns false when delivery fails or the deadline expires. Records still waiting for the first sim plan read are sent without their inputs and outputs rather than kept back, except each trace’s root span, spans recorded by a framework integration, calls configured with node() or withNode(), and nested trace roots.
Bitfab.close(timeoutMs?: number)
30000). Idempotent. Returns false if delivery or shutdown misses the deadline. Use it when a long-running process creates transient clients. Shared clients may remain open until the process-wide exit hook runs instead.
Client environment variables
Transport environment variables
Commit ref environment variables
CommitRef
commit_ref on every root trace completion. sha is the commit. branch is the checked-out branch, or null when detached or unknown. dirty is true when the working tree had uncommitted or untracked changes, false when it was clean, and null 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 in a child process off the event loop, with a two second timeout per command, so it never sits on the code path 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. On the wire the field is root_sha. In a browser bundle neither environment variables nor git exist, so no commit_ref is sent.
Interfaces
BitfabConfig
See constructor table.
SpanOptions
NodeOptions is Omit<SpanOptions, "captureWhen"> & { capture?: boolean }. capture defaults to true. mockOnReplay may only select a node whose capture remains enabled.
CaptureWhen
SpanType
CurrentSpan
CurrentTrace
TraceSearchResult
client.traces.search. tokens is { input, output, cached, total }, each a number or null. nextCursor is null on the last page; pass it back as cursor to fetch the next one.
DetachedTrace
client.getTrace(traceId), where traceId is the canonical Bitfab trace ID. Methods have the same semantics as CurrentTrace, but send to the server immediately. They block until the server responds. They reject if the update is refused. When capture is off outside a replay item or a seedTrace call, all methods return Promise.resolve() instead.
CapturedSpan
getTraceSpan and getSpan. SpanLookup is { id: string } | { name: string; type?: SpanType; occurrence?: "first" | "last" | number } | { type: SpanType; name?: string; occurrence?: "first" | "last" | number }.
WrapBAMLOptions
WrapJevOptions<TReply>
WrapOpenRouterOptions<TResponse>
WrapAnthropicOptions<TResponse>
WrapOpenAIResponsesOptions<TResponse>
WrappedBamlFn<TArgs, TReturn>
collector is null before the first call or when @boundaryml/baml is unavailable. After each successful call, it holds the BAML Collector instance from that invocation.
ReplayOptions
onItemStart fires when a worker begins processing an item, before replay loads its inputs, prepares mocks or a database branch, or invokes customer code. Pair it with onItemFinish. onItemFinish fires exactly once per item as it finishes, in completion order rather than input order. Together they distinguish queued items from in-flight items whose callback has not returned. Both callbacks always carry the item. onItemFinish never represents whole-run completion. Replay doesn’t know pass/fail at finish time, because verdicts are assigned later. The totals only split ran-ok from errored as a result. A throwing lifecycle callback never crashes the run. The deprecated onProgress callback receives the same per-item finish events, plus its legacy item-less terminal complete event. It is ignored when onItemFinish is also provided. The SDK exports a ready-made reporter, reportReplayProgress, that accepts either lifecycle event. Pass it as both onItemStart and onItemFinish to write lifecycle events to stderr. The Bitfab plugin uses start events to identify in-flight traces in its liveness heartbeat. It uses finish events to write per-item result files.
Each finished item’s ingestionType identifies its source as captured or seeded. It matches the final item’s value and is undefined against older servers.
getCurrentReplayBranch()
maxConcurrency also bounds live branches. With attempts set, each attempt resolves its own branch too. It returns null outside a replay item, and for an item whose source trace carried no DB snapshot reference. That is the fallback path. Use it like this: const url = branch?.databaseUrl ?? process.env.DATABASE_URL.
The value object is immutable and per item, built from the replay context, so parallel items each see their own branch. Reading databaseUrl marks the trace as having used the branch, reported as accessed. The other fields inspect the branch without exposing the connection string. They deliberately do not mark the branch as accessed. That also means the URL does not appear in JSON.stringify(branch).
Every field the service puts on the lease is copied onto the branch, so one added server-side is readable before you upgrade the SDK. It just won’t be in the type yet. databaseUrl is the sole exception. It is the credential. It is also the only member that may mark the branch as accessed.
dbBranch tunes the branch itself: { minCu: 2, maxCu: 2, warmupSql: "SELECT 1;" }. Setting minCu equal to maxCu pins the compute, so items stay comparable. warmupSql runs inside the branch’s readiness check, so warm-up time is never charged to the replayed call. Invalid warm-up SQL fails the branch rather than quietly handing back a cold one. All fields are optional. dbBranch: true enables branching. It leaves the mirror’s own defaults in place.
ReplayResult<T>
original prefix.
A trace error means the replayed function started and threw. A replay error means Bitfab could not invoke it, for example because database warmup, input loading, or mock preparation failed. The original thrown value is preserved in the corresponding field. error remains its string message, for compatibility and JSON output.
If database branch resolution fails, replayError is a DbBranchReplayError with code, message, originalTraceId, and an optional cause. Resolver codes such as branch_create_failed, snapshot_from_replaced_origin, invalid_snapshot_ref, seeded_trace_has_no_snapshot (the source was seeded, so it pinned no database instant), and internal_error therefore remain available in memory, progress events, result files, and ReplayError.items. HTTP, timeout, and network failures while requesting a lease use lease_request_failed, with the original client exception as cause.
A replay whose delivery Bitfab cannot confirm in time is a console.warn 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: the run completes and the item is not marked errored. If no completed item’s trace persisted at all, or experiment finalization fails after items have settled, replay() throws ReplayError. It exposes items, experimentId, experimentUrl, and cause, plus the deprecated aliases testRunId and testRunUrl, so callers do not lose the individual failures:
serializeReplayResult
traceError and replayError, including DbBranchReplayError.code, originalTraceId, and nested cause. Use it for direct-run stdout instead of raw JSON.stringify. Raw JSON.stringify reduces JavaScript Error objects to {}.
AllowedEnvVars
OPENAI_API_KEY is currently allowed.
ActiveSpanContext
Mock override types
SpanNodeMeta is a span’s structural identity during replay. It carries no output payload, so matching runs on metadata only. spanName resolves as options.name ?? fn.name ?? traceFunctionKey. originalSpanId is this span’s id in the replayed trace, absent when the live span has no recorded counterpart, such as one the changed code newly introduced.
MockOverrideCtx.inputs is the live arguments the changed code passed on this run. getOriginalOutput() returns the span’s recorded output, memoized per replay item. It rejects when the span has no recorded counterpart. A synchronous span requires a synchronous resolver result, including a synchronous NO_MOCK_OVERRIDE.
null and undefined are legitimate mocked outputs, so returning either substitutes that value rather than declining. Decline with NO_MOCK_OVERRIDE.
Replay supporting types
AdaptContext.metadata is the original trace’s stored metadata, what seedTrace or getCurrentTrace().setMetadata put on it. It is requested from the server only when an adapter is registered. 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.
Replay registry types
ReplayRegistryOptions because the installed command owns progress reporting. ReplayRegistryContext.params holds the values supplied through --params and repeated --param name=value.
Seed option types
seedTrace overloads. SeedCaseOptions writes a trace without running anything. SeedRunOptions runs the function once and records it. spanName and spanType label and type the root span the case form writes. They have no run-form equivalent, because the executed function names its own root span.
Database snapshot types
DbSnapshotConfig optionally pins the provider at capture time. It is not required. The provider is otherwise resolved at replay time. See Database branching.
CaptureSurface
withSpan / span produce "opt-in", while trace / withTrace / node / withNode produce "opt-out". Entering one beneath the other throws MixedTracingError before the inner function runs. Framework-managed spans and replay/seed root wrappers are neutral and may contain either surface.
Error Behavior Summary
Module Resolution
- Node.js ESM:
dist/index.js - Node.js CJS:
dist/index.cjs - Browser: works, but
AsyncLocalStorage-dependent features degrade (seewithSpansemantics)