Skip to main content

zevruna (Python)

Execution traces for AI agents — models, tools, APIs, databases and MCP, on one timeline.

One run, one trace: what the agent did, how long each step took, and whether the run actually achieved anything. Including the runs that failed while every call returned 200.

This is the Python half of the Zevruna protocol. It emits the same versioned event schema as @zevruna/observe and behaves identically down to retry backoff, batch size, buffer eviction and redaction. That is enforced, not asserted: both SDKs replay the same ten fixtures in CI and the build fails if they diverge.

Status: beta. The protocol is stable; the ergonomics may still move.

Install

pip install zevruna

No runtime dependencies, deliberately. An observability SDK that drags a dependency tree into your process has made your build a place your monitoring can break.

Python 3.8+.

Usage

One boundary, wrapped. Instrumenting the MCP client covers every tool call it makes, so these two lines are usually the whole integration.

from zevruna import observe_agent, instrument_mcp_client

client = instrument_mcp_client(mcp_client, "acme-crm")

async with observe_agent("support-agent"):
    await run_support_agent(user_input)

Every entry point works in sync code too — with instead of async with. Context propagation uses contextvars, so a span opened inside asyncio.gather finds the span that was open when it started, not whichever sibling happened to finish last.

observe_agent(name, run_id=None, attributes=None)

One complete agent execution. Everything instrumented inside, including MCP calls through a wrapped client, attaches to this run automatically. An exception is recorded as a failed run and re-raised unchanged — your error handling is never degraded.

instrument_mcp_client(client, server_name)

Wraps call_tool so every call becomes a step: tool name, timing, outcome. Call sites do not change. A 200 carrying isError is recorded as a failure, because it is one, and raised as zevruna.McpToolError.

observe_step(kind, name, attempt=1, attributes=None)

Any other boundary worth timing. kind is model, tool, mcp, http, db, agent or approval. Nested calls become child steps automatically. Outside an observe_agent there is no run to attach to, so the step runs untraced rather than starting one.

async with observe_step("model", "plan", attributes={"model": "claude-opus-5", "input_tokens": 812}):
    await call_model(prompt)

mark_success(outcome=None) / mark_failure(reason, outcome=None)

The business outcome, which is not the HTTP status. A run where every request succeeded and the ticket never got resolved is a failed run, and only your code knows that.

milestone(name, attributes=None)

A zero-duration marker on the timeline — hand-off, human approval, escalation.

flush() / shutdown()

Runs are batched and flushed on a daemon thread. On serverless or any short-lived process, call shutdown() before exit or the buffered runs are lost. flush() returns a FlushResult with sent, queued, events, rejected, dropped, error and error_kind, and never raises.

await aflush() and await ashutdown() run the same work in a thread — urllib is blocking, and an SDK that stalls your event loop to deliver telemetry has broken the one promise it makes.

A 202 is not proof the runs were kept: over your plan's monthly quota the API accepts the request and drops the batch. flush() reads the response, so that comes back as sent=0 with the reason in error and error_kind="quota", and warns once on stderr. It is not requeued — the quota will still be there on the next flush, and a growing buffer would evict live runs.

Configuration

ZEVRUNA_TOKEN=zv_live_…
ZEVRUNA_ENDPOINT=https://zevruna.com/api
ZEVRUNA_ENVIRONMENT=production
ZEVRUNA_DISABLED=1
ZEVRUNA_SAMPLE_RATE=0.25
ZEVRUNA_BATCH_SIZE=50
ZEVRUNA_FLUSH_INTERVAL_MS=5000
ZEVRUNA_TIMEOUT_MS=10000
ZEVRUNA_MAX_RETRIES=2
ZEVRUNA_MAX_BUFFER=500
ZEVRUNA_CAPTURE_ERROR_TEXT=1
ZEVRUNA_PROTOCOL=legacy

Or pass them to init(...). init is optional; the first wrapper call configures from the environment. Every name, default and clamp is identical in the Node SDK.

What is collected

Step names, kinds, timings, attempt counts, error classes, and the attributes you pass. Never tool arguments, never tool results, never payloads.

  • A built-in key denylist (password, secret, token, api_key, authorization, credential, cookie, ssn, card_number and their neighbours) is matched case-insensitively as a substring and replaced before anything leaves the process. Token counts — input_tokens and friends — are explicitly exempt.
  • Your redact(key, value) hook runs after it.
  • Error text written by an MCP server can quote the arguments it rejected, so it is not transmitted by default. capture_error_text=True opts in. Either way your own code always receives the server's full message.

Details: what we collect.

Tests

python -m unittest discover -s tests -t .          # this SDK
node ../../protocol/contract-tests/run.mjs         # this SDK against every other one

License

MIT

Metadata

Release files for zevruna 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for zevruna 0.2.0
File Size Uploaded Provenance Actions
zevruna-0.2.0.tar.gz 20.2 kB Without provenance Details for zevruna-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for zevruna 0.2.0
File Interpreter ABI Platform Actions
zevruna-0.2.0-py3-none-any.whl Python 3 none any Details for zevruna-0.2.0-py3-none-any.whl

Total release size: 35.6 kB

Release files / zevruna-0.2.0.tar.gz

Download URL zevruna-0.2.0.tar.gz
Size 20.2 kB
Tags Source
SHA-256 checksum
How to use checksums
2a49f6e534b44194b9592c27af279bda24f5e26bbc94940fb030496ab9e0138e
BLAKE2b-256 checksum
How to use checksums
d4adad2ed61c6d1ecc29c1d196727f50747561bceeee2a93f23a84dac3445bcf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Release files / zevruna-0.2.0-py3-none-any.whl

Download URL zevruna-0.2.0-py3-none-any.whl
Size 15.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a4e6c557e7ed866844923aa3835d05bb67c925d0af03b3518a9a56a0a2fce10b
BLAKE2b-256 checksum
How to use checksums
ec86f75b97db674f7ec482fa26cfc19690e15765e384b6426ad9f3f345bc93eb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Security for zevruna 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page