- Install the transport plugin into your existing agent process.
- Run the process as a worker that dials out to the edge.
- Point a ClutchCall agent at the worker.
- Route an inbound number.
- Place a real call.
AgentSession, your STT/LLM/TTS plugins,
your provider keys, and your tool calls all stay verbatim. Only the bytes on
the wire are ours.
This is the plugin path. If you must keep your LiveKit server running
(other participants join the room), use the front-proxy sidecar instead:
Bridge a LiveKit Agent.
The integration reference compares
both paths.
What you’ll build
:443 plane. It decodes that edge to the runtime’s 8 kHz PCM bus. Your
worker holds one outbound QUIC/WebTransport session to the edge. The session
carries three lanes:
- presence (liveness),
- audio over datagrams, and
- per-call control (job assign, flush, barge-in, playback finished) over a stream.
Why the agent is the client. Your worker dials out and opens no
inbound ports. It works behind NAT, in a private subnet, or in a container
with no public address. Nothing about your network has to change.
Prerequisites
required
A working
livekit-agents (Python) or @livekit/agents (TypeScript) agent
with an entrypoint, plus the provider keys it already uses.required
A media app key + secret for your tenant, in
CLUTCHCALL_MEDIA_KEY and
CLUTCHCALL_MEDIA_SECRET. These are distinct from your API
credentials. Issue them from the console (agent.clutchcall.dev) or the
control-plane API.required
See SIP Trunking /
Number Provisioning.
required
The worker needs QUIC egress to
engine.clutchcall.dev. The WebSocket fallback
covers presence only. Media over TCP is a degraded last resort.Build the mini-app
1
Start from your unmodified agent
Any LiveKit agent works. It needs no ClutchCall awareness. This is the
file that you already have, untouched:
- Python
- TypeScript
my_agent.py
Your
entrypoint still calls session.start(room=ctx.room, ...). Leave
it. The plugin wraps AgentSession.start, binds our audio IO, and drops
the room argument. The SDK skips its own RoomIO whenever custom audio
IO is set, so the code never touches the room.2
Install the transport plugin
- Python
- TypeScript
quic extra pulls in aioquic. Without it, the package imports but cannot
connect.3
Run it as a worker
Replace your worker runner (The worker authenticates, registers under
cli.run_app(...)) with ours. This is the only
new file in your repo.- Python
- TypeScript
worker.py
agent_name, and waits. You
should see it report ready and await calls. It stays connected with a
keepalive. If the link drops, it reconnects with exponential backoff. An
engine restart therefore does not strand it on a dead socket.4
Point a ClutchCall agent at the worker
The engine models a remote agent as a
VENDOR_BRIDGE node whose
vendor_provider is clutchcall. The decision lives in the agent
config, so your dialplan and trunk routing stay untouched.agent-config.json
With this config, the runtime runs no turn detection and no model
in the engine. It hands the caller’s audio to your worker. It streams your
agent’s audio back onto the call. The full field reference is in
Runtime Configuration.
5
Route a number to the agent
Attach an inbound DID or SIP trunk to this agent. Use the same routing that
you would use for any native ClutchCall agent. See
SIP Trunking and
Number Provisioning.
6
Connect a caller
Dial the number. To drive it programmatically, place an outbound call
from the SDK. On answer, the engine dispatches to your worker in the same
way as for an inbound call:
- TypeScript
- Python
What your agent sees
Yourentrypoint gets a JobContext whose ctx.room is a veneer, not a
LiveKit room. It carries the fields that agents actually read. The fields
mirror how LiveKit surfaces SIP participant attributes. An agent that reads the
caller’s number therefore keeps working unchanged:
Room APIs beyond these fields are not backed. This includes track
publication, participant enumeration, and room data messages. There is no room
behind the veneer.
Verify it worked
The gateway gives this agent everything that it gives a native agent. The caller edge is identical:- Call events & CDR — the call appears in reports with an
initiated → established → clearedlifecycle and a Q.850 clear cause. - Media quality — per-call MOS / jitter / loss scoring lands on the CDR.
- Recording — the engine captures both legs if recording is enabled for the trunk.
- Transcripts — the plugin forwards finalized turns over the control stream. Your remote agent’s history lands in the same store as a native agent’s history.
Troubleshooting
The worker registers, but calls find no agent
The worker registers, but calls find no agent
Presence is tied to a live connection. If the worker’s session drops and
the reconnect did not land yet, dispatch finds nothing. Check that the
worker logs a ready line after the most recent reconnect. Check that
vendor_room in the agent config exactly matches agent_name in the
worker config.The agent speaks, but the caller hears nothing or a click
The agent speaks, but the caller hears nothing or a click
The cause is almost always a sample-rate mismatch. Check
sample_rate /
sampleRate against the leg (8000 for PSTN). The plugin paces and
re-chunks outbound audio into steady 20 ms frames, so it handles bursty
TTS. It does not handle a wrong rate.The worker connects but no audio flows
The worker connects but no audio flows
Media travels on QUIC datagrams. Check that outbound UDP/443 to
engine.clutchcall.dev is open. A network that permits TCP/443 but drops UDP
will complete presence and stall media.Audio bandwidth is higher than expected
Audio bandwidth is higher than expected
The wire codec is raw
pcm16 today. Opus is not yet wired. Budget for
uncompressed audio between your worker and the edge.Related
LiveKit integration reference
All three paths side by side: plugin, front-proxy, browser shim.
Bridge a LiveKit Agent
The front-proxy sidecar. Use it when your LiveKit room must keep running.
From Self-Hosted LiveKit
The full migration plan: trunks, tokens, rollout.
Runtime Configuration
Every pipeline node field, including
VENDOR_BRIDGE.
