Stările și evenimentele apelului
Un apel e o mașină de stări care emite un flux de evenimente doar cu adăugări. Stările îți spun unde e apelul acum; evenimentele îți spun tot ce s-a întâmplat. Ambele vocabulare sunt definite o singură dată în @callagent/contracts și oglindite în PHP (App\Enums\CallState, App\Enums\CallEventType), cu un test care citește sursa PHP și pică dacă cele două nu mai coincid.
Motivele de încheiere sunt excepția, și au deviat
Stările și tipurile de eveniment chiar sunt oglindite, iar un test o impune. Motivele de încheiere nu sunt. CallEndedReasonSchema din contracte are 19 valori; App\Enums\Calls\EndedReason din control plane are 20, scrise altfel — customer_hangup față de customer_hung_up, concurrency_exceeded față de concurrency_limit, insufficient_credits față de insufficient_credit.
GET /calls/{id} și payload-ul de webhook întorc valorile din PHP. Sunt pe Când ceva eșuează. Folosește lista de mai jos doar când citești chiar @callagent/contracts.
Cele 17 stări
created ─→ queued ─→ ringing ─→ connecting ─→ connected
│
┌──────────────────────────────────────────────┴───────────────────────────┐
│ listening ⇄ user_speaking ⇄ thinking ⇄ tool_executing │
│ ⇄ assistant_speaking ⇄ on_hold ⇄ transferring │
│ ⇄ human_queue ⇄ human_connected │
└──────────────────────────────────────────────────────────────────────────┘
│
ending ─→ ended | failedÎn bucla de conversație aproape orice stare poate urma aproape oricărei alteia, pentru că apelantul poate întrerupe în orice clipă — asta este barge-in-ul, exprimat ca tabelă de tranziții. ending, ended și failed sunt accesibile din orice stare neterminală.
| Grup | Stări |
|---|---|
| Pornire | created, queued, ringing, connecting, connected |
| Conversație | listening, user_speaking, thinking, tool_executing, assistant_speaking |
| Ținut sau în mișcare | on_hold, transferring, human_queue, human_connected |
| Terminale | ending → ended, failed |
ending e intenționat o fundătură: singurul ei succesor e ended. Un apel care a început să își ia rămas-bun nu poate fi tras înapoi în conversație.
Concurența e ținută de fiecare stare neterminală, și de asta finalizarea eliberează explicit slotul — altfel un apel care a crăpat fără să ajungă la ended ar ține un slot la nesfârșit.
Cele 19 motive de încheiere din contracte
Astea sunt valorile din contracte. API-ul REST întoarce cele 20 din control plane, scrise altfel — vezi avertismentul de mai sus.
| Grup | Valori |
|---|---|
| A închis cineva | customer_hangup, assistant_hangup, operator_hangup |
| Asistentul a decis | assistant_end_call_tool, assistant_end_call_phrase |
| S-a încheiat în altă parte | transfer_completed |
| S-a atins o limită | max_duration_reached, silence_timeout |
| Nu s-a conectat niciodată | no_answer, busy, rejected, voicemail_reached |
| Refuzat înainte de start | concurrency_exceeded, insufficient_credits |
| S-a stricat ceva | pipeline_error, provider_error, telephony_error, gateway_shutdown |
| — | unknown |
Distincția dintre assistant_end_call_tool și assistant_end_call_phrase contează când depanezi: primul înseamnă că modelul a ales să apeleze endCall, al doilea înseamnă că apelantul a spus ceva din endCallPhrases.
Fluxul de evenimente
Șaptezeci de tipuri de eveniment tipate, grupate pe prefix, identice în TypeScript și în PHP. call_events e doar cu inserări, iar GET /calls/{call}/events le întoarce în ordine.
Tabelul de mai jos le grupează pe cele pe care e cel mai probabil să acționezi. GET /webhooks/events e un vocabular diferit, mai mic — cele 28 de tipuri de webhook de ieșire, pe Webhooks.
| Categorie | Evenimente |
|---|---|
| Ciclu de viață | call.created, call.ringing, call.connected, call.ended, call.failed, call.state.changed, transport.connected, transport.disconnected |
| Vorbire, intrare | user.speech.started, user.speech.ended, transcript.partial, transcript.final, endpoint.detected |
| Model | llm.started, llm.first_token, llm.completed, llm.failed, llm.aborted |
| Sinteză | tts.started, tts.first_audio, tts.completed, tts.failed, tts.aborted |
| Redare | assistant.speech.started, assistant.speech.interrupted, assistant.speech.completed |
| Speech to speech | s2s.started, s2s.first_audio, s2s.completed, s2s.failed |
| Tools | tool.started, tool.completed, tool.failed, tool.rejected |
| MCP | mcp.started, mcp.completed, mcp.failed |
| Knowledge | knowledge.queried, knowledge.insufficient |
| Rutare | handoff.started, handoff.completed, transfer.started, transfer.completed, transfer.failed |
| Contact center | operator.queued, operator.ringing, operator.connected, operator.rejected, operator.missed, operator.wrap_up, queue.abandoned, queue.overflowed |
| DTMF și SIP | dtmf.received, dtmf.sent, sip.request.sent, sip.response.received, voicemail.detected |
| Artefacte | recording.consent_announced, recording.consent_recorded, recording.started, recording.paused, recording.resumed, recording.ready, analysis.completed |
| Sănătatea furnizorilor | provider.fallback, provider.error |
Formele payload-urilor per eveniment sunt în referința tipurilor de eveniment.
Formatul comun
Fiecare eveniment împarte un singur format, ca un consumator să poată ordona și corela fără să știe tipul:
{
"id": "01JQ…",
"type": "endpoint.detected",
"organizationId": "01JQ…",
"callId": "01JQ…",
"legId": "01JQ…",
"assistantId": "01JQ…",
"sequence": 42,
"at": "2026-01-01T12:00:03.412+00:00",
"elapsedMs": 3412,
"generationId": "gen_7",
"traceId": "…",
"data": { "strategy": "hybrid", "silenceMs": 400, "confidence": 0.82 }
}elapsedMs sunt milisecundele de la începutul apelului, și ele sunt cele care permit desenarea unui timeline fără join pe timestamp-uri. generationId grupează fiecare eveniment care ține de un tur al asistentului — evenimentele llm.*, tool.* și tts.* ale unui singur răspuns îl împart, deci „cât a costat turul 7 și cât a durat" e un singur filtru.
Cum citești fluxul
curl "$CALLAGENT_API_URL/calls/01JQ…/events" \
-H "Authorization: Bearer $CALLAGENT_API_KEY"const response = await fetch(`${process.env.CALLAGENT_API_URL}/calls/${callId}/events`, {
headers: { Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}` },
});
const { data: events } = await response.json();
// Un tur, cap-coadă
const turn = events.filter((event) => event.generationId === 'gen_7');import os, requests
events = requests.get(
f"{os.environ['CALLAGENT_API_URL']}/calls/{call_id}/events",
headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
timeout=30,
).json()["data"]Același vocabular e folosit pentru webhooks — abonează-te la evenimentele care te interesează, în loc să faci polling — și pentru subiectele NATS pe care cele două planuri le schimbă intern (ca.call.<organizationId>.<action>).
Mesaje, nu doar evenimente
GET /calls/{call}/messages întoarce conversația în sine, ca roluri și conținut — system, user, assistant, tool, operator — cu interrupted marcând orice tur al asistentului pe care apelantul l-a tăiat, și cu toolCalls / toolCallId legând o cerere de rezultatul ei. Asta e vederea de folosit pentru evaluare și pentru a reda o conversație într-un model; fluxul de evenimente e pentru depanat cum s-a întâmplat.
Mai departe
- Cum funcționează un apel — ce produce evenimentele astea.
- Apeluri și artefacte — ce lasă în urmă un apel încheiat.
- Webhooks — cum primești evenimentele pe măsură ce se întâmplă.