Skip to main content
The module is github.com/Project-White-Rabbit/bitfab-go. It requires Go 1.25 or later. Runtime dependencies are github.com/google/uuid, go.opentelemetry.io/otel, go.opentelemetry.io/otel/trace, and go.opentelemetry.io/otel/sdk.

Framework Integrations

No framework-native adapters are shipped for Go yet. Instrument Go code manually with client.Span or client.Start. See the Go SDK guide for the walkthrough. The Frameworks overview page tracks framework coverage across every SDK.

Package Constants

Version is the value attached to SDK requests. The two byte limits are transport safeguards. The SDK enforces them automatically. They’re exported so a caller can validate capture sizes before tracing.

Constructor

NewClient

Option

WithServiceURL

WithEnabled

When enabled is false, Span still executes the callback. Start returns a no-op *ActiveSpan. No data is sent to the API. Without this option, NewClient reads BITFAB_CAPTURE_ENABLED: on for 1, true, or yes, off for 0, false, or no (case-insensitive, trimmed), and sending stays on when it is unset. WithEnabled wins over the variable.

WithAPIKey

It’s equivalent to the apiKey argument of NewClient. Use it to construct a client purely from options. Whichever one is set last wins.

WithStrict

Makes an unresolvable API key a fatal misconfiguration. NewClient panics instead of disabling tracing. It’s off by default. That way, a missing telemetry key never crashes the host app. Turn it on in standalone programs where an untraced run is a failure you want surfaced immediately.

Client environment variables

Transport environment variables

Commit ref environment variables

CommitRef

The commit the traced code was running at, sent as commit_ref on every root trace completion. sha is the commit. branch is the checked-out branch, or nil when detached or unknown. dirty is true when the working tree had uncommitted or untracked changes, false when it was clean, and nil 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 goroutine, 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. Nil pointer fields serialize as JSON null.

type Client

Opaque struct. Construct via NewClient.

Span lookup types

(c *Client) Span

Executes fn inside a traced span. fn’s return value is captured as the span output. WithFinalize records a transformed output asynchronously while returning the original value. Returns: fn’s (any, error) return values. Errors: fn’s own error is captured and returned to the caller. A panic is recorded before the identical panic value is rethrown. Entering an automatic subtree with Span returns MixedTracingError before running fn. An unknown WithType value degrades to custom with a one-time warning. An internal instrumentation failure runs fn untraced instead of crashing the host.

(c *Client) Start

Start/End-style span for instrumenting existing functions. Always call defer span.End(). Returns a child context that carries the span. It also returns an *ActiveSpan for recording data. When enabled is false, it returns the original context instead. The returned *ActiveSpan is then a zero value whose methods are all no-ops. Spans exist only where you create them. Nesting between spans is automatic, but only among spans that already exist. Wrapping just the outermost function, for example, records a trace with a single node. See Instrumentation for more.

(c *Client) FlushTraces

Drains this client’s pending span deliveries, up to timeout. Returns false when an export fails, the deadline expires, or records remain held while the first simulation plan read is still running. Ready records are still flushed when other records remain held. Close never drops held records: it sends them with the plan applied when the plan has loaded, and without inputs and outputs when it has not. Use it for a mid-run flush, such as before reading a span back with GetTraceSpan.

(c *Client) Close

Flushes pending spans. Then permanently shuts down this client’s OpenTelemetry worker. It’s idempotent. It returns false when an export failed or the deadline expired. A closed client no longer records spans. Go has no atexit. Always defer client.Close(...) in main().

(c *Client) GetFunction

(c *Client) GetTrace

Returns a handle to an existing trace by its canonical Bitfab UUID. No active span is required. The ID is validated locally. Each update waits for the server response and returns errors for missing or inaccessible traces. The request honors context cancellation. AddContext appends an entry. SetMetadata merges keys into existing metadata. SetSessionID and SetName replace their respective values. Nil maps and empty session IDs or names are ignored. A disabled client sends no updates.

(c *Client) GetTraceSpan

Fetches one persisted span without loading its trace. traceID is the canonical Bitfab trace ID. Set the span’s Bitfab SpanLookup.ID alone, or SpanLookup.Name, SpanLookup.Type, or both. Anything else returns the error bitfab: provide id, or name and/or type, and a Type outside the six span types is refused, both before any request is sent. A name or type lookup picks among the matching spans in the order they ran and defaults to the last match. Use FirstSpanOccurrence or SpanOccurrenceAt(index) to override it, for example SpanLookup{Type: "llm", Occurrence: FirstSpanOccurrence} for the first model call in the trace. A miss returns nil, nil.

(c *Client) GetSpan

Reads one span from its span ID alone, either Bitfab’s span ID or the one the SDK sent when recording, so no trace ID is needed. Returns nil, nil when the span is not in the API key’s organization.

Datasets

