diff --git a/agents/build/custom-llm.mdx b/agents/build/custom-llm.mdx index 38172ef..4cd4253 100644 --- a/agents/build/custom-llm.mdx +++ b/agents/build/custom-llm.mdx @@ -126,7 +126,7 @@ A custom LLM never silently falls back to a platform model, because a platform m - Each request gets one retry and a 10 second response cap. - When a generation still fails, the agent speaks a brief hold line and stays on the call. -- After three consecutive failed generations the agent apologizes, hangs up, and the session records `ended_reason: llm_endpoint_failure`. Each failure also emits an `llm.endpoint_error` event on the session timeline. +- After three consecutive failed generations the agent apologizes, hangs up, and the session ends with `end_reason: llm_endpoint_failure` (in the [sessions API](/agents/monitor/conversation-history#status-and-end-reason), the dashboard, and the `call.ended` webhook's `ended_reason`). Each failure also emits an `llm.endpoint_error` event on the session timeline. Voice conversations are latency sensitive, so aim for a time-to-first-token under 800 ms. Turn latency is attributed per session in [conversation history](/agents/monitor/conversation-history), which lets you tell endpoint time from platform time. diff --git a/agents/monitor/conversation-history.mdx b/agents/monitor/conversation-history.mdx index 693c649..52a64db 100644 --- a/agents/monitor/conversation-history.mdx +++ b/agents/monitor/conversation-history.mdx @@ -54,6 +54,7 @@ Each row carries the session facts: "agent_name": "Support agent", "name": "Order #4821 follow-up", "status": "completed", + "end_reason": "user_hangup", "source": "phone", "caller_number": "+15551234567", "dialed_number": "+14155550100", @@ -70,12 +71,35 @@ Each row carries the session facts: `caller_number` and `dialed_number` are set for phone sessions only (E.164) and are `null` for web sessions. `name` is the display name you passed when creating the session (`null` when omitted), and `metadata` echoes back whatever you attached when creating the session. +### Status and end reason + +`status` says whether a conversation happened: `completed` means a participant connected and the call is over (it is billed even if the runtime ended it), `failed` means nobody ever connected, `active` means it is in progress, `pending` means the session was created and no one has connected yet, and `unknown` means the platform temporarily lost track of a live call. + +`end_reason` says why it ended. It is `null` while a session is live, for a short moment after it ends, and on sessions that predate the field. + +| `end_reason` | `status` | Meaning | +| ---------------------- | ----------- | -------------------------------------------------------------------------------- | +| `user_hangup` | `completed` | The caller hung up | +| `agent_hangup` | `completed` | The agent ended the call (end-call tool, workflow end node, or idle hangup) | +| `escalated` | `completed` | The call was transferred to a human | +| `conversation_timeout` | `completed` | The call reached its maximum duration | +| `heartbeat_timeout` | `completed` | The agent runtime stopped responding mid-call | +| `agent_join_timeout` | `completed` | The caller connected but no agent joined the call | +| `internal_error` | `completed` | The agent runtime hit an internal error | +| `llm_endpoint_failure` | `completed` | A [custom LLM](/agents/build/custom-llm) failed repeatedly and the agent hung up | +| `room_join_timeout` | `failed` | The caller never connected before the join deadline | +| `cancelled` | `failed` | The session was ended before anyone connected | +| `dial_failed` | `failed` | An outbound call was not answered; `dial_status` has the detail | + +The same values appear on the [`call.ended` webhook](/agents/monitor/webhooks) and in the dashboard, so the three surfaces never disagree about a session. + ### Filters | Parameter | Behavior | | ---------------------------------- | ----------------------------------------------------------------------------------------------------- | | `agent_id` | Sessions of a single agent | | `status` | One of `pending`, `active`, `completed`, `failed`, `unknown`; comma-separate values to match several | +| `end_reason` | One or more of the `end_reason` values above, comma-separated | | `caller_number` | Exact match on the caller's E.164 number; a bare number gets `+` prepended automatically | | `created_after` / `created_before` | ISO 8601 timestamps | @@ -109,6 +133,7 @@ curl "https://api.fish.audio/v1/agent/sessions/SESSION_ID" \ { "session_id": "…", "status": "completed", + "end_reason": "user_hangup", "items": [ { "type": "message", diff --git a/agents/monitor/webhooks.mdx b/agents/monitor/webhooks.mdx index 2f128dc..04dbc6f 100644 --- a/agents/monitor/webhooks.mdx +++ b/agents/monitor/webhooks.mdx @@ -105,6 +105,7 @@ Every payload carries the `event` name and a `session` object with a fixed set o "branch_id": "b7d4…", "source": "phone", "status": "completed", + "end_reason": "user_hangup", "conversation_started_at": "2026-07-23T12:01:12Z", "conversation_ended_at": "2026-07-23T12:04:16Z", "duration_seconds": 184, @@ -113,7 +114,7 @@ Every payload carries the `event` name and a `session` object with a fixed set o "agent_name": "Support agent", "config_hash": "sha256:9c41…" }, - "ended_reason": "hangup" + "ended_reason": "user_hangup" } ``` @@ -180,7 +181,7 @@ Every payload carries the `event` name and a `session` object with a fixed set o -`ended_reason` is `hangup` for a call that terminated normally and `error` when the session failed. New values may be added as richer end causes ship. Treat unrecognized values as informational rather than rejecting the event. +`ended_reason` is the same value as `session.end_reason` and uses the [sessions API vocabulary](/agents/monitor/conversation-history#status-and-end-reason): `user_hangup`, `agent_hangup`, `escalated`, `conversation_timeout`, `heartbeat_timeout`, `agent_join_timeout`, `internal_error`, or `llm_endpoint_failure`. `call.ended` is only sent for sessions in which a participant connected, so the never-connected reasons (`room_join_timeout`, `cancelled`, `dial_failed`) do not appear here; query the sessions API with `status=failed` for those. In the rare case the reason is not settled yet at delivery time, both fields are `null`. New values may be added; treat unrecognized values as informational rather than rejecting the event. `dial_status` is `answered`, `busy`, `no_answer`, or `failed`. `answered_by` is deprecated: answering-machine detection has been removed, so it is `unknown` once the call is answered and `null` before (older events may carry `human` or `voicemail`). Both facts also appear on the session, alongside `direction` (`inbound` or `outbound`) and `batch_call_id` (reserved, always `null` today); these session fields are present in every webhook payload, with the dial fields `null` on inbound sessions. The `phone_call.dial_finished` snapshot is taken when the dial resolves, so on an answered call `conversation_ended_at` and `duration_seconds` are still `null`. The final numbers arrive with `call.ended`.