Skip to content

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ă.

GrupStări
Pornirecreated, queued, ringing, connecting, connected
Conversațielistening, user_speaking, thinking, tool_executing, assistant_speaking
Ținut sau în mișcareon_hold, transferring, human_queue, human_connected
Terminaleending → 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.

GrupValori
A închis cinevacustomer_hangup, assistant_hangup, operator_hangup
Asistentul a decisassistant_end_call_tool, assistant_end_call_phrase
S-a încheiat în altă partetransfer_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 startconcurrency_exceeded, insufficient_credits
S-a stricat cevapipeline_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.

CategorieEvenimente
Ciclu de viațăcall.created, call.ringing, call.connected, call.ended, call.failed, call.state.changed, transport.connected, transport.disconnected
Vorbire, intrareuser.speech.started, user.speech.ended, transcript.partial, transcript.final, endpoint.detected
Modelllm.started, llm.first_token, llm.completed, llm.failed, llm.aborted
Sintezătts.started, tts.first_audio, tts.completed, tts.failed, tts.aborted
Redareassistant.speech.started, assistant.speech.interrupted, assistant.speech.completed
Speech to speechs2s.started, s2s.first_audio, s2s.completed, s2s.failed
Toolstool.started, tool.completed, tool.failed, tool.rejected
MCPmcp.started, mcp.completed, mcp.failed
Knowledgeknowledge.queried, knowledge.insufficient
Rutarehandoff.started, handoff.completed, transfer.started, transfer.completed, transfer.failed
Contact centeroperator.queued, operator.ringing, operator.connected, operator.rejected, operator.missed, operator.wrap_up, queue.abandoned, queue.overflowed
DTMF și SIPdtmf.received, dtmf.sent, sip.request.sent, sip.response.received, voicemail.detected
Artefacterecording.consent_announced, recording.consent_recorded, recording.started, recording.paused, recording.resumed, recording.ready, analysis.completed
Sănătatea furnizorilorprovider.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:

json
{
  "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 ​

bash
curl "$CALLAGENT_API_URL/calls/01JQ…/events" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY"
ts
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');
python
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 ​

Scrisă după cod. Ce nu este încă implementat este marcat ca atare, niciodată descris ca funcțional.