The request and response types are:
client.Datasets holds a *DatasetsClient for the authenticated organization. It exposes the same operations the Bitfab MCP tools expose to a coding agent.
  • Save is an upsert keyed on (TraceFunctionKey, Name). Created is true for a new dataset and false when an existing one was updated. An empty Description leaves an existing description untouched. Set ClearDescription: true to write an empty description explicitly. Combining it with a nonempty Description returns an error before sending a request.
  • Update renames a dataset or changes its description by id, keeping its id, traces, and graders. An empty Name or Description leaves that field untouched. ClearDescription: true writes an empty description, and combining it with a nonempty Description returns an error. Leaving every field at its zero value returns an error before sending a request. A blank name, or a name another dataset on the same trace function already uses, fails with a 400 status error.
  • Golden on SaveDatasetParams or UpdateDatasetParams marks the dataset as golden for the whole organization when it points to true, a trusted reference set listed first in the app and in List, and unmarks it when it points to false. nil leaves it as it is. Dataset.Golden reads false from an older server.
  • List scopes to params.TraceFunctionKey when set. The zero value lists every dataset in the organization.
  • Dataset carries ID, TraceFunctionKey, Name, Description (*string), TraceCount, Graders ([]DatasetGraderRef with ID and Name), CreatedAt, and UpdatedAt.
  • ListTraces returns DatasetTraceIDs{DatasetID, TraceIDs}. This is the same membership a replay selects with ReplayOptions.DatasetID.
  • AddTraces and AddGraders each accept 1 to 100 ids. They report partial acceptance instead of failing outright. An id outside the organization, or under another trace function, comes back in SkippedTraceIDs or SkippedGraderIDs. An id already present comes back in AlreadyPresentTraceIDs or AlreadyAssignedGraderIDs.
  • RemoveTraces never deletes a trace. It only removes its membership in the dataset. Ids that were not members come back in NotPresentTraceIDs. RemoveGraders reports NotAssignedGraderIDs the same way.
  • RerunGraders re-scores every trace in the dataset. RerunGradersOptions.GraderIDs defaults to every assigned grader. Passing an unassigned id fails the call. RerunGraders blocks until the run finishes and returns the completed Run. A request that matches an in-flight run joins it, reported as JoinedExisting. An errored run returns a *GraderRerunError carrying the Run and its Error. When Timeout (default 90 seconds) elapses first, it returns a *GraderRerunTimeoutError. The timeout only stops the wait. The run keeps going on Bitfab, and GetGraderRerun with Run.ID reads it later. Match either error with errors.As. To do other work while the run goes, call RerunGraders in a goroutine. Cancelling ctx while waiting returns ctx.Err().
  • GraderRerun has Status (a GraderRerunStatus, one of GraderRerunPending, GraderRerunRunning, GraderRerunCompleted, GraderRerunErrored, with Terminal()), GraderIDs, Progress (*GraderRerunProgress while running), Result (*GraderRerunResult when completed), and Error.
  • A dataset id from another organization fails with a 404 status error.

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. It returns a page of stable trace summaries matching every filter supplied. 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. An empty field is omitted from the request rather than sent as a filter. The server defaults Limit to 50 and caps it at 500; use the returned NextCursor for pagination. The request and response types are:
Reach it as client.Traces. Tokens is the same TokenUsage struct replay items use, with *int64 fields Input, Output, Cached, and Total. NextCursor is nil on the last page; pass it back as Cursor to fetch the next one.

Spans

List returns the spans of one trace in the order they ran. traceID is the canonical Bitfab trace ID, and one that is not a valid ID returns bitfab: invalid trace ID before any request. No filter is required: with no SpanType or SpanName it returns every span in the trace, and when both are set a span must match both. Empty fields are left out of the request. Fields picks which span fields each span carries, and none by default; a field you did not ask for, or one with no content, is nil. Limit is at most 50 when Fields is set. Pass NextCursor back as Cursor for the next page until HasMore is false. A trace outside the organization is a 404 error. Reach it as client.Spans. For example, the input and output of every model call in one trace:

Replay

(c *Client) Replay

Replay fetches historical traces for traceFunctionKey. It re-runs their recorded inputs through fn. It waits for every emitted trace to persist. Then it completes the resulting experiment. fn must be a non-nil function. It may take context.Context as its first parameter, followed by any number of JSON-decodable typed parameters. It may return zero or more values, plus an optional final error. A single non-error return becomes ReplayItem.Result. Multiple returns become []any instead. Replay catches a panic from fn. It stores the panic as that item’s TraceError. Every invocation is automatically wrapped in a root span under traceFunctionKey. A production function that uses Start and End receives the replay root’s context. As a result, its existing spans become children of that root.

type ReplayOptions

Name says what the run is testing, in a few words, such as baseline or shorter system prompt. Bitfab already records the commit, branch, tree state, datasets, and who ran it with every experiment, so leave those out of the name. Notes holds run conditions Bitfab cannot see on its own, such as an environment override or a forced feature flag, and is kept on the experiment next to its name. Both are omitted from the start request when empty. ExperimentGroupID files the run under an experiment group, creating the group if it does not exist yet. ExperimentGroupName names that group when this replay creates it, or when the group exists but has no name yet; a group that already has a name keeps it. ExperimentGroupName requires ExperimentGroupID, and setting it alone is refused before any request is sent. Both are omitted from the start request when empty. Limit defaults to 5. It accepts values from 1 through 5000. MaxConcurrency defaults to 10. TraceIDs accepts at most 100 IDs. When it’s present, it determines the item count on its own. Limit is then omitted from the start request. ExperimentalSelectiveReplay accepts a MustRun list of ReplayNodeIdentity{TraceFunctionKey, SpanName} values. It requires MockMarked and rejects overrides. See selective replay for reuse declarations, output type requirements, limits, and coverage reports. When CodeChangeFiles is nil, replay first looks for BITFAB_CODE_CHANGE_PATH. If that’s not set, it captures the rename-aware Git diff of uncommitted working-tree changes against HEAD instead. 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, else origin/main, origin/master, main, or master), so changes you already committed still show. An explicit slice always wins over the automatic capture. Set DisableCodeChangeCapture, or the BITFAB_DISABLE_CODE_CHANGE_CAPTURE environment variable, to opt out entirely. Set BITFAB_CODE_CHANGE_BASE to a ref (for example origin/main) to diff against its merge-base instead, which includes committed branch changes. Mock defaults to MockMarked. MockNone and MockAll select the other two strategies. MockOverrides take precedence over overrides registered on the client. Both take precedence over the base strategy. Mock interception only applies to closure-style child Client.Span calls, because those calls own execution. Manual Start/End instrumentation cannot prevent caller-owned code from running. DatasetID and DatasetIDs are the same selector at two counts, so set one of them: one dataset on DatasetID, several on DatasetIDs. Several replays the union of their traces, deduped where they overlap, graded by the union of their graders, and attributes the experiment to every one of them, so it appears under each dataset’s experiments. A non-nil DBBranch enables a historical database branch. An empty DBBranchOptions uses the connected project’s defaults. Set MinCU, MaxCU, or WarmupSQL to tune provisioning instead. Resolution happens inside the bounded item workers. Attempts accepts 1 through 100 and defaults to one when zero. Work is queued attempt-major. Each attempt gets its own replay trace and historical database branch. ReplayItem.Attempt and lifecycle items carry the zero-based attempt index. ReplayResult.Attempts reports the requested count. By default every approved assertion is judged on each replay as it finishes, and the verdict is saved as that assertion’s agent verdict with the evidence it cited. An assertion that already carries a verdict is left alone. Set SkipAssertionJudging 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. OnlyWithAssertions limits selection to traces carrying an approved assertion. An assertion still awaiting review does not qualify a trace, because only an approved one is checked on a replay. AdaptInputs receives the original trace’s stored metadata in AdaptContext.Metadata. The SDK requests metadata only when an adapter is configured. ReplayItem.IngestionType reports captured or seeded, and is nil against older servers. DryRun resolves and adapts recorded inputs without invoking the function or provisioning database branches. It creates no experiment, so the result’s ExperimentID and ExperimentURL are empty, and it does not require any persisted replay traces. Items retain their inputs and original outputs, with no replay duration or trace ID.

