@microsoft/voice-widget-embed
v0.2.0
Published
Registers the <voice-agent> custom element: a self-contained CDN/global build plus an ESM build for bundlers.
Readme
@microsoft/voice-widget-embed
The self-contained CDN bundle — the one-line <script> entry that auto-registers the
<voice-agent> custom element. Bundles the supported UI @microsoft/voice-widget-ui
(which in turn uses the headless @microsoft/voice-widget core) plus the Voice Live
provider, so a single tag embeds a working voice agent.
<script src="https://cdn.jsdelivr.net/npm/@microsoft/voice-widget-embed" async></script>
<voice-agent
provider="voicelive"
auth-endpoint="https://your-broker.example.com/session"
provider-config='{"targetType":"model","model":"gpt-realtime"}'
></voice-agent>Loading the bundle imports @microsoft/voice-widget-provider-voicelive (which
self-registers "voicelive") and calls register() to define <voice-agent>. On the user's first
gesture the widget acquires a session (POST the SDP offer to auth-endpoint) and connects over
WebRTC. On stop or terminal disconnect it releases browser media and ends the broker control
session; Start a call creates a fresh SDP session rather than resuming the previous conversation.
Attributes
The attributes below are the embed's stable, supported API (semver-guarded). For theming (
--va-*variables) and internationalization, see@microsoft/voice-widget-ui. Attributes listed as "not read yet" are reserved — they become supported in a future release.
| Attribute | Meaning |
| --- | --- |
| provider | required — provider name (e.g. voicelive) |
| provider-config | provider config as a JSON object — its shape depends on the provider. For the built-in voicelive: model mode {"targetType":"model","model":"gpt-realtime"} or agent mode {"targetType":"agent","agentName":"…","projectName":"…"} (optional voice, agentVersion, VAD, … pass through — see @microsoft/voice-widget-provider-voicelive). A third-party provider defines its own shape. One of provider-config / widget-id. |
| widget-id | broker resolves stored config server-side from this id (alternative to inline provider-config). The page carries only the id; your broker maps it to the real config. See the widget-id example. One of provider-config / widget-id. |
| auth-endpoint | optional neutral session endpoint; the adapter adds provider-specific fields (Voice Live adds sdpOffer) |
| fetch-credentials | credentials mode for the auth-endpoint fetch: include (default — sends cookies for same-domain cookie auth), omit, or same-origin |
| variant | bar / compact / full |
| placement | bottom-right / bottom-left / top-right / top-left / inline |
| start-label | nonblank trigger text; blank values use the built-in default |
| default-expanded | start expanded |
| mute-button | show the mic mute button — opt-in, off by default; add the bare attribute (or mute-button="true") to enable |
| show-transcript | render the built-in live transcript — opt-in, off by default |
| text-input | render the typed-message form — opt-in, off by default; enabled while connected |
| telemetry-console | mirror structured telemetry events to console.debug — opt-in, off by default (a local-dev sink; see Telemetry) |
| theme | color scheme: auto (default — follows the OS via prefers-color-scheme) / light / dark; observed live |
| accent-color | any CSS color for the accent (buttons, focus ring). Sets both accent stops to that color for a solid fill; falls back to the brand default if omitted or invalid; observed live |
| orb-palette | brand palette for the animated orb, as a JSON object { "base", "highlight" } of CSS colors — e.g. '{"base":"#0a5a4e","highlight":"#00e0a0"}'. Independent of accent-color (see below); invalid or partial input falls back to the default orb; observed live |
| lang-strings | i18n text overrides as a JSON object, merged over the built-in English strings — e.g. '{"idle":"Habla con el asistente","startCall":"Iniciar llamada"}'. Keys: idle, connecting, listening, thinking, speaking, connected, disconnected, error, startCall, endCall, expandAssistant, minimize, mute, unmute, panelLabel. Unknown keys are ignored; a nonblank start-label still wins the trigger text; observed live |
| ui-locale | UI-locale metadata as a JSON object { "lang"?, "dir"?, "strings"? } — sets the host lang/dir and string overrides in one update, for hosts driving locale from their own i18n runtime. dir is ltr/rtl/auto; strings uses the lang-strings keys. Only the provided fields apply; observed live |
Reserved for a future release — avatar and dismissible — are not read yet.
When the widget doesn't appear
If a required attribute is missing or malformed, the element cannot mount and nothing renders — it deliberately does not paint fallback text over your page. The failure is reported three ways so it is still easy to find:
| Where | What you get |
| --- | --- |
| voice-agent:error event | detail is { code, message, recoverable } — the same channel as session errors, so existing error handling catches setup failures too. code is "mount_failed" (recoverable: false) for a bad attribute, or "provider_not_registered" (recoverable: true) when a named provider never registers (see below) |
| data-voice-agent-error attribute | the reason, readable straight from devtools with no script wiring |
| console | [voice-agent] failed to mount: …, or the waiting-for-provider warning |
document.addEventListener("voice-agent:error", (e) => {
if (e.detail.code === "mount_failed") console.warn("check your attributes:", e.detail.message);
if (e.detail.code === "provider_not_registered") console.warn("provider never showed up:", e.detail.message);
});A named but not-yet-registered provider is not an error — at first. A provider can
self-register from a separate <script> that has not run yet (script order is not
guaranteed), so <voice-agent> waits for it instead of failing. While waiting, the element
sets a data-voice-agent-awaiting-provider="<name>" attribute (readable from devtools;
removed once it mounts or is removed from the page) and stays silent, because a slow
provider script is normal.
If the provider still has not registered 10 seconds later, the wait reports itself on all
three channels above — a voice-agent:error event with code: "provider_not_registered" and
recoverable: true, a matching data-voice-agent-error attribute, and a console warning —
each naming the provider it is waiting for and which providers are registered. That is
the usual symptom of a misspelled provider value or a missing provider <script>.
The element keeps waiting after that report (hence recoverable: true, and hence
data-voice-agent-awaiting-provider stays set): if the provider does eventually register, the
element mounts normally and both attributes are cleared.
The most common cause is supplying neither provider-config nor widget-id.
Transcript and typed messages
<voice-agent
provider="voicelive"
auth-endpoint="/api/session"
provider-config='{"targetType":"model","model":"gpt-realtime"}'
show-transcript
text-input
></voice-agent>Both features are optional and independent. Partial agent transcript events update one in-progress bubble; the final event replaces it. The transcript retains the latest 100 messages and resets on a new call. Typed input is sent as a Realtime user message and appears in the same transcript.
Theming & i18n
<voice-agent
provider="voicelive"
auth-endpoint="/api/session"
provider-config='{"targetType":"model","model":"gpt-realtime"}'
theme="dark"
accent-color="#0f6cbd"
lang-strings='{"idle":"Habla con el asistente","startCall":"Iniciar llamada","endCall":"Colgar"}'
></voice-agent>theme and accent-color are neutral, cross-provider theming knobs; for deeper restyling,
override the --va-* CSS variables directly (see @microsoft/voice-widget-ui).
theme="auto" tracks the OS light/dark setting live.
Control accent vs. orb palette
accent-color and orb-palette are independent knobs:
accent-colorstyles the controls — the trigger pill, call button, focus rings, and the user's transcript bubbles. It never recolors the orb.orb-palettestyles only the animated orb. You give two brand anchors —base(the sphere/body) andhighlight(the crests and glow) — and the full orb gradient and wave colors are derived from them while preserving the original animation and gloss.
Use them together to, say, keep a neutral accent while giving the orb a distinct brand color:
<voice-agent
provider="voicelive"
auth-endpoint="/api/session"
provider-config='{"targetType":"model","model":"gpt-realtime"}'
accent-color="#0f6cbd"
orb-palette='{"base":"#0a5a4e","highlight":"#00e0a0"}'
></voice-agent>An invalid or partial orb-palette (e.g. only base) falls back to the default orb rather than
throwing, so a typo never breaks the widget.
Change any of the UI attributes without removing the element or restarting its active session:
const widget = document.querySelector("voice-agent");
widget.setAttribute("theme", "light");
widget.setAttribute("accent-color", "#2899f5");
widget.setAttribute("orb-palette", JSON.stringify({ base: "#7a1030", highlight: "#ff8a5c" }));
widget.setAttribute("lang-strings", JSON.stringify({ startCall: "Iniciar llamada" }));Removing an attribute restores its default (theme returns to auto, accent overrides are cleared,
and strings return to English defaults while a nonblank start-label still wins the idle text).
An unsupported live theme also resolves to auto, and an invalid accent clears the override,
matching a fresh mount with those attribute values. Invalid live lang-strings JSON is logged and
leaves the current strings unchanged because it cannot be parsed safely.
To switch language, direction, and strings together — the typical move when a host drives locale from
its own i18n runtime — set ui-locale (or call the element's setUiLocale() method, preferred for
frequent in-call updates). Both keep the active session, mic, and connection intact:
const widget = document.querySelector("voice-agent");
widget.setUiLocale({ lang: "ar", dir: "rtl", strings: { endCall: "إنهاء المكالمة" } });
// or declaratively:
widget.setAttribute("ui-locale", '{"lang":"ar","dir":"rtl","strings":{"endCall":"إنهاء المكالمة"}}');lang/dir land on the widget host so screen readers announce the new language and the panel lays out
in the right direction; strings re-resolve from English defaults so an omitted key never keeps the prior
language. Invalid live ui-locale JSON is logged and leaves the current locale unchanged.
Model vs. agent target
provider-config selects the Voice Live target:
<!-- Model mode: a Realtime model. -->
<voice-agent provider="voicelive" auth-endpoint="…"
provider-config='{"targetType":"model","model":"gpt-realtime"}'></voice-agent>
<!-- Agent mode: a voice-enabled Foundry agent (Entra-only; the broker holds the credential). -->
<voice-agent provider="voicelive" auth-endpoint="…"
provider-config='{"targetType":"agent","agentName":"support-bot","projectName":"contoso-prod"}'></voice-agent>The widget forwards provider-config to your broker unchanged; the broker resolves the target
(model or agent) and holds the credential. Agent mode is Entra-only. Optional fields (voice,
language, VAD, etc.) are passed through too — see
@microsoft/voice-widget-provider-voicelive for the full config shape.
Programmatic / advanced
The package ships two builds, and which one you use decides how you load it:
| Build | File | How to load |
| --- | --- | --- |
| global (IIFE) | dist/index.global.js — the jsdelivr/unpkg entry | a plain <script src="…"> tag. Self-contained (it inlines its dependencies, because there is no module resolver on the page) and exposes window.VoiceAgent |
| ESM | dist/index.js — the import entry | import it through a bundler. It imports its dependencies by name rather than inlining them, so the page ends up with exactly one provider registry shared between the widget and any provider you register. Those bare specifiers are also why a browser-native <script type="module"> cannot load this file directly — use the global build for that |
import {
register,
mountVoiceAgent,
registerProvider,
createProvider,
hasProvider,
} from "@microsoft/voice-widget-embed";register()— define<voice-agent>(idempotent; auto-called on load).mountVoiceAgent(target, opts)— mount programmatically, accepting values that can't be expressed as attributes (e.g.clientTools). See@microsoft/voice-widget-ui.registerProvider(name, factory)— register the factory that creates one adapter per widget.createProvider(name)— create a fresh adapter for advanced direct use.hasProvider(name)— check whether a provider name is registered.onProviderRegistered(listener)— observe provider registrations as they happen; returns an unsubscribe. Not replayed for providers already registered, so checkhasProviderfirst.
The same names are available on window.VoiceAgent when the global build is loaded — that is how
a third-party provider script registers itself on a page with no bundler.
Bring your own provider
The default entry includes Voice Live. An application that supplies its own provider can omit that code:
import "@microsoft/voice-widget-embed/provider-neutral";
import "@your-scope/voice-widget-provider-yourvendor";For a no-bundler page, load dist/provider-neutral.global.js followed by the provider's IIFE. That
global still exposes window.VoiceAgent, including registerProvider, createProvider, and
hasProvider, but registers no provider itself. createProvider(name) creates an independent
adapter instance for advanced direct use.
Client-side tools — the voice-agent:call event
The agent can call functions the host page registers. HTML attributes can't carry functions, so
the declarative element exposes a pre-connect event: <voice-agent> dispatches
voice-agent:call before it connects, and the host mutates event.detail.config.clientTools.
<!-- Attach the listener BEFORE the element upgrades. `voice-agent:call` normally fires
synchronously from connectedCallback, so a listener added after the bundle has
registered the element has already missed it. (If `provider` names a provider
that has not self-registered yet, the element waits and the event instead fires
the moment that provider registers — see "Ordering matters" below.) -->
<script>
document.addEventListener("voice-agent:call", (e) => {
e.detail.config.clientTools = {
// name (case-sensitive) must match the tool declared to the model on the session
addToCart: ({ productName }) => {
addItem(productName);
return { ok: true }; // JSON return is handed back to the agent
},
};
});
</script>
<voice-agent provider="voicelive" auth-endpoint="…" provider-config='{"targetType":"model","model":"gpt-realtime"}'></voice-agent>
<!-- The global (IIFE) build: self-contained, so a plain <script> tag can load it
directly. `dist/index.js` is ESM with bare imports and needs a bundler — see
"Programmatic / advanced". -->
<script src="/path/to/voice-agent-embed.global.js"></script>Ordering matters. <voice-agent> dispatches voice-agent:call from its
connectedCallback — synchronously, the moment the element upgrades — as long as its
provider is already registered, which is the common case (a bundled first-party provider,
or any provider script that ran before the element upgraded). So register the listener
first. The event is bubbles: true / composed: true, so a document-level listener
added before the bundle loads (as above) catches it wherever the element sits. Attaching to
the element in a <script> that runs after an already-registered bundle upgraded it is
too late — the handler never fires and no tools are injected.
If provider names a provider that has not self-registered yet (e.g. its <script> has not
run — async script order is not guaranteed), the element instead waits and dispatches
voice-agent:call later, the moment that provider registers. A document-level listener
added up front still catches it either way; only a listener attached to the element itself,
after the fact, can miss it.
The tool schemas are declared to the model server-side (via your broker / session config); the
handlers are registered here. A void return is acked as { ok: true }; an unknown tool or a
thrown handler returns an error to the agent. Only registered functions can be invoked. When you
mount in JS instead of using the tag, pass clientTools to mountVoiceAgent directly (see
@microsoft/voice-widget-ui); the underlying protocol lives in
@microsoft/voice-widget-provider-voicelive.
Client tools run untrusted input. "Only registered functions can be invoked" bounds which functions run, not what they are asked to do. A handler's arguments are filled in by the model and can be prompt-injected — validate them before any sensitive action (e.g. allow only
http:/https:URLs before navigating; neverevalor inject a model-supplied string as HTML/JS). Its return value is sent to the model and may be spoken aloud, so return only end-user-safe values — no PII or internal error detail. See Security - handler inputs and outputs.
registerClientTool (programmatic) and voice-agent:unhandledtool
The <voice-agent> element exposes no imperative methods — for a declarative embed, inject
tools before connect via the voice-agent:call event shown above. If you mount
programmatically, the controller returned by mountVoiceAgent exposes registerClientTool
(throws on a duplicate name; returns an unregister):
import { mountVoiceAgent } from "@microsoft/voice-widget-embed";
const controller = mountVoiceAgent(document.querySelector("#slot"), {
provider: "voicelive",
config: { targetType: "model", model: "gpt-realtime" },
authEndpoint: "/api/session",
});
const unregister = controller.registerClientTool("openModal", ({ id }) => openModal(id));When the agent calls a name with no registered handler, the element fires a
voice-agent:unhandledtool CustomEvent (detail: { name, args, callId }):
el.addEventListener("voice-agent:unhandledtool", (e) => {
console.warn("unhandled client tool:", e.detail.name, e.detail.args);
});This is notification only — it cannot fulfill the call. The adapter still returns a standard error output to the agent.
Scope: model and agent targets over WebRTC are supported.
Telemetry — the voice-agent:telemetry event
The element dispatches a voice-agent:telemetry CustomEvent for every structured telemetry
event, with the event as detail:
el.addEventListener("voice-agent:telemetry", (e) => {
const event = e.detail; // VoiceAgentTelemetryEvent — { name, ...envelope, data }
track(event.name, event);
});Each event carries a widgetInstanceId / correlationId / sessionId envelope for tracing a
session across the widget and your broker, spanning failures that happen before a server-side
session exists. The event is opt-in, low-frequency, and the SDK-supported structured counterpart
to the raw-event firehose. The telemetry-console attribute additionally mirrors events to
console.debug (opt-in, off by default) — a local-dev sink that changes no behavior. See
docs/telemetry.md for the full event catalogs and wiring.
