Skip to main content
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 by create()/attach() (see the note below).
  • Methods on the Voyage object returned by create()/attach() act on that specific Voyage.
They behave identically; the module-level helpers just save you from threading the 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

This is the recommended entry point. It wraps one block of work in a single Voyage and handles the terminal lifecycle for you. Use with for synchronous code or async with when the work runs in an event loop.
Creates the Voyage on enter (same arguments and semantics as 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

Creates a new Voyage, makes it the current Voyage for the execution context, and updates the process-wide fallback used by contexts that have not selected one. It emits 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

Attaches to an existing Voyage, makes it current for the execution context, and updates the process-wide fallback. 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

If SAIL_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

Records a timestamped event on the current span/agent. Agent attribution comes from the enclosing 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

Usable as a context manager or a decorator. @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.
Returns a context manager that emits 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 via merge_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.
Sailbox commands are covered the same way. A foreground command’s auto-span lasts until the command finishes; a background command’s auto-span covers only starting it, not the detached run.

agent

Returns a context manager that marks all events and inference calls inside it as belonging to a named 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.
Each call enters a fresh agent/span frame (concurrent calls don’t share state) and emits the same events as the with form.

error

Records an error-level voyage.error event without terminating the Voyage.

complete

Emits 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

Emits 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

Blocks until all buffered events are delivered, raising on delivery failure. A bounded best-effort flush also runs automatically at process exit, but that is not a substitute for complete()/fail() for product-critical terminal state.

headers

Returns a copy of 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

Env vars a child process needs to 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

Disables Voyage telemetry for this process by installing a no-op current Voyage, the public form of the state 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; level one of debug/info/warn/error; payload and metadata must be JSON objects; occurred_at must be RFC3339 with a timezone; version must be a positive integer. Oversized human text does not raise: a message or span name over 4 KiB is truncated, and a payload over 64 KiB is replaced by a {"_truncated": true, "_original_bytes": N} stub, each with a warning. metadata over 64 KiB raises at create() (startup is when failing is cheapest). create() and attach() 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_dropped notice is emitted; lifecycle events (voyage.started plus the terminal voyage.completed/voyage.failed) are preserved.
  • Delivery semantics: flush() blocks and raises delivery errors (sail.VoyageError and subclasses), the strict primitive. complete() and fail() perform a bounded best-effort flush and warn instead of raising. cancel() calls the dedicated cancel endpoint and raises sail.VoyageError subclasses on HTTP failure. event() never raises network errors.
  • Cancel semantics: cancel() marks the server-side Voyage cancelled without enqueueing or posting a voyage.cancelled event. It stops recording future trace updates; it does not terminate external agent code. Without SAIL_API_KEY, NoopVoyage.cancel() is a safe no-op.