Web SDK
@callagent/web-sdk — voce și chat în browser (§60). Zero dependențe la runtime; se livrează într-un tag <script> pe paginile altora, așa că fiecare kilobyte este Largest Contentful Paint-ul altcuiva.
| Bundle | Brut | gzip | brotli |
|---|---|---|---|
dist/index.js (ESM) | 56.59 KB | 14.75 KB | 12.62 KB |
dist/widget.global.js (IIFE) | 53.71 KB | 17.38 KB | 15.39 KB |
Pentru lista completă de tipuri generate vezi tipurile SDK.
Instalare
Nepublicat încă
@callagent/web-sdk este private: true fără publishConfig, deci nu este pe npm: npm i @callagent/web-sdk eșuează cu 404. Astăzi se consumă dintr-un checkout al acestui repository.
pnpm install
pnpm --filter @callagent/web-sdk buildCalea de import de mai jos este cea pe care o va avea odată ce pachetul este publicat; în interiorul workspace-ului funcționează deja.
Pornește un apel
import { CallAgent } from '@callagent/web-sdk';
const client = new CallAgent({
publicKey: 'ca_pk_…',
apiUrl: 'https://api.callagent.dev',
});start() are nevoie de un endpoint care încă nu există
start() face un POST către POST /api/v1/client/calls ca să creeze sesiunea. Ruta aceea nu este implementată în control plane, deci pe un deployment real apelul eșuează. Serverul de demo de mai jos o simulează, și așa este exersat SDK-ul astăzi.
const call = await client.start({
assistantId: '01J…',
variables: { plan: 'pro' },
metadata: { pageUrl: location.href },
});Evenimente
call.on('transcript', (t) => …); // { speaker, text, final, at, confidence?, language? }
call.on('message', (m) => …); // mesaje assistant / user / tool
call.on('speech-start', (e) => …); // { speaker, at }
call.on('speech-end', (e) => …);
call.on('state', (s) => …); // { state, previous, at } — oglindește CallState de pe server
call.on('error', (e) => …); // CallAgentError cu un `code` stabil
call.on('connected', (c) => …);
call.on('ended', (e) => …);
call.on('reconnecting', (e) => …); // { attempt }
call.on('reconnected', (e) => …);on întoarce o funcție de dezabonare.
Control
call.mute();
call.unmute();
call.setMuted(true);
call.send('do you deliver to Cluj?'); // mesaj tastat, voce sau chat
call.sendDtmf('123#');
call.setVolume(0.6);
await call.stop();Doar chat
const chat = await client.startChat({ assistantId: '01J…' });Același protocol, aceleași evenimente, fără microfon și fără difuzor.
Tool-uri client-side (§32)
client.registerTool('getCartTotal', () => ({ total: 42.5, currency: 'EUR' }));
client.on('client-tool', async ({ name, arguments: args }) => {
if (name !== 'scrollTo') return undefined; // undefined = „nu e al meu”
document.querySelector(String(args.selector))?.scrollIntoView();
return { scrolled: true };
});O înregistrare pe nume are prioritate față de un handler generic. Un handler care aruncă trimite eroarea înapoi la model în loc să blocheze tura; un tool neînregistrat este raportat.
Erori
CallAgentError.code este o uniune închisă, așa că ramifici pe el, nu pe textul mesajului:
invalid_public_key · private_key_rejected · auth_failed · token_expired
microphone_denied · microphone_unavailable · audio_unsupported
transport_failed · transport_closed · protocol_mismatch
call_start_failed · call_ended · client_tool_failed · not_connected · aborted · server_errorPermisiuni pentru microfon
Eșecurile getUserMedia sunt cea mai mare sursă de tichete „widget-ul nu merge”, iar erorile proprii ale browserului nu înseamnă nimic pentru un vizitator. Fiecare este tradusă într-un mesaj lizibil:
| Eroarea browserului | Ce i se spune utilizatorului |
|---|---|
NotAllowedError | "Microphone access was blocked. Click the padlock in the address bar…" |
NotFoundError | "No microphone was found. Plug one in or pick a different input device." |
NotReadableError | "The microphone is in use by another application." |
| context nesecurizat | "Microphone capture requires HTTPS. Load this page over https:// (or localhost)." |
Aceste mesaje sunt în engleză, oricare ar fi locale-ul paginii
Sunt string-uri hard-codate în apps/web-sdk/src/audio/microphone.ts, iar pachetul de traduceri al widget-ului (ro, en, fr, …) nu are intrări pentru ele — acoperă doar eticheta launcher-ului, butoanele și stările apelului. Un vizitator român va vedea textul englezesc de mai sus. Dacă îți construiești propriul UI, prinde CallAgentError și afișează propriul mesaj în funcție de code (microphone_denied, microphone_unavailable, audio_unsupported) în loc să afișezi message.
queryMicrophonePermission() este exportată pentru paginile care vor să explice prompt-ul înainte de a-l declanșa.
Transporturi
WebRTC acolo unde RTCPeerConnection există și se poate construi; WebSocket în rest. Forțează unul cu transports: ['websocket'].
Sub WebRTC, browserul se ocupă de jitter buffering, de mascarea pierderilor de pachete și de encodarea Opus. Calea WebSocket transmite PCM16 mono brut în frame-uri binare de 20 ms, cu un canal de control JSON { "event": "..." } alături — exact formatul de fir pe care WebRtcTransport din gateway îl implementează deja.
Ce este cablat cap la cap
SDK-ul își implementează complet partea lui din protocolul de sesiune: toate cele 14 evenimente de server pe care le declară protocolul lui (ready, format, answer, ice, state, transcript, message, speech, clear, mark, tool_call, error, ended, stop) au un handler, iar toate cele 10 frame-uri de client sunt tipizate.
Lipsa este pe partea de gateway, și ea decide care dintre evenimentele de mai sus le vei observa efectiv astăzi.
| Direcție | Cablat cap la cap | Declarat, încă netransportat |
|---|---|---|
| Gateway → browser | format · clear · mark · stop (+ answer/ice când un agent ICE le furnizează) | ready · state · transcript · message · speech · tool_call · error · ended |
| Browser → gateway | start · dtmf · mark · stop · offer · ice | mute · text · tool_result · auth |
Citit practic: audio și barge-in-ul funcționează. Transcrierile, mesajele de conversație, granițele de vorbire, schimbările de stare și apelurile de tool inițiate de server sunt specificate și tratate, dar nu ajung niciodată, așa că call.on('transcript', …) este cod corect care încă nu se va declanșa.
Trei consecințe de știut înainte să construiești pe ele:
call.send()este inert. Frame-ultextajunge în ramura netratată a gateway-ului, deci un mesaj de chat tastat este aruncat. Asta e tot modul chat.- Răspunsurile tool-urilor client-side sunt inerte.
tool_resulteste la fel netratat — și cum nicitool_callnu ajunge vreodată, dus-întorsul nu are nicio jumătate vie. mute()nu ajunge la server. Tot oprește microfonul local, ceea ce percepe utilizatorul, dar gateway-ul nu este anunțat.
Evenimentele necunoscute sunt ignorate de ambele părți, deci nimic din toate astea nu aruncă. O asimetrie chiar pierde date în tăcere: gateway-ul trimite { "event": "dtmf", "digits": … }, care nu se află în setul de evenimente de server al SDK-ului, deci DTMF-ul de intrare este parsat ca necunoscut și aruncat.
client.on('client-tool') este un hook de înregistrare, nu un eveniment de observare
Este declarat pe CallAgentEvents, și nimic nu îl emite vreodată. Trecerea unui listener înregistrează un executor generic de tool-uri a cărui valoare de retur este trimisă înapoi la model — care este comportamentul util și motivul pentru care API-ul are forma asta. Dar un listener adăugat ca să urmărească apelurile de tool trecând nu va fi apelat niciodată. Evenimentul care chiar este emis este call.on('tool-call', …), per apel.
Întrerupere
Când gateway-ul trimite clear, redarea se oprește în câteva zeci de milisecunde: fiecare nod audio programat este oprit la momentul zero și coada este aruncată. Gateway-ul se oprește din scris în aceeași clipă, iar socket-ul este ordonat, deci nimic din enunțul întrerupt nu mai ajunge după aceea. Sub WebRTC, elementul remote este pus pe mut 150 ms ca să înghită jitter buffer-ul.
Reconectare
Backoff exponențial cu jitter (500 ms → 20 s, 8 încercări implicit), configurabil cu reconnect. Fiecare reconectare reia handshake-ul cu un token citit din nou. Audio-ul de la microfon este aruncat cât timp conexiunea este căzută, nu pus în coadă — rejucarea unei vorbiri vechi de câteva secunde face asistentul să răspundă la o întrebare depășită.
Suport în browsere
Chrome, Edge, Firefox și Safari 14.1+. Captura folosește un AudioWorklet acolo unde este disponibil și cade înapoi pe ScriptProcessorNode. Este necesar un context securizat (HTTPS sau localhost).
Cum îl rulezi local
pnpm --filter @callagent/web-sdk build
pnpm --filter @callagent/web-sdk demo # http://localhost:4321Serverul de demo include un control plane și un gateway simulate care vorbesc același protocol de sesiune, așa că SDK-ul poate fi exersat înainte ca gateway-ul real să își expună socket-ul. Schimbul de token pe care îl simulează se potrivește exact cu POST /api/v1/client-tokens din producție, până la envelope-ul data și la câmpurile snake_case.