Repository navigation
docs(agents): read transfer results and the call after a transfer #252
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,9 +4,9 @@ | |
| icon: "clock-rotate-left" | ||
| --- | ||
|
|
||
| Every production session your agents handle (from the API, the console, [public agents](/agents/deploy/public-agents), or [phone calls](/agents/telephony/inbound-calls)) is queryable over REST: a lightweight list for browsing, a merged timeline of messages and tool activity per session, and per-speaker recordings when the agent [records audio](#what-gets-stored). You can query a session while the call is still in progress. | ||
|
|
||
| This is a server-side API: authenticate with your API key. The client SDKs deliberately expose no history interface; fetch history from your backend and pass it to your frontend as needed. | ||
|
|
||
| ## What gets stored | ||
|
|
||
|
|
@@ -65,8 +65,8 @@ | |
| "metadata": { "crm_ticket": "T-4821" } | ||
| } | ||
| ], | ||
| "has_more": true, | ||
| "next_cursor": "…" | ||
| } | ||
| ``` | ||
|
|
||
|
|
@@ -82,7 +82,7 @@ | |
| | ---------------------- | ----------- | -------------------------------------------------------------------------------- | | ||
| | `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,12 +186,13 @@ | |
| | `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: | ||
|
|
||
| - **Role vocabulary**: the history API's `assistant` is the same speaker the [SDK's live events](/agents/deploy/web-sdk) call `agent`. | ||
| - **Order**: items are sorted by `created_at` ascending; a `tool_result`'s timestamp is its completion time, so long-running tools appear where they actually finished, with messages in between. Items sharing a timestamp order `message`, then `tool_call`, then `tool_result`. | ||
| - **`tool_source`**: where the tool ran, one of `client`, `webhook`, `builtin` (platform tools such as call transfer and hang-up), `mcp` (tools from a connected MCP server), `background` (work the agent delegated to a background task), or `unknown` (calls recorded before source attribution). Treat it as an open set. See [Tools](/agents/build/tools). | ||
| - **Payloads**: `input` and `output` are JSON strings, symmetric with the live SDK events, so one parser covers both. `output` and `error` are stored up to 256 KB; beyond that the text is cut and `output_truncated` is `true`. | ||
|
|
@@ -204,6 +205,95 @@ | |
| appear without a version bump. | ||
| </Warning> | ||
|
|
||
| ### 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. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: printf '%s\n' '--- PR diff ---'
git diff --unified=12 ec127aed383d2a8188b99fae297a9a240d20dc6d 7cf506f02cb6f28ddc67fa2ce001a2f9e21ea91e -- agents/monitor/conversation-history.mdx
printf '%s\n' '--- current relevant lines ---'
nl -ba agents/monitor/conversation-history.mdx | sed -n '230,315p'Repository: fishaudio/docs Length of output: 27686 🏁 Script executed: printf '%s\n' '--- transfer reference locations ---'
rg -n -F --glob '*.mdx' -e 'Transfer results' -e 'refer_failed' -e 'referred' -e 'duration of `0`' agents/telephony agents/monitor || test "$?" -eq 1
printf '%s\n' '--- linked transfer reference files ---'
rg --files agents/telephony | rg 'transfer'
printf '%s\n' '--- relevant transfer reference ---'
file=$(rg --files agents/telephony | rg '/transfers\.mdx$' | head -n 1)
if [ -n "$file" ]; then
nl -ba "$file" | sed -n '1,260p'
else
printf '%s\n' 'transfers.mdx not found'
fiRepository: fishaudio/docs Length of output: 26859 Align the no-pickup outcome with the transfer reference. The transfer reference defines 🤖 Prompt for AI Agents |
||
|
|
||
| ### 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). | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,7 @@ | |
| ## Prerequisites | ||
|
|
||
| - A phone number bound to your agent, answering inbound calls. See [Inbound calls](/agents/telephony/inbound-calls). | ||
| - The number must support transfers. Numbers purchased from the platform inventory (`provider: "twilio"`) do; see [Phone numbers](/agents/telephony/phone-numbers). [Imported SIP numbers](/agents/telephony/byo-sip) support cold transfers when the carrier honors SIP REFER to the destination (on Twilio, enable Call Transfer and allow PSTN transfers on the trunk), and warm transfers when a termination is configured. | ||
|
|
||
| ## Configure destinations | ||
|
|
||
|
|
@@ -359,10 +359,65 @@ | |
|
|
||
| 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. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Qualify when the console shows call details. This sentence says a connected transfer shows its duration and end reason. Line 415 says after-transfer details never appear for cold transfers on imported SIP numbers. It also says other cold-transfer details can arrive up to about an hour after the call ends. State these limits here so users do not expect details on every connected transfer. 🤖 Prompt for AI Agents |
||
| - **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`: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: set -u
printf '%s\n' '--- PR diff for transfers.mdx ---'
git diff --no-ext-diff --unified=30 cc9d3e0dc56f2e52c6350fd6183afa50d90ec51b ce788f308dd218d316abee8b1de9b6a75ef380cf -- agents/telephony/transfers.mdx
printf '%s\n' '--- AgentSessionTransferItem references ---'
rg -n -F --glob '*.mdx' --glob '*.md' --glob '*.json' --glob '*.yaml' --glob '*.yml' --glob '*.ts' --glob '*.tsx' 'AgentSessionTransferItem' . || test "$?" -eq 1
printf '%s\n' '--- transfer field references ---'
rg -n -F --glob '*.mdx' --glob '*.md' --glob '*.json' --glob '*.yaml' --glob '*.yml' --glob '*.ts' --glob '*.tsx' -e 'consult_end_reason' -e 'transferred_call_' -e 'dial_status' . || test "$?" -eq 1Repository: fishaudio/docs Length of output: 18096 🏁 Script executed: set -u
printf '%s\n' '--- AgentSessionTransferItem schema ---'
sed -n '19105,19315p' api-reference/openapi.json
printf '%s\n' '--- Conversation-history transfer item documentation ---'
sed -n '205,305p' agents/monitor/conversation-history.mdx
printf '%s\n' '--- OpenAPI schema diff base..head ---'
git diff --no-ext-diff --unified=8 cc9d3e0dc56f2e52c6350fd6183afa50d90ec51b ce788f308dd218d316abee8b1de9b6a75ef380cf -- api-reference/openapi.jsonRepository: fishaudio/docs Length of output: 16155 Align the published session schema with the documented transfer fields.
🤖 Prompt for AI Agents |
||
|
|
||
| | `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. | ||
| - **Supported countries**: destination numbers are limited to an allowlist of country codes. | ||
|
|
||
| ## Billing | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: fishaudio/docs
Length of output: 33514
🏁 Script executed:
Repository: fishaudio/docs
Length of output: 25680
Align the transfer-item schema before publishing this guarantee.
api-reference/openapi.jsoncurrently definesAgentSessionTransferItemwith only five required properties. It does not definedial_status,consult_end_reason, or anytransferred_call_*property. Its optional properties anddefault: nullvalues also do not guarantee that the fields are present in responses.If the API emits these fields, add them to the schema and mark the nullable fields as required. Otherwise, remove the unsupported fields and the “every field is always present” guarantee from this documentation.
🤖 Prompt for AI Agents