Docs · SDK 1.8.2 · updated 3 Sep 2026

Ship it before lunch.

Install, initialise, wrap the run. Everything else on this page is optional — read it when you hit the thing it describes.

Quickstart

Nocturne needs three things: the package, an API key, and a run boundary. The SDK patches the model and HTTP clients already imported in your process, so individual functions do not need decorating.

# pip install nocturne
import os, nocturne

nocturne.init(
    api_key=os.environ["NOCTURNE_KEY"],
    service="support-triage",
    environment="prod",
    capture_bodies=False,        # default
)

with nocturne.run(tenant_id=customer.id, name="triage") as run:
    result = agent.invoke({"input": message})
    run.tag(outcome=result.status, escalated=result.escalated)

Spans appear in the explorer within about two seconds of the run ending. If nothing shows up, check that the process exited cleanly — the exporter flushes on shutdown, and kill -9 loses the last batch.

Runs and spans

A run is one invocation of an agent, from the request that triggered it to the answer it produced. A span is one unit of work inside that run. Spans nest: a tool call that retries three times has three http.* children.

Everything in the product hangs off that hierarchy. Cost is summed up the tree, rules are evaluated over sibling spans, and replay walks the tree in recorded order.

  • agent.run — the root. Carries tenant, environment, and the final outcome.
  • llm.chat — one model call. Carries model, token counts, finish reason and time-to-first-token.
  • tool.<name> — one tool invocation, whatever the framework calls it.
  • http.<method> — an outbound request seen by the patched transport, including SDK-internal retries.

Span schema

Every span carries these fields. Anything else you attach lands in attributes and is queryable.

FieldTypeNotes
span_idstring16 hex chars, unique within the trace
parent_idstring?null on the root span only
namestringe.g. tool.refunds.create
kindenumagent · llm · tool · http · retry
start_ns / end_nsint64monotonic, nanoseconds since epoch
statusenumok · error · degraded
gen_ai.usage.*intinput_tokens, output_tokens, cached_tokens
cost_usdfloatcomputed at ingest from the model price table
tenant_idstring?inherited by every child span

Sampling

Head sampling is a percentage decided at the root; the whole run is kept or dropped together, so you never see a trace with holes in it. Tail sampling keeps everything for a short window and then discards runs that were unremarkable.

nocturne.init(
    sampling=nocturne.TailSample(
        keep_errors=True,          # always keep failed runs
        keep_slower_than="p95",     # and the slow tail
        keep_costlier_than=0.25,    # USD per run
        baseline=0.10,             # 10% of everything else
    )
)

Tail sampling buffers spans in the collector for up to 30 seconds. A run longer than that is decided on what has arrived so far. For long-horizon agents, prefer head sampling with keep_errors handled by a rule.

Redaction

Redaction runs in your process, before export. Matched values are replaced with a stable hash so you can still group by them without holding the plaintext.

nocturne.init(
    capture_bodies=True,
    redact=[
        nocturne.redact.EMAIL,
        nocturne.redact.CREDIT_CARD,
        nocturne.redact.pattern(r"cus_[a-zA-Z0-9]{5,}", label="customer"),
    ],
)

Alert rules

Rules are evaluated on the ingest path as spans arrive, which is why a loop can page you 38 seconds in rather than after the run finally gives up. The rule language is small on purpose.

# runaway tool loop
when span.kind == "tool"
 and count(span.name) over run > 5
 and distinct(span.input_hash) over run == 1
then page("refunds-oncall") with sample_trace

# context bloat before it becomes a bill
when span.kind == "llm"
 and span.usage.input_tokens > 4000
 and count(span.kind) over run > 3
then slack("#agent-health")

Replay API

Replay re-executes a stored trace with the recorded tool responses, so external systems are not touched twice. Swap the model, the prompt, or both, and diff the outcome.

curl -X POST https://api.nocturne.dev/v1/replays \
  -H "Authorization: Bearer $NOCTURNE_KEY" \
  -d '{
    "trace_id": "9f3c2a41",
    "overrides": { "model": "claude-sonnet-4-5", "prompt_version": 12 },
    "tools": "recorded"
  }'

# {"replay_id":"rp_71ac","cost_usd":0.0181,"status":"ok",
#  "diff_url":"https://app.nocturne.dev/replays/rp_71ac"}

Self-hosting

The collector and span store run as two containers plus ClickHouse. On Enterprise you get a Helm chart; the Compose file below is enough to evaluate it on a laptop.

git clone https://github.com/nocturne-dev/collector
cd collector && docker compose up -d

# point the SDK at it
export NOCTURNE_ENDPOINT=http://localhost:4318/v1/traces

Support

Engineers answer the inbox. Free and Team: help@nocturne.dev, one business day. Scale: a shared Slack Connect channel, four hours during EU or NA business hours. Enterprise: a named engineer and a four-hour P1 target, any hour.

Security disclosures go to security@nocturne.dev. We publish the SOC 2 Type II report and the most recent penetration test summary under NDA on request.

developed by simple flow