sail.voyage is a flight recorder for long-running agent, evaluation, and
background-task trajectories. Your harness owns the work loop; Sail records
timeline events, spans, agent metadata, and correlated
inference calls. For a guided introduction, see the
Voyages guide; this page is the API reference.
Two ways to call
Every operation is available two ways:- Module-level helpers (
sail.voyage.event(...),sail.voyage.span(...), …) act on the current Voyage of your execution context, set bycreate()/attach()(see the note below). - Methods on the
Voyageobject returned bycreate()/attach()act on that specific Voyage.
Voyage object through your code.
The current Voyage follows Python
contextvars with a process-wide fallback:
concurrent tasks that each start their own Voyage keep their own attribution,
and a context that never started one (a raw threading.Thread, code after
asyncio.run returns) uses the process’s most recently started Voyage. Span
and agent contexts are strictly context-scoped, so a raw thread does not
inherit the active span/agent but can still use the current Voyage for
inference correlation.Async forms
Networked operations expose an async form through.aio. Module-level
create and attach provide create.aio() and attach.aio(). The
complete, fail, cancel, and flush methods provide .aio forms both on
the module and on a Voyage object. run() supports async with:
sail.voyage.run
with for synchronous
code or async with when the work runs in an event loop.
create(); always creates, never reads
SAIL_VOYAGE_ID), emits voyage.completed on clean exit, and on an
exception emits voyage.failed with the exception’s type and message, then
re-raises. Terminal delivery is the same bounded best-effort flush as
complete()/fail(). A voyage.flush() inside the block confirms only
events emitted before that call, not the terminal event emitted on exit. Use
create() plus complete()/fail() and a following flush() when terminal
delivery must be raise-on-failure confirmed. Without SAIL_API_KEY the block
runs with telemetry disabled. If your start and finish happen in different
parts of your code, use create() with complete()/fail() instead.
sail.voyage.create
voyage.started. Always creates, even when SAIL_VOYAGE_ID is
set in the environment; a child process joining its parent’s Voyage uses
attach() instead.
Returns a
Voyage. When SAIL_API_KEY is absent it
returns a no-op Voyage instead (see below).
sail.voyage.attach
voyage_id defaults to
SAIL_VOYAGE_ID, the handoff a parent process sets so its children join the
parent’s Voyage (see
child-process attach). Raises
ValueError when neither is provided and telemetry is enabled; without
SAIL_API_KEY it returns a no-op Voyage instead, so a keyless child keeps
running with telemetry disabled. Attaching does not emit a second
voyage.started.
No-op when unauthenticated
IfSAIL_API_KEY is not set, create() and attach() return a NoopVoyage:
no Voyage is created, no network calls are made, and id() /
dashboard_url() return None. Every method is a safe no-op. This lets the
same script run locally without credentials. (Sailbox and inference APIs
still require SAIL_API_KEY.)
The Voyage object
create() and attach() return a Voyage with these attributes:
It exposes the same operations as the module-level helpers below
(
event, span, agent, error, complete, fail, flush, headers).
event
agent() context, or from the
SAIL_AGENT_* env defaults when no context is active. There is no
per-event override; a one-shot attributed event is
with voyage.agent(...): voyage.event(...). Events are buffered locally and
flushed by a background thread; event() validates input, enqueues quickly,
and does not raise network errors.
span
@sail.span() names the
span after the decorated function’s __qualname__; the with form requires
a name. The decorator resolves the current Voyage at call time, so
module-level decoration before create() attributes correctly. Decorating a
generator function raises TypeError (the context would close at generator
creation); async def is fully supported.
span.started on enter and
span.completed (or span.failed, with the exception type) on exit. Spans
nest: a span opened inside another becomes its child automatically. A span
carries no agent identity of its own. Wrap it in
agent() to attribute it and everything inside it to a named agent.
Span outcomes
The yielded span object accepts outcome data viamerge_payload(). The
terminal event carries the started payload shallow-merged with everything
merged during the span; outcome keys win on conflict, and repeated calls
accumulate. The span.started event is unchanged, and outcomes ride
span.failed too. Partial results recorded before a crash are kept.
Auto spans
When Sail inference or a Sailbox exec runs inside a current Voyage with no active span, the SDK synthesizes a timed span for that operation. The generated span is marked as auto in the dashboard and named from the calling code when possible. Explicit spans always win; auto-spans only fill gaps where you declared nothing.agent
name (the only required argument) is the
display identity shown in the dashboard; the stable attribution key is derived
from it automatically (lowercased, ASCII, hyphenated). role= is an optional
grouping label for filtering across workflows. slug= (advanced) pins the
attribution key explicitly. Use it when renaming a display name should keep
one identity, or when a child process must attach as the same agent.
Decorator form
agent() and span() also work as decorators, which is the most direct way
to instrument a whole function. sail.agent and sail.span are top-level
re-exports of the same objects.
with form.
error
voyage.error event without terminating the Voyage.
complete
voyage.completed and performs a bounded (10s) best-effort flush.
Always call complete() (or fail()) before your process exits. The first
terminal event wins; any events emitted after it are delivered best-effort. On
delivery failure it warns and returns (the event stays
buffered for background/atexit retry) instead of raising; call
flush() afterwards if you need raise-on-failure delivery
confirmation.
fail
voyage.failed and performs the same bounded best-effort flush as
complete(). It warns instead of raising on delivery failure. error_type
must be non-empty.
flush
complete()/fail() for product-critical terminal state.
headers
existing with the full attribution context set:
X-Sail-Voyage-Id for the current Voyage, plus X-Sail-Voyage-Span-Id and
X-Sail-Voyage-Agent-Id for the span/agent active at call time. Use this to
correlate a raw HTTP/OpenAI client with the Voyage when you can’t use the
inference wrappers. Compute it per request, never
once at client construction, so each call carries the context actually
active when it is made.
child_env
attach() to the
current Voyage: merge into the child’s environment instead of exporting
SAIL_VOYAGE_ID by hand. With agent=True (default) the active agent()
context rides along as the child’s SAIL_AGENT_* defaults. Returns {}
when telemetry is disabled, so the handoff is keyless-safe.
disable
create()/attach() enter when
SAIL_API_KEY is absent. For controllers that catch a startup telemetry
failure and choose to continue unobserved.
Module helpers
sail.voyage.id() and sail.voyage.dashboard_url() return the current
Voyage’s id and dashboard URL (or None when there is no current Voyage or
it is a no-op).
sail.voyage.cancel() delegates to the current Voyage and marks it cancelled
on the server. It does not emit a voyage.cancelled event. With no current
Voyage, or when the current Voyage is a no-op because telemetry is disabled, it
returns without network I/O.
Environment variables
Validation and delivery semantics
- Validation:
kind≤ 128 characters;levelone ofdebug/info/warn/error;payloadandmetadatamust be JSON objects;occurred_atmust be RFC3339 with a timezone;versionmust be a positive integer. Oversized human text does not raise: amessageor span name over 4 KiB is truncated, and apayloadover 64 KiB is replaced by a{"_truncated": true, "_original_bytes": N}stub, each with a warning.metadataover 64 KiB raises atcreate()(startup is when failing is cheapest).create()andattach()validate their arguments before the no-key gate, so a malformed call raises even when telemetry is disabled. - Buffering: events go to a bounded local buffer. When it is full, the
oldest non-terminal events are dropped first and a
sdk.events_droppednotice is emitted; lifecycle events (voyage.startedplus the terminalvoyage.completed/voyage.failed) are preserved. - Delivery semantics:
flush()blocks and raises delivery errors (sail.VoyageErrorand subclasses), the strict primitive.complete()andfail()perform a bounded best-effort flush and warn instead of raising.cancel()calls the dedicated cancel endpoint and raisessail.VoyageErrorsubclasses on HTTP failure.event()never raises network errors. - Cancel semantics:
cancel()marks the server-side Voyage cancelled without enqueueing or posting avoyage.cancelledevent. It stops recording future trace updates; it does not terminate external agent code. WithoutSAIL_API_KEY,NoopVoyage.cancel()is a safe no-op.