Replay mock types

Resolve takes precedence over Value. A nil Value is a valid flat override. The first matching override wins. GetOriginalOutput fetches the matched historical output lazily. It memoizes that output per replay item.
Per-call overrides are evaluated before client-registered overrides. A mocked span upload carries mocked: true, mockTarget: "output", and a mockSource of recorded or override.

Bound replay functions

Go function values cannot carry decorator metadata. These APIs explicitly bind a callable to its declared key. That lets Client.Replay reject a mismatch before starting the experiment. Plain callables remain valid for handler-instrumented workflows with no declared Go root.

Historical database branch

Inside a replay item whose source trace has a resolvable snapshot, GetCurrentReplayBranch returns that item’s *ReplayBranch. Outside replay, or when the source trace has no resolvable snapshot, it returns nil instead. DatabaseURL() is the only accessor that exposes the connection string. Calling it also marks the trace’s db_snapshot_usage.accessed flag. JSON encoding and String() both omit the URL. The branch also exposes its Neon branch ID, environment key, expiration, snapshot timestamp, console URL, read-only flag, region, and source trace ID as direct fields. Any other field the server adds later comes through Extra. Resolution failures produce a *DBBranchReplayError on the item’s ReplayError. It preserves Code, OriginalTraceID, and Cause. ReplayItem.DBSnapshotRef and ReplayItem.DBBranchTimings expose the historical pin and the server-measured provisioning phases.

Replay input adaptation

The adapter runs before typed parameter decoding. Its returned slice is passed positionally to fn. That same slice is stored as ReplayItem.Input. An adapter error or panic becomes the item’s ReplayError. It does not stop other items from running.

Replay results and errors

ReplayItem carries everything about one replayed trace, including:
  • Input (the adapted recorded input), Result, and OriginalOutput
  • A compatible Error message, plus the actual TraceError and ReplayError
  • The replay’s own DurationMS, plus the original trace’s duration, token, and model measurements
  • The replay’s own token usage
  • The original trace and span lineage
  • The new server TraceID
Per-item function and setup errors stay on their items. 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., and leaves the item without an error, so --fail-on-error does not trip on it. A run-wide completion failure, or an HTTP failure from the status check itself, returns *ReplayError. Use errors.As and errors.Unwrap to inspect it without losing the partial items it collected or the experiment it created. TestRunID and TestRunURL carry the same values as ExperimentID and ExperimentURL. They are deprecated and will be removed in a later minor release.

Replay progress and serialization

OnExperimentStart fires once, right after the server creates the experiment and before any item runs, with its ExperimentID and dashboard ExperimentURL. It does not fire on a dry run, which creates no experiment. Use it to hold the experiment ID even if the run later fails. OnItemStart fires when a worker begins an item. This happens before replay loads its inputs, prepares mocks, or resolves a database branch. Its Item field carries only the original trace and span lineage at that point, because the rest of ReplayItem doesn’t exist yet. OnItemFinish fires exactly once per item, as it finishes. Items finish in completion order, not input order. OnItemFinish carries the full ReplayItem. Both callbacks report running totals, Completed, Succeeded, Errored, and Total. Replay doesn’t know pass or fail at finish time, since verdicts are assigned later. The totals only split runs that completed without error from runs that errored. A panicking callback is recovered. It never crashes the run. ReportReplayProgress writes one @@bitfab:progress {json} line to stderr. Pass it from both callbacks:
OnItemFinish flushes the finished trace. When that trace’s delivery is acknowledged, OnItemFinish fills the event’s server TraceID directly from the successful ingestion response. If per-item delivery can’t be confirmed, TraceID stays nil in the event instead. The run-wide persistence barrier then falls back to the status endpoint before filling in the final result. Replay aggregates token usage only at completion. Because of that, token usage appears only in the final ReplayResult, not in progress events. A panic inside a callback is swallowed. If BITFAB_REPLAY_RESULT_PATH is set, a successful Replay writes the structured result JSON there with one trailing newline.

