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
Wiring
- LiveKit
- Pipecat
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 aSink (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.
leanistiming_onlywithout 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.
debugis the only policy that carries snapshots, and it is named for when that trade is worth making.
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 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]vsvoicegateway[pipecat].