Vaanieval
Concepts

Architecture

How the SDK, the on-disk session package, the upload protocol and the dashboard fit together — and why capture is deliberately kept off the live media path.

Vaanieval has three moving parts and one file format between them.

The design constraint

A voice agent is a soft-real-time system. Anything that adds latency to the media path degrades the very experience you are trying to measure. Every architectural decision follows from that:

DecisionConsequence
Capture writes to local disk, never to the networkA slow or unreachable collector cannot stall a call
Upload happens after session.end()Network cost is paid when nobody is listening
Response bodies are never drained to inspect themReading a stream to capture it would hold back the first token
Instrumentation failures are swallowed by defaultMany capture failures degrade observability rather than propagate; this is not a zero-impact guarantee
The library ships with no runtime dependenciesNothing in your agent's dependency tree changes

Error isolation is a tradeoff, not just a feature. strict: false (the default) means capture can silently degrade — you find out from capture_status in the manifest and the unverifiable class in the dashboard, not from an exception. Set strict: true in staging to surface those failures loudly, and keep it off in production.

Layer 1: the SDK

The SDK does three things:

It models the call. You open a Session, then Turns, then Operations. See Sessions, turns and operations.

It instruments providers. On construction the observer patches HTTP (httpx/aiohttp in Python, global fetch in Node) and offers websocket observation. A patched call is recorded only when there is an ambient session and the URL matches one of your endpoint rules — otherwise it passes through untouched. See Endpoints and instrumentation.

It writes. Events are appended to a JSONL file; PCM chunks are appended to per-track raw files. Writes are serialised so append order is preserved.

Layer 2: the session package

session.end() produces a directory. That directory is the contract between the SDK and the dashboard — you can inspect it, diff it, archive it, or replay it into a different backend.

manifest.json
events.jsonl
call.audio

manifest.json is written last, after finalization, using a staged write and rename. Its presence signals a finalized package, not necessarily complete capture: inspect capture_status for known gaps. A crash before finalization can leave a directory without a manifest.

call.audio is a single stereo track composed from the two mono captures — agent on the left channel, caller on the right — aligned on the session timeline so the silences are real silences. See Session package.

Layer 3: the dashboard

A FastAPI service with three responsibilities: accept uploads, import them, and serve the console.

Handshake

POST /v1/sessions with the manifest and an idempotency-key header equal to the session id. Returns the URLs to upload each object to.

Development-checkout uploaders additionally declare object sizes and SHA-256 digests as upload_objects during this handshake and support per-object response headers. These additions are not yet verified in the public Git revisions; the local API remains backwards compatible. See HTTP API.

Objects

PUT each object. Bodies are streamed to a .part file and renamed on success, capped at 128 MiB (413 beyond that). Only four object names are accepted: events.jsonl, call.audio, caller.audio, agent.audio.

Completion

POST /v1/sessions/{id}/complete with each object's byte size and SHA-256. The server verifies both and rejects the completion on a mismatch — a truncated upload is a 400, not a call with quietly missing turns.

The session lands as ready if operations were imported, or partial if the objects arrived but no operations could be read. partial is what surfaces as unverifiable in the dashboard.

Scaling limits you should know before you commit. The dashboard stores metadata in a single SQLite file and audio on the local filesystem, with unauthenticated reads, no tenant isolation and no automated retention policy. Keep app.main:app on localhost; an ingest API key is not tenant isolation. The separate app.cloud.main:app hosted-beta entrypoint is under construction, not a public SaaS readiness claim. Challenger evaluation runs on a two-worker thread pool in the same process, so a batch of replays competes with request serving. See Self-hosting for the full list.

Hosted development is a separate runtime

The development checkout contains app.cloud.main:app and a separate python -m app.cloud.worker process, with cloud dependencies and an explicit SQL migration sequence (001_initial.sql then 002_media_and_workspace_deletion.sql, schema version 2). This does not replace the local runtime described above. Admissions default closed and require controlled-beta enablement, a subject allowlist and open admissions.

The hosted working tree now includes separate WAV playback/media-link and asynchronous workspace-data deletion paths. A recording's preview.available, not the global playback capability alone, determines whether its preview is ready. Workspace deletion requires recent verified Clerk factor authentication, retains a disclosed minimal ledger and does not delete the Clerk identity or constitute a legal erasure guarantee. Derived turns, analytics and evaluations remain unavailable, with no evaluation-provider calls. Tests of the slice do not establish real Azure permissions, recovery/rollback, policy/privacy readiness or public-launch readiness.

Where the numbers come from

Latency coverage depends on recorded milestones, audio evidence and integration metadata. Derived spans and inferred attribution carry caveats; an operation's duration alone does not establish endpointing or caller-visible wait:

The caller-visible reply wait is caller stops speaking → first audio byte. If any milestone in that chain is missing, the turn is reported as unmeasurable and excluded from the percentile — it is never estimated from operation start and end times. That is why the dashboard always publishes a denominator like "68% of 111 turns measurable".

Next

On this page