Payload serialization helpers

These helpers expose the SDK’s JSON round trip for applications that need to preflight a captured value. Normal tracing and replay do not require calling them. Standard interface methods such as Error, Unwrap, MarshalJSON, String, and Format are documented with their owning replay types, rather than as independent entry points. TraceState is exported. This lets current-span handles share state across package boundaries. It’s transport plumbing, though, not an application-facing API.

type SpanFunc

type SpanOption

WithName

Defaults to traceFunctionKey for Span, or the spanName arg for Start.

WithType

One of "llm", "agent", "function", "guardrail", "handoff", "custom". An unknown value warns once. It then degrades to "custom" without changing the user’s result. Defaults to "custom".

WithMockOnReplay

Marks a closure-style child Span for recorded-output substitution under MockMarked. It has no effect outside replay. A selected occurrence that is absent from the historical tree returns an error without executing the real callback.

WithMockOutputType

Decodes a recorded or overridden JSON output into T before a mocked closure-style Span returns it. Use it for structs, slices, and other concrete outputs that would otherwise arrive as JSON-shaped map[string]any or []any values. Primitive and deliberately dynamic outputs do not need it.

WithFunctionName

Recorded as span_data.function_name.

WithInput

One arg stored directly. Multiple args stored as a slice. Only relevant to Span. For Start, use ActiveSpan.SetInput.

WithFinalize

Transforms the recorded output without changing the caller’s result. It works with Client.Span, Function.Span, and Start/End. Finalizers run in the background once per successful real invocation. Errors, panics, and mocked outputs skip finalization.
An ordinary finalizer error or panic omits the output and records finalize failed: …. Return &bitfab.StreamFinalizationError{Output: partial, Err: err} to preserve partial output with the original error message. Finalizer failures never become the function’s returned error. FlushTraces and Close wait for pending finalizers within their total timeout. A root completes after its children’s finalizers settle. Trace metadata and the dropped state remain available until then. Close permanently closes delivery even when the timeout expires. Repeated calls preserve its original success or failure result.

WithExperimentID

Attributes the span and the root trace it starts to an experiment. WithTestRunID is a deprecated alias. Passing both with different values makes Span return an error and Start panic. It works with Span and Start, including their fluent Function forms. Replay attribution takes precedence over this option so items remain in their owning experiment. Outside replay, the explicit value is used.

WithCaptureWhen

CaptureWhenNested records the span only when ctx contains an active Bitfab parent span. Without a parent, Span runs the callback untraced. Start returns the original context with a no-op ActiveSpan instead. The default is CaptureWhenAlways. An unknown value warns once. It then falls back to that default.

type Function

Obtained via (*Client).GetFunction(key). Fluent wrapper that binds traceFunctionKey.

(f *Function) Span

(f *Function) Start

type ActiveSpan

Returned by Start. All methods are safe on nil, safe under recover(), and idempotent where noted.

(s *ActiveSpan) SetInput

One arg stored directly. Multiple args stored as a slice. No-op on nil receiver.

(s *ActiveSpan) SetOutput

(s *ActiveSpan) SetError

(s *ActiveSpan) AddContext

Appends the map as one entry on span_data.contexts. No-op when context == nil.

(s *ActiveSpan) SetPrompt

Overwrites span_data.prompt. No-op on empty string.

(s *ActiveSpan) End

Idempotent via sync.Once. Sends the span in a background goroutine. Any panic inside the send path is recovered. That keeps the host app from crashing.

Trace-Level API

type ContextEntry

type CurrentSpan

Returns the active span’s canonical Bitfab span and trace IDs. GetCurrentSpan returns nil outside a span. Both methods are safe on a nil receiver. They return an empty string in that case.

type CurrentTrace

Returned by GetCurrentTrace.

(ct *CurrentTrace) TraceID

Returns the canonical Bitfab trace ID. It’s safe on a nil receiver. It returns an empty string in that case.

(ct *CurrentTrace) SetSessionID

(ct *CurrentTrace) SetName

Sets the trace’s title in Bitfab. The title is a searchable and filterable field, stored in the trace’s name column. Unset, the trace is titled by its trace function key instead. Empty strings are ignored. It’s safe on a nil receiver.

(ct *CurrentTrace) SetMetadata

Shallow-merges searchable exact-match metadata. Later keys win. 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.

(ct *CurrentTrace) AddContext

Appends. Accumulates across calls.

(ct *CurrentTrace) Drop

Flags the trace to be dropped. Once the flag is set, no spans that complete afterward are uploaded at all. The flag itself rides along on the trace’s completion payload. At completion, the server does the rest of the cleanup:
  • It scrubs any payloads that already raced out ahead of the flag, meaning the trace, its external trace, and any sibling spans
  • It deletes the archived S3 objects
  • It marks the trace dropped instead of completed, keeping only a skeleton audit row
Drop is safe on a nil receiver. That makes it a no-op, so calling GetCurrentTrace(ctx).Drop() outside a span does nothing. Drop never panics.

GetCurrentTrace

Returns nil when ctx carries no active span. Callers must nil-check.

Assertions

