@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.
It works against both kinds of agent, with no change to the page:
  • a native TeleQuick agent (the in-engine ASR→LLM→TTS pipeline), and
  • an external agent — your own livekit-agents worker 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

The page never holds a privileged credential. Your backend mints a per-call grant — a call sid plus a capability token scoped to that one call, 1-hour TTL — and the widget opens the transport with it.

1. Install

npm
React 18+ is a peer dependency. The widget deliberately never opens the transport itself. It renders against an 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:
WtHumanAudioSession is not on the public registry yet. It is the browser voice client the console’s Test Drive ships (wt-human-audio-client.ts), and today it is a single file you vendor into your app rather than a package you install. Until it is published, either vendor that file — ask support@telequick.dev for the current copy — or implement AgentSession yourself over the public MoQT helpers in Browser Voice Agent (captureMicrophone, OpusPlayer, attachCaller from telequick_connect/moqt). Everything else on this page is unaffected by which one you pick.

2. Mint the grant on your backend

One call against the administration API with an org-scoped mpk_… key. Mint the key in the console under Settings → API keys with scope mcp:telephony:write.
app/api/agent-session/route.ts
Never ship the mpk_… key to the browser. It is org-scoped and acts as its creator. The page only ever sees the per-call grant, which authorises one call’s namespaces and expires in an hour.

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 your modalities 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 reports ended:

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.

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.