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)
// npm i @nocturne/sdk
import { init, run } from "@nocturne/sdk";
init({
apiKey: process.env.NOCTURNE_KEY!,
service: "support-triage",
environment: "prod",
captureBodies: false,
});
await run({ tenantId: customer.id, name: "triage" }, async (span) => {
const result = await agent.invoke({ input: message });
span.tag({ outcome: result.status });
});
# already running an OTel collector? skip the SDK
curl -X POST https://ingest.nocturne.dev/v1/traces \
-H "Authorization: Bearer $NOCTURNE_KEY" \
-H "Content-Type: application/x-protobuf" \
--data-binary @spans.pb
# 202 Accepted
# {"accepted":418,"rejected":0,"trace_ids":["9f3c2a41"]}
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.
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.