client.Traces reads and writes assertions about what should happen when a trace is replayed. Reads inherit from the nearest replay ancestor with assertions. Writes target the original trace. client.Categories manages the organization-scoped categories that group assertions and scenarios:
Save creates a category or updates one by ID. A nil Description preserves the existing value; a pointer to an empty string clears it. Delete detaches the category from its assertions while preserving those assertions and their verdicts. Categories are organizational metadata and do not carry approval fields.
client.OrganizationMembers.List returns the people in the organization ordered by name. It takes no arguments, because an API key belongs to one organization. Read it to find the address to pass as an assertion’s AssigneeEmail, which is refused when it belongs to nobody here. Membership reaches Bitfab through the identity provider’s mirror, so an empty slice means nothing has been mirrored rather than that the organization is empty.
Source defaults to TraceAssertionAgent ("agent"). Use TraceAssertionHuman ("human") for a human-authored assertion. Saving an existing ID updates it in place. Omit the ID to create an assertion. 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. ClearAssigneeEmail leaves it to nobody, and setting both at once is an error. Reads expose a nullable Assignee. Assigning routes an assertion rather than deciding it, so it never returns the assertion to ApprovalPending the way an edit to the wording, criteria, target, or category does. Nil criteria, targets, category IDs, assignee emails, and justification pointers preserve existing values on update. Set a corresponding Clear... flag for criteria, targets, categories, or assignees to send an explicit null. Point a justification field to a nil slice to clear it. Each evidence entry is a JustificationSpan{SpanID, Text}. Assertion reads expose one ApprovalFields decision covering applicability and the current category assignment; the SDK cannot write that approval state. Each entry that carries assertion text is checked against the writing rules after it is saved, and text that breaks one is rewritten in place as one well-formed assertion. The save returns once the text is saved, and each checked TraceAssertion comes back with Rewrite.Status AssertionRewritePending. Bitfab runs the check in the background, usually within a minute. GetAssertions rows carry the latest Rewrite, or nil when no check ever ran: its Status (AssertionRewritePending, AssertionRewriteRewritten, AssertionRewriteUnchanged, AssertionRewriteNotChecked, or AssertionRewriteCheckFailed, which also covers a check unfinished 15 minutes after the save), the text you sent, what changed, and in SeparateAssertions any other claim the sent text bundled. Those were not saved, so save each as its own assertion. Set SkipGuidedRewrite to save text exactly as sent, with no check. SaveAssertions delegates to SaveAssertionsAll. The entire batch travels in one request and is saved atomically by the server. Limits are 500 trace updates, 50 assertions per trace update, and 1,000 assertions in total. An empty update list sends no request. ArchiveAssertions accepts up to 50 assertion IDs and returns the IDs archived. It hides those assertions from subsequent reads without deleting them.

Assertion targets

An absent target checks the whole trace. TraceTarget{Kind: TraceTargetOutput} checks its output. A span target requires a name on the trace under evaluation. It does not accept a span ID from the original trace. Use FirstSpanOccurrence, LastSpanOccurrence, or SpanOccurrenceAt(0) for a zero-based occurrence. An empty occurrence leaves the server’s default in effect. Numeric occurrences are encoded as JSON numbers. Both numeric and named occurrences are decoded when assertions are read back.

Scenarios

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, never what the run did or should do. A scenario can also hold an actual outcome: what one run actually did, one example and never what it should have done. client.Traces reads and updates the scenario built from a trace.
Name, Description, Trigger, and Data each replace the saved value whole, and a nil field keeps what is saved. AppendData 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. Set Data or AppendData, not both. Setting both, setting no field besides TraceID, or passing an AppendData with no history entries and no missing items returns an error before any request. Bitfab never fills in ActualOutcome. The caller saves it. Set ActualOutcome to replace the saved outcome whole, set ClearActualOutcome to remove it (the request sends "actualOutcome": null), or leave both unset to keep what is saved. Setting both returns an error before any request. A save that only sets ActualOutcome or ClearActualOutcome is valid. A nil Actions is sent as an empty list and a nil Details as an empty object. Each action needs a Name. Result and At are nil when unknown. CategoryIDs files the scenario under categories and replaces its whole set of categories. Leave it nil to keep the saved set, or set ClearCategoryIDs to file the scenario under none (the request sends "categoryIds": []). Setting both returns an error before any request, and a save that only sets one of them is valid. An id outside the organization makes the save fail with a 400 that names the missing ids. Reads return the scenario’s Categories sorted by title. Manage the categories themselves with client.Categories.
A history entry’s Source, and an actual outcome’s Source, names the span its text came from, on this trace or another trace linked to the same scenario. Leave it nil 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 SaveScenario returns the saved scenario. It only updates a scenario that already exists: a trace with no scenario fails with a 404 status error, from either method.

Verdicts

client.Verdicts writes agent-authored or human-authored verdicts and reads them back with per-assertion detail. client.Labels, the Label* type names, the Label and Abstain fields, WithLabelExperimentID, and GenerateLabelEvidence are deprecated aliases kept for one minor release; they send the same requests as the names below. The deprecated LabelAction* constants equal the new action values, so an outcome compares equal to either spelling. LabelActionNoActiveLabel is "no-active-verdict", the same value as VerdictActionNoActiveVerdict, because that is what the server now reports for an archive with nothing to clear. The deprecated LabelTarget field on VerdictUpdate is merged with the embedded VerdictTarget field by field, so code that sets LabelTarget and then assigns a promoted field such as update.AssertionID still sends both. The update is refused only when the two set the same field to different values. Reading a promoted field such as update.TraceID returns only what the embedded VerdictTarget holds.
Set exactly one of TraceID or OriginalTraceID. A replay verdict uses OriginalTraceID together with WithVerdictExperimentID(experimentID). Attempt is an optional pointer to a non-negative integer. An explicit zero is sent as zero. AssertionID names the approved assertion the verdict scores. Save, SaveAll, and Skip return an error without sending anything when it is empty, since verdicts are only written on assertions, never on a whole trace. Archive may leave it empty to clear an old whole-trace agent verdict. SaveHuman and SaveHumanAll return the same kind of error when a HumanVerdictUpdate has an empty AssertionID. With neither Skip nor Archive set, Verdict is sent with Annotation as its reason. VerdictPass and VerdictFail need an Annotation. An empty Verdict is sent as a fail, so always set it. Skip withholds a verdict when the check did not run, and Annotation beside it records why. That reason is read back by Get and GetAll, and it is what separates a verdict nobody could reach from a trace nobody looked at. A skip sent without it clears the note the previous verdict carried. Skip on VerdictsClient addresses a target only, so a skip that carries a reason goes through Save with Skip: true. Archive clears its previous verdict. Both preserve the target, including a replay attempt or assertion. Setting Skip or Archive together with each other or with a Verdict is rejected. VerdictAbstained records an abstention, and Abstain(ctx, target, options...) addresses a target the same way Skip does, so an abstention that carries a reason goes through Save with Verdict: bitfab.VerdictAbstained and Annotation set. 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. Setting Confidence or Evidence on an abstention, including through the deprecated Abstain field, returns an error before any request. It reads back as a Verdict of VerdictAbstained, and the trace’s Abstained count says how many of its failed verdicts are abstentions. A person never abstains, so SaveHuman and SaveHumanAll refuse VerdictAbstained. VerdictConfidence accepts VerdictConfidenceVeryLow, VerdictConfidenceLow, VerdictConfidenceMedium, VerdictConfidenceHigh, and VerdictConfidenceVeryHigh. An empty confidence is omitted. VerdictAction is VerdictActionSet, VerdictActionArchived, VerdictActionNoActiveVerdict, VerdictActionSkipped, or VerdictActionAbstained. Verdict is VerdictPass, VerdictFail, or VerdictAbstained. Evidence points to one {SpanID, Text} entry per cited span on the evaluated trace. A nil pointer preserves saved evidence. Point it to a nil slice to clear saved evidence. Batch writes accept up to 200 updates. Human writes use a separate endpoint, are recorded as written by a person, and target a trace ID directly plus the assertion they score. Verdicts have no approval step, so agent and human verdicts both count as soon as they are written.

Reading verdicts

VerdictStatus is VerdictStatusJudged, VerdictStatusSkipped, or VerdictStatusNotJudged. VerdictSource is VerdictSourceHuman or VerdictSourceAgent. Nullable fields use pointers so an absent verdict remains distinct from a failing one. Approved reports human validation. Get returns nil when no trace is returned. GetAll reads up to 100 trace IDs in one request. An empty ID list returns an empty slice without a request. Get and GetAll return evidence with each whole-trace and per-assertion verdict. GenerateEvidence generates suggested evidence for an assertion verdict. 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, SpanName, signature-only Parameters (names and runtime types, never values), SpanType, and IsMocked. Save only the candidate’s span ID and text as verdict evidence; saved evidence remains on Verdicts.Get and Verdicts.GetAll.

Grader verdicts

client.Graders.GetVerdicts reads individual automated grader verdicts, including each grader’s reason and failure diagnostic. GetLabels, SaveLabel, SaveLabelAll, and the GraderLabel* type names are deprecated aliases kept for one minor release.
Set TraceIDs, GraderID, or both. Omitting both returns an error before any request. Limit: 0 leaves the server default in effect. Source is GraderVerdictSourceHuman ("human") or GraderVerdictSourceLive ("live_grader").
Posts verdicts for external graders: graders created with 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 never abstains, so VerdictAbstained is refused before any request. 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. Empty Reason, FailureDiagnostic, and Confidence are left out of the request.

Experiments

client.Experiments reads runs back after the fact, for a dashboard or a report outside the replay process.
List pages newest first; every filter is optional and they combine with AND, Metadata matches by containment, and the time bounds apply to CreatedAt. Experiment carries the provenance fields (GitBranch, CommitSHA, BaseSHA, ExperimentSHA, RunBy, CompletedAt, Metadata), with nullable fields as pointers. ExperimentTotals 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. ExperimentRollup carries the Traces, Verdicts, and Checks tallies, Categories (one ExperimentCategoryTally per category), and Jitter. The fields Labels, Categories[].Labels, ExperimentTotals.AwaitingLabels, and OriginalUnlabeled are deprecated, carry the same values as Verdicts, Categories[].Verdicts, AwaitingVerdicts, and OriginalWithoutVerdict, and stay for one minor release. Each ExperimentTally has Passing, Failing, Fixed, Regressed, StillPassing, StillFailing, OriginalWithoutVerdict, 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. GetRollupAll sends up to 100 ids in one request and returns an empty slice with no request for an empty list. An id outside the organization is a 404 error, and a batch with one unknown id fails whole. ReplayOptions.Metadata, or --metadata key=value (repeatable) on the replay command, tags a run with your own strings. Keys match ^[A-Za-z0-9_.:-]{1,40}$, values are at most 500 characters, at most 50 keys.

Concurrency Model

  • Nested span context is carried on context.Context. It’s safe across goroutines that inherit that context
  • Goroutines that do not inherit the context will not see the parent span
  • Spans are queued on a private OpenTelemetry batching worker. Each client gets its own worker, started lazily on the first span it sends. Queueing itself never blocks. FlushTraces and Close are what drain the worker
  • Trace state is stored in a mutex-protected package-level map, keyed by traceID

Error Behavior Summary

Capture policy and simulation plans

