diff --git a/agents/monitor/conversation-history.mdx b/agents/monitor/conversation-history.mdx index 9f2e086..88a4e3a 100644 --- a/agents/monitor/conversation-history.mdx +++ b/agents/monitor/conversation-history.mdx @@ -82,7 +82,7 @@ Each row carries the session facts: | ---------------------- | ----------- | -------------------------------------------------------------------------------- | | `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 | +| `escalated` | `completed` | The call was transferred to a human. See [Transfer items](#transfer-items) | | `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 | @@ -186,8 +186,9 @@ curl "https://api.fish.audio/v1/agent/sessions/SESSION_ID" \ | `type` | What it is | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `message` | One utterance. `role` is `user` or `assistant` (system prompt content is never exposed). `turn_id` groups items belonging to the same conversational turn. | -| `tool_call` | A tool invocation as it started. `input` is a JSON string, stored in full with no truncation. | +| `tool_call` | A tool invocation as it started. `input` is a JSON string, stored in full with no truncation. | | `tool_result` | The paired outcome, matched by `call_id`. `status` is `completed` or `failed`. `latency_ms` is the execution time. | +| `transfer` | One [call transfer](/agents/telephony/transfers) attempt, successful or not. See [Transfer items](#transfer-items). | Details worth knowing: @@ -204,6 +205,95 @@ Details worth knowing: appear without a version bump. +### Transfer items + +When the agent [transfers a phone call](/agents/telephony/transfers), each attempt becomes one `transfer` item, including attempts that failed. Its `call_id` is the `call_id` of the `_transfer_call` `tool_call` that started it, and its `created_at` is when the attempt started, which is where it sits in the timeline. Every field is always present and is `null` when there is no value. + +```json +{ + "type": "transfer", + "call_id": "tool_3c1…", + "destination": "Front desk", + "destination_type": "phone", + "target": "+14155550123", + "mode": "warm", + "warm_connect": "confirm", + "outcome": "merged", + "sip_status": null, + "dial_status": null, + "decline_reason": null, + "consult_end_reason": null, + "hold_started_at": "2026-07-22T09:15:40Z", + "hold_ended_at": "2026-07-22T09:16:25Z", + "dial_started_at": "2026-07-22T09:15:41Z", + "answered_at": "2026-07-22T09:15:57Z", + "ended_at": "2026-07-22T09:16:25Z", + "transferred_call_ended_at": "2026-07-22T09:30:11Z", + "transferred_call_duration_seconds": 826, + "transferred_call_end_reason": "caller_hangup", + "messages": [ + { + "role": "assistant", + "content": "Hi, I have a caller who needs help with a delivery. Can you take the call?", + "created_at": "2026-07-22T09:15:59Z" + }, + { + "role": "transfer_target", + "content": "Sure, put them through.", + "created_at": "2026-07-22T09:16:21Z" + } + ], + "created_at": "2026-07-22T09:15:39Z" +} +``` + +| Field | Description | +| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `call_id` | The `call_id` of the `_transfer_call` tool call that started the attempt. | +| `destination` | The destination's name at the time of the call. An empty string when the agent has a single unnamed destination. | +| `destination_type` | `phone` or `sip`. | +| `target` | The E.164 number or SIP URI that was dialed. | +| `mode` | `cold` or `warm`. | +| `warm_connect` | `confirm`, `briefing`, or `direct` on warm transfers. `null` on cold transfers. | +| `outcome` | How the attempt ended. `merged` and `referred` are the successful ones. See [Transfer results](/agents/telephony/transfers#transfer-results) for every value. `null` while the attempt is in progress. | +| `sip_status` | `dial_failed` and `refer_failed` only: the final SIP response code the carrier returned, such as `486`. `null` when the carrier gave none. | +| `dial_status` | `dial_failed` and `refer_failed` only: `busy`, `no_answer`, or `failed`. See [Why a call did not connect](/agents/telephony/transfers#why-a-call-did-not-connect) for how each is decided. | +| `decline_reason` | `declined` only: the agent's short summary of why your team member declined. `null` when they gave no reason. What they actually said is in `messages`. | +| `consult_end_reason` | `consult_closed` only: how the consult call ended before the calls were merged, `transfer_target_hangup` or `connection_lost`. `null` when the platform could not tell. | +| `hold_started_at` | Warm: when the caller started hearing hold music. | +| `hold_ended_at` | Warm: when the caller came off hold, connected to your team member or back with the agent. | +| `dial_started_at` | Warm: when the platform started dialing the destination. | +| `answered_at` | Warm: when the destination answered the consult call. | +| `ended_at` | When the attempt reached its outcome: the merge on a warm transfer, the completed handoff on a cold one (normally the moment the destination answered), or the failure. | +| `transferred_call_ended_at` | `merged` and `referred` only: when the call between the caller and your team member ended. See [After the transfer](#after-the-transfer). | +| `transferred_call_duration_seconds` | `merged` and `referred` only: how long that call lasted. Warm: from `ended_at` to `transferred_call_ended_at`. Cold: the duration on the carrier's call record. | +| `transferred_call_end_reason` | `merged` only: how that call ended. Always `null` on cold transfers. | +| `messages` | Warm: the transcript of the private consult call. `role` is `assistant` (the agent briefing) or `transfer_target` (the person it dialed). These lines never appear among the caller's `message` items. | + +A warm transfer's consult call is also recorded when the session [records audio](#what-gets-stored). Its tracks are under `transfers` in the [recording response](#download-recordings), where `transfer_id` is the item's `call_id`. + +#### After the transfer + +On a successful transfer the session ends with `end_reason` `escalated`, whatever happens on the call afterwards. The three `transferred_call_*` fields describe that call between the caller and your team member. + +`transferred_call_end_reason` is one of: + +| Value | Meaning | +| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `caller_hangup` | The caller hung up. On an [outbound call](/agents/telephony/outbound-calls), this is the person the agent called. | +| `transfer_target_hangup` | Your team member hung up. | +| `conversation_timeout` | The call reached the agent's [maximum call duration](/agents/build/configuration#call-duration). The limit counts from the start of the call, time before the transfer included. | +| `connection_lost` | One of the lines dropped. Nobody hung up. | +| `null` | The platform could not tell how the call ended. | + +When the values arrive depends on the mode: + +- **Warm**: when the call ends. They are `null` while the caller and your team member are still talking. +- **Cold**: the platform leaves the call at the handoff, so the end time and duration come from the carrier's call record. They appear up to about an hour after the call ends. Who hung up is never known, so `transferred_call_end_reason` stays `null`. +- **Cold transfers on [imported SIP numbers](/agents/telephony/byo-sip)**: the call record stays with your carrier, so the three fields stay `null`. + +When `transferred_call_ended_at` is `null`, the other two are `null` as well. A `referred` item with a duration of `0` means the carrier's record shows the destination never picked up, for example because the caller hung up while it was still ringing. + ### Correlate with live events `call_id` is the same identifier your client receives in the SDK's `toolCallStarted` / `toolCallCompleted` / `toolCallFailed` events. Log it live, and you can align in-call UI with the post-call record: jump from a tool chip in your interface straight to the matching `tool_call` / `tool_result` pair in history. See the [Web SDK](/agents/deploy/web-sdk). diff --git a/agents/telephony/transfers.mdx b/agents/telephony/transfers.mdx index bfdc9af..12d7f92 100644 --- a/agents/telephony/transfers.mdx +++ b/agents/telephony/transfers.mdx @@ -359,6 +359,61 @@ The response is the updated number, with `caller_id_sync_status` normally `synce The older boolean `cold_transfer_use_original_caller` still works, but it is deprecated. Use `transfer_caller_id` instead. +## Transfer results + +Every transfer attempt is recorded on the session, including the ones that failed: + +- **In the console**: open the call from the **Conversations** page. Each attempt has a card in the timeline with the destination, the result and why a failed one failed. A connected transfer also shows how long the caller and your team member talked and how their call ended. +- **Through the API**: each attempt is a `transfer` item in the session's `items`. See [Transfer items](/agents/monitor/conversation-history#transfer-items) for every field. + +A session with a successful transfer ends with `end_reason` `escalated`, however the call goes after the handoff. A failed attempt does not make the session `escalated`. What the agent does after a failure is described under [cold](#what-happens-on-a-cold-transfer) and [warm](#what-happens-on-a-warm-transfer) transfers. + +### Outcomes + +| `outcome` | Mode | What happened | +| ----------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `merged` | Warm | The caller and your team member were connected. | +| `referred` | Cold | The carrier took over the call, normally once the destination answered. A [call length](#after-a-successful-transfer) of `0` means the destination never picked up. | +| `dial_failed` | Warm | The destination could not be reached: busy, rejected, not answered within the [ring timeout](#timeouts), or the call failed. See [Why a call did not connect](#why-a-call-did-not-connect). | +| `declined` | Warm | Your team member said they could not take the call. `decline_reason` has the agent's summary of why. | +| `voicemail` | Warm | Voicemail answered the consult call. | +| `ivr` | Warm | A phone menu answered the consult call, with [Wait for a person to answer](#waiting-for-a-person-to-answer) on. | +| `consult_closed` | Warm | The consult call ended before the calls were merged. `consult_end_reason` is `transfer_target_hangup` when your team member hung up and `connection_lost` when the line dropped. | +| `consult_timeout` | Warm | Your team member answered but neither took nor declined the call within the [confirm timeout](#timeouts). | +| `merge_failed` | Warm | The caller and your team member could not be connected. | +| `refer_failed` | Cold | The carrier refused the handoff, or the destination did not answer while the caller was still on the line. See [Why a call did not connect](#why-a-call-did-not-connect). | +| `caller_hangup` | Warm | The caller hung up while on hold. | +| `cancelled` | Either | The transfer stopped before it started, for example because the caller hung up during the announcement or the call was already ending. | +| `unavailable` | Warm | The warm transfer could not be set up, so nobody was dialed. | + +`outcome` is `null` while an attempt is in progress. New values may be added, so don't reject ones you don't recognize. + +### Why a call did not connect + +`dial_failed` and `refer_failed` carry `dial_status`, which groups the carrier's answer the same way as an [outbound call's](/agents/telephony/outbound-calls#the-dial-outcome) `dial_status`: + +| `dial_status` | Meaning | SIP responses | +| ------------- | -------------------------------------------------- | --------------------------------------------------------- | +| `busy` | The line was busy, or the person rejected the call | `486`, `600`, `603` | +| `no_answer` | Nobody answered | `408`, `480`, `487`, or no response within the time limit | +| `failed` | Any other failure | Every other code | + +`sip_status` is the carrier's final SIP response code as received, and `null` when the carrier gave none. When a cold handoff rings past the time limit with the caller still on the line, the attempt is `refer_failed` with `dial_status` `no_answer` and no `sip_status`. + +### After a successful transfer + +`merged` and `referred` items carry three facts about the call between the caller and your team member: + +| Field | Warm | Cold | +| ----------------------------------- | ---------------------------------- | ------------------------------------------------------------------ | +| `transferred_call_ended_at` | When the call ended | When the call ended, from the carrier's call record | +| `transferred_call_duration_seconds` | Seconds from the merge to the end | Seconds from the answer to the end, from the carrier's call record | +| `transferred_call_end_reason` | Who hung up, or why the call ended | Always `null` | + +The end reason is `caller_hangup`, `transfer_target_hangup`, `conversation_timeout` (the agent's [maximum call duration](/agents/build/configuration#call-duration) was reached, counted from the start of the call), `connection_lost` (a line dropped), or `null` when the platform could not tell. See [After the transfer](/agents/monitor/conversation-history#after-the-transfer). + +A warm transfer's values are there once the call ends. On a cold transfer the platform leaves the call at the handoff, so the values come from the carrier's call record and appear up to about an hour after the call ends. They never appear for cold transfers on [imported SIP numbers](/agents/telephony/byo-sip), because that call record stays with your carrier. + ## Limitations - **Phone calls only**: web and SDK sessions have no phone leg to hand off, so the `_transfer_call` tool never ships for them.