Skip to main content
attach() is VoiceGateway’s observe seam: a passive meter for cost and latency. Call it once with your AgentSession or PipelineTask and every STT, LLM, and TTS call flowing through it is priced and recorded. It never reroutes, throttles, or blocks a call: control lives in guard(). Because attach() is the only source of metrics, pairing it with guard() never double-counts; guard() writes no metrics of its own.

Signature

attach() picks its framework by inspecting the session’s type. An object it does not recognize as a LiveKit or Pipecat target falls through the LiveKit path silently: no error, no rows written. Pass a real AgentSession or PipelineTask.

Wiring

attach() subscribes to the per-component metrics_collected events (works with any plugin, no wrapping) and finalizes on the session’s close event: in-flight writes drain, then the sink flushes.

What it records

One row per request, through a Sink (local SQLite by default, or a collector when collector_url / VOICEGW_COLLECTOR_URL is set): LLM and TTS usage come straight from the framework’s usage metric. STT is derived from audio duration: on Pipecat, the observer accumulates each STT service’s AudioRawFrame bytes and converts 16-bit mono PCM to seconds.

Channel, session, and tenant

Channel ("telephony" / "web") auto-detects from the transport when omitted: a LiveKit SIP participant or a Pipecat Twilio/Telnyx/Plivo serializer means telephony; anything else means web. Session correlation is automatic and identical on both frameworks: the id is created (or reused) via Python contextvars the moment attach() runs. Every row from that session shares it, and a task spawned inside the same async context inherits it too, with no argument needed. Tenant attribution is opt-in: pass tenant_id= when one deployment serves several customers. See Tenant attribution.

policy: naming the set instead of spelling it out

The four capture flags below are the mechanism. policy= is the way to say what you actually want, because the interesting thing is the set and four independent booleans cannot name one.
The flags mix two unrelated questions, and the policies separate them:
  • What does it cost to run? Dead air polls once a second for the life of every session. Turn capture costs nothing between events. lean is timing_only without that standing cost.
  • What does it disclose? A transcript is what the caller said. A snapshot is your own system prompt and every tool payload. debug is the only policy that carries snapshots, and it is named for when that trade is worth making.
Tool-call rows ride turns: they correlate to a turn and their turn_index is meaningless without one. Both are timing-only and neither carries a payload. An explicit argument overrides the policy, so attach(session, policy="timing_only", dead_air=False) is lean. An unknown policy name is refused rather than defaulting, because a typo that silently selected standard would turn “nothing the caller said” into transcript capture.
The environment kill-switches still beat everything, including a policy. A fleet-wide override that a policy could cancel would not be a kill-switch.

room, heartbeat, transcript, snapshots, turns, dead_air (LiveKit)

snapshots defaults off because it is a strictly larger disclosure than a transcript. A transcript is what the caller said. A snapshot carries your system prompt, the full message history, and every tool call’s arguments and result, so it captures your own prompt and whatever payloads your tools handle. That should be asked for, not assumed.
Snapshots need a local sink. When VOICEGW_COLLECTOR_URL is set, capture is skipped: a collector has no replay tables, and the dashboard reads replay from the local store, so capturing there would buffer rows nothing could flush.

Config: what actually gates writes

attach() always writes cost and latency; no config toggle turns it off. It resolves storage from environment variables only, never from voicegw.yaml: VOICEGW_DB_PATH for local SQLite (default ~/.config/voicegateway/voicegw.db), or VOICEGW_COLLECTOR_URL + VOICEGW_API_KEY for fleet mode. voicegw.yaml’s observability: block (latency_tracking, cost_tracking, request_logging, all default true) configures the Gateway behind voicegw serve / voicegw status / the dashboard: a separate reader-side process, not attach()’s own write path.

See also

  • guard(): the active control seam that composes with attach().
  • Frameworks and extras: install voicegateway[livekit] vs voicegateway[pipecat].