WithCaptureEnabled(false) turns off ordinary recording while preserving replay and seed recording. WithEnabled is a deprecated alias. CaptureEnabled() reports the effective ordinary recording state, including credential availability. Credentials resolve at first use, so a client created during package initialization can use environment variables loaded later. Pass WithAPIKeyFunc(func() string { ... }) for a deferred credential provider. A successful resolution is cached; an empty result can be retried on a later call. WithStrict(true) panics for an unresolved key at first use. WithTimeout(duration) sets the default HTTP request timeout. Simulation-plan reads run in the background. NewClient starts the first read when the plan is enabled, the API key is passed directly or set in BITFAB_API_KEY, and capture is on. With WithAPIKeyFunc, the first Trace root starts it, so the key provider is still called at first use. A root or GetFunction call starts a read only when a key resolves and the client is recording, meaning capture is on or ctx carries a replay or seed. Otherwise it sends no read and prints no warning. A Trace root that starts while that first read is running waits for it, for at most five seconds from when the read started, and stops waiting when ctx is cancelled. Once the first read has finished, whether it succeeded or failed, no root waits, and nested roots and descendant spans never wait. Capture settings match the owning trace function key and span name. While the first read is running, the SDK holds affected records. When it succeeds, held records are sent with the plan applied. Once the plan has loaded, a span with capture off is not sent, its children carry the nearest captured ancestor the SDK resolved while the trace ran so Bitfab nests them under the closest call above them whose capture is on, and it does not count toward MaxCapturedSubtreeSpans. The root span of each trace, framework-origin spans, and spans that recorded an error are still sent, losing inputs, outputs, and serialized representations while keeping timing, errors, context, and links, and the SDK never builds their inputs or outputs. Configured nodes (Client.Node) and nested Trace roots ignore the plan and always keep their inputs and outputs. A discovered call that shares a configured node’s name is still dropped. When the read fails (a network error, a timeout, a non-404 HTTP error, or an unreadable body), held records and every later record are sent without inputs and outputs, marked content_off_by_simulation_plan, until a later read succeeds, and the SDK logs one warning. The root span of each trace, framework-origin spans, configured nodes, and nested Trace roots keep their content. Spans sent without content only because the plan could not be read still count toward MaxCapturedSubtreeSpans. Reads retry every 10 seconds after a failure and refresh every 60 seconds after a success, and a failed refresh keeps the last plan that loaded. A 404 counts as a loaded plan with nothing turned off. The hold keeps at most 1,000 records and sends the oldest beyond that without inputs and outputs. FlushTraces and Close wait within their existing deadline for held records and active submission, keeping completion behind pending spans of its trace. WithSimulationPlan(false) or a nonblank BITFAB_DISABLE_SIM_PLAN disables the plan gate. Plan reads have a separate five-second timeout. Every span carries the Go SDK name/version and instrumentation source in span_origin.

Seed and reseed

SeedCase records supplied inputs and expected output without executing code. SeedTrace runs a callable once. Both work with WithCaptureEnabled(false), require an API key, set ingestion type to seeded, and omit database snapshot pins.
ReseedTrace loads recorded inputs and metadata, runs the function, and adopts the successful run under the existing trace ID. Verdicts, assertions, and dataset memberships stay on that trace; PreviousRunTraceID identifies the previous run. A failed execution is recorded and returned as an error without adopting it.

Project registry command

Experimental GitHub Actions replay uses RunCloudReplayCLI(ctx context.Context, args []string, stdout, stderr io.Writer) error. Route arguments containing --cloud or a --cloud- prefix to it before initializing the application’s registry; RunReplayCLI does this for you. It runs the Python helper embedded in the SDK, streams the replay’s output to stdout and stderr, and returns an error wrapping an *exec.ExitError with the replay’s exit code when it is not zero. On the runner it starts the same executable again with the arguments you passed; --cloud-init finds the main package that calls RunCloudReplayCLI and runs it with go run, or takes --run when there is more than one. See Cloud replay for its supported flags and recovery commands. A Go project compiles its registry into its own command. RunReplayCLI accepts a pipeline name followed by SDK-owned replay options; RunSeedCLI accepts the same pipeline with --cases PATH [--run] or --from-trace IDS.
Replay flags include --limit, --attempts, --trace-ids, --dataset-ids (--dataset-id), --grader-ids, --name, --notes, --experiment-group-id, --experiment-group-name, --concurrency (--max-concurrency), --mock, --only-with-assertions, --skip-assertion-judging, --db-branch, --no-db-branch, --code-change, --no-code-change, --dry-run, and --fail-on-error. With --fail-on-error, RunReplayCLI prints the result as usual and then returns an error reading [replay] N of M replayed items errored; exiting 1 because of --fail-on-error when any item errored, so a command that exits 1 on a returned error fails the run. Without it, errored items only appear in the summary. Under --dry-run, items whose inputs failed to resolve count. --name says what the run tests, while --notes holds run conditions Bitfab cannot see, such as an environment override or a forced feature flag. --experiment-group-name names the group from --experiment-group-id when this run creates it or it has no name yet, and requires --experiment-group-id. --params loads a JSON object; repeatable --param name=value entries override it and decode JSON values when possible. The options factory modifies a copy of the registration’s options before command flags apply. A limit bounds the selected source. Explicit trace IDs keep their order; dataset members are deduplicated and sorted before pinning up to 100 IDs. When an assertion filter narrows a bound, the command checks each candidate’s own approved assertions before filling it; one still awaiting review does not make a trace eligible. Reads stop when the bound fills and transport errors propagate. The server revalidates the final selection, so concurrent changes can still reduce it. Empty replay selections return an error. --seed PATH is a compatibility spelling for --cases PATH. Case files contain a JSON array or one JSON object per line, each with an input array and optional expected, metadata, sessionId, and name. Without --run, they record cases. With --run, each case executes once and must omit expected. Final JSON goes to stdout and human-readable diagnostics to stderr; replay progress uses @@bitfab:progress. Each 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 returning the error, so the experiment ID is never lost. RunReplayCLI sets OnExperimentStart itself to print that line, replacing any value in the registration’s options. AdaptInputs receives the recorded inputs before type decoding. A returned value that is assignable to its parameter’s type is passed through unchanged. Any other value is converted through JSON into the parameter’s type, which drops fields tagged json:"-", including function fields. Return live values as the parameter’s exact type. RunReplayCLI prints each item’s inputs as JSON in its result. An Options.AdaptInputs that passes live values, such as a struct holding a function, must tag the function fields json:"-". Go cannot encode a function value, so an untagged field makes RunReplayCLI return an encoding error after the replay finishes and the result is not printed. A registry’s Options.OnItemFinish also runs after each progress event, so project code can save verdicts while items finish. Panics in that callback are reported without aborting replay. Dry runs skip the project grading callback.

