@telequick/agent-widget is the embeddable web front end for a TeleQuick
voice agent. It renders a connect button, an audio visualizer driven by a real
AnalyserNode, a live transcript, a typed-turn composer, and a camera tile —
and it opens the media itself over MoQT/QUIC.
Provenance. The widget’s UI shape and interaction model derive from
LiveKit’s
agent-starter-react
and @livekit/components-react (Apache-2.0), retargeted from a LiveKit
Room onto TeleQuick’s transport. The component fork lives in
telequick-components/. Nothing in the page talks to a LiveKit
server: there is no Room, no SFU, and no WebRTC transport.- a native TeleQuick agent (the in-engine ASR→LLM→TTS pipeline), and
- an external agent — your own
livekit-agentsworker running over our transport (see LiveKit Agent over TeleQuick Transport).
An external agent gets the mic always, and the camera and keyboard when it
asks. A native agent gets all three. An external agent gets the mic,
plus whichever of the camera (
accepts_video / acceptsVideo) and keyboard
(accepts_text / acceptsText) its worker declared.Nothing is silently dropped either way. The engine announces the real
capability for the call — from what the worker that took it declared — and the
widget shows or hides each control in response, so a user is never offered an
input that goes nowhere. See
Camera and keyboard input.The shape of it
1. Install
npm
AgentSession — a five-method interface (start, sendText,
setCameraEnabled, setMicEnabled, stop) — so the UI and the wire evolve
apart, and so the widget is testable without a browser. createWtSession
adapts the proven voice client to that interface:
2. Mint the grant on your backend
One call against the administration API with an org-scopedmpk_… key. Mint
the key in the console under Settings → API keys with scope
mcp:telephony:write.
- TypeScript
- Python
app/api/agent-session/route.ts
3a. Embed it as a tag
The shortest path — no React in your own build. Drop the element and one module script:
The element fetches the grant on
connectedCallback and mounts the widget. A
missing agent-id, or a configureAgentWidget you forgot to call, logs an
error to the console rather than rendering a dead button.
The snippet uses bare module specifiers, so it needs either a bundler step or
an import map
on the page. Bundling is the usual answer: the result is one script tag you
host yourself, which is also what lets you pin a version.
3b. Or mount the React component
Use this when the widget lives inside an app you already build, and you want the state transitions for your own chrome.AgentSession
required
What opens the transport.
createWtSession({ WtHumanAudioSession, grant })
builds one; any object implementing AgentSession
(start / sendText / setCameraEnabled / setMicEnabled / stop) also
works, which is what makes the widget testable without a browser.{ audio?, text?, video? }
default:"all three"
Which controls the embedder wants.
(s: SessionState) => void
Called on every transition, for your own page chrome.
string
Optional heading above the widget.
Modalities are negotiated, not assumed
A control appears only when both yourmodalities config and the
agent’s announced capabilities allow it. The engine announces once per session
what the agent can accept besides the microphone, so a control never appears
that would silently do nothing:
The disabled control carries the reason the engine sent
(
AgentCapabilities.videoReason) as its tooltip, so the page explains itself —
an external worker that never asked for frames reads “External agent: this
worker did not declare video support.”
Camera frames are rate-limited server-side to one frame per second by
default. Video into a realtime model is billed per frame, and a browser
sending 30 fps would multiply the bill by thirty for no gain. Frames are also
capped at 1 MB each, typed turns at 4 KB.
The wire, for reference
You never write this — the client does — but it is what to look for in a network trace.Hanging up
The browser closing its MoQT session tells the engine nothing on its own: the agent session stays up with its provider WebSocket open, and for an external agent your worker stays assigned to a job with no caller. End the session from your backend when the widget reportsended:
Browser support
The media plane is WebTransport-only in the browser today — there is no WebSocket fallback rung in the browser SDK; that ladder ships only in the native cores. In practice: a current Chromium, Edge, Firefox 133+, or Safari Technology Preview. See Browser compatibility.Related
LiveKit Agent over TeleQuick Transport
Point the widget at your own
livekit-agents worker. The page does not change.Outbound Calls from a LiveKit Agent
The same worker, dialling out: one call or a paced campaign.
Browser Voice Agent
The layer underneath: capture, encode, and play back by hand.
Recordings API
Fetch the audio for any session the widget started.