Isolated replay processes

Set ReplayOptions.Concurrency on a registry entry to run each work item in a fresh process of the project’s compiled command:
The parent owns the experiment and emits progress; each child executes and persists one assigned item, including its own database lease, then writes its result. Child grading runs after the result is saved. A grading panic is reported while preserving the item. A failed or timed-out child becomes an item error. Child logs go to files; failures include only a bounded tail. ChildTimeout defaults to 40 minutes. RunReplayCLI re-executes the current binary, so call it from a project command that builds the same registry on startup. ChildDeliveryTimeout is how long a child waits for its spans to reach Bitfab. It bounds two waits. The first runs before OnItemFinishInChildProcess: the child flushes the item’s spans to read back event.Item.TraceID, and if that flush does not confirm delivery, it asks Bitfab whether the trace is stored for up to the same duration again. The second runs after the hook: the child flushes whatever is still queued, including spans the hook itself recorded, before RunReplayCLI returns. Raise it when a large trace uploads slowly and items come back with a missing TraceID or missing spans. It defaults to 30 seconds under Primitive: "process", must not be negative, and is refused under "goroutine", since no child process exists there. A child reports span delivery failures, such as a rejected or failed upload or delivery not confirmed within ChildDeliveryTimeout, as [replay] span delivery problem for <trace>#<attempt>: ... on the command’s own stderr, even when the item succeeds. Repeated messages are counted instead of repeated.
The concurrency object owns attempts and maximum concurrency; do not also set their scalar ReplayOptions fields. CLI --attempts and --concurrency override the object’s values. The default primitive is "goroutine"; "process" defaults to four workers and enables memory admission. MemoryThrottle may point to false to disable it. Child hooks and memory admission require process mode. Dry runs execute in the parent and skip child and grading hooks. Memory admission reads available memory and the operating system’s memory pressure on Linux and macOS, reserves capacity for starting workers, and learns their observed peak RSS. Workers wait while pressure is critical, meaning the critical kern.memorystatus_vm_pressure_level on macOS or every task stalled on memory for 10% or more of the last ten seconds on Linux. Swap at 85% used delays workers only where pressure cannot be read. One worker can always start when none is active. Configure initial per-child memory with BITFAB_REPLAY_CHILD_MEMORY_MB and the free-memory floor with BITFAB_REPLAY_MEMORY_FLOOR_MB (both default to 2048 MiB). Set BITFAB_REPLAY_MEMORY_THROTTLE=off to disable admission. Platforms without memory probes retain the explicit worker bound.

Automatic subtree tracing

Use Client.Trace with bitfab-instrument to capture first-party calls from one root. Client.Node configures discovered functions. See the subtree reference for options, build commands, linked roots, replay mocking, finalizers, and concurrency.

Explicit database snapshot provider

WithDBSnapshot validates configuration before client construction and returns an option and an error. Neon is currently the supported provider:
Ordinary span and subtree roots then include provider: "neon" alongside their timestamp. Without configuration, the server resolves the provider at replay time. Configuration performs no database I/O, and seeded traces still omit snapshot pins. Replay summaries follow TypeScript’s case semantics: seeded rows show “Matched expected” or “Missed expected” against their stored output; captured rows show “Same” or “Changed.” A case seed stores expected, while a run-based seed stores its execution result. Python currently creates run-based seeds and uses “Same/Changed” for all rows. Errors are counted separately.

Local BAML execution

WithBAMLExecutor(executor) enables client.Call(ctx, methodName, inputs). Use bamladapter.Execute from the optional github.com/Project-White-Rabbit/bitfab-go/baml module. WithEnvVars(map[string]string) supplies credentials without changing process environment. Only OPENAI_API_KEY is forwarded. PrepareBAML exposes the canonical source preparation and input coercion used by custom executors. bamladapter.WrapBAML[Input, Output](factory, fn, onCollector) returns a typed wrapper. factory is the generated client’s NewCollector function. fn(ctx, input, collector) calls a generated function with its WithCollector(collector) option. Call(ctx, input) preserves the typed result and error. Collector() returns the most recently completed native collector. Successful invocations enrich the active span with rendered prompt and LLM metadata. An optional callback receives the same collector. Callback panics do not alter the result. bitfab.WrapJev[Req, Reply](call, opts...) wraps a func(ctx, req) (reply, error) that calls Jev (TypeSafe’s System One model). After a successful call it adds the reply’s model, provider (typesafe), inputTokens, outputTokens, and durationMs to GetCurrentSpan(ctx). It opens no span and needs no optional module. The reply may be a struct with json tags, a pointer to one, a map[string]any, json.RawMessage, or a []byte or string holding the JSON body. WithOnReply(fn) receives each reply after it is recorded; a panic inside it is recovered. An error is returned unchanged and records nothing. See the Jev framework guide. GetCurrentSpan(ctx).SetPrompt(string) and .AddContext(map[string]any) update the active span. Both safely do nothing outside a recorded span.