Skip to content

feat(workflows): introduce structured WarmTransferError with SIP facts (#7200) - #7253

Open
rrfunde wants to merge 6 commits into
livekit:mainfrom
rrfunde:fix/warm-transfer-structured-error
Open

feat(workflows): introduce structured WarmTransferError with SIP facts (#7200)#7253
rrfunde wants to merge 6 commits into
livekit:mainfrom
rrfunde:fix/warm-transfer-structured-error

Conversation

@rrfunde

@rrfunde rrfunde commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #7200

Summary

Introduces a typed WarmTransferError(ToolError) subclass and WarmTransferFailure enum to preserve platform facts (disconnect reason, SIP call status, dial failure causes) when a warm transfer fails.

Changes

  • Defined WarmTransferFailure enum and WarmTransferError(ToolError) carrying code, disconnect_reason, call_status, and reason.
  • Subscribed to participant_disconnected on the human-agent room to record participant.disconnect_reason and sip.callStatus.
  • Distinguish between DESTINATION_LEFT and ROOM_CLOSED on human-agent room close.
  • Chained underlying dial exceptions (SipCallError) as __cause__ on DIAL_FAILED.
  • Properly detach and re-attach listeners across participant moves in _merge_calls.
  • Added unit tests in tests/test_warm_transfer_error.py.

livekit#7200)

Expose WarmTransferError and WarmTransferFailure carrying disconnect_reason, call_status, and cause on warm transfer failures.
@rrfunde
rrfunde requested a review from a team as a code owner September 12, 2026 11:38

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 2 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment thread livekit-agents/livekit/agents/beta/workflows/warm_transfer.py Outdated
logger.exception("could not dial human agent")
self._set_result(ToolError("could not dial human agent"))
err = WarmTransferError("could not dial human agent", code=WarmTransferFailure.DIAL_FAILED)
err.__cause__ = e

@devin-ai-integration devin-ai-integration Bot Sep 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟨 Dial errors retain sensitive context

When dialing fails, WarmTransferError retains the caught exception through implicit __context__. Downstream traceback rendering can expose provider payloads or credentials.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f870f9806e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +210 to +211
self._destination_disconnect_reason: rtc.DisconnectReason.ValueType | None = None
self._destination_call_status: str | None = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Initialize failure state outside the overridable SIP hook

When TwilioConnectorWarmTransferTask is used, its no-op _setup_origination() override prevents these fields from being initialized. If the staging room later closes without first emitting participant_disconnected—for example after a server shutdown or connection failure—_on_human_agent_room_close() raises AttributeError while reading _destination_disconnect_reason and never calls _set_result, potentially leaving the caller on hold indefinitely. Initialize this shared failure state directly in the base constructor rather than in the SIP-specific hook.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 12, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-13T04:21:16.710434Z 0d1adb6 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@rrfunde

rrfunde commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback:

  • Initialized destination failure tracking state (_destination_disconnect_reason, _destination_call_status, _human_agent_participant_disconnected_cb) in WarmTransferTask.__init__ rather than _setup_origination(), ensuring subclasses like TwilioConnectorWarmTransferTask have these attributes initialized even when _setup_origination() is overridden.
  • Removed err.__cause__ = e in on_enter dial failure handling to avoid exposing raw provider details/credentials in tracebacks and telemetry.
  • Added a unit test in tests/test_warm_transfer_error.py verifying TwilioConnectorWarmTransferTask initialization.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 026b35534a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tests/test_warm_transfer_error.py Outdated
task._set_io_enabled = MagicMock()
task.done = MagicMock(return_value=False)
task.complete = MagicMock()
task._human_agent_failed_fut = asyncio.get_event_loop().create_future()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Create the futures inside a running event loop

On Python 3.14, which this package explicitly supports via requires-python = ">=3.10,<3.15", these synchronous tests call asyncio.get_event_loop() when no current loop exists and raise RuntimeError before exercising the callback; the same issue occurs on line 99. Mark these tests async or create the futures within an explicitly managed event loop so the unit suite remains runnable on supported Python versions.

AGENTS.md reference: AGENTS.md:L128-L130

Useful? React with 👍 / 👎.

@rrfunde

rrfunde commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Fixed: Marked the warm transfer unit tests as async so create_future() runs within an active event loop, preventing RuntimeError on Python 3.14.

@rrfunde

rrfunde commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Addressed review feedback: used asyncio.get_running_loop().create_future() inside the async test functions so futures are created within the active event loop without triggering get_event_loop() errors on Python 3.14.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6bbada48a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +388 to +390
if participant.identity == self._human_agent_identity:
self._destination_disconnect_reason = participant.disconnect_reason
self._destination_call_status = participant.attributes.get("sip.callStatus")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Complete the transfer when the destination disconnects

When the SIP destination leaves with USER_UNAVAILABLE (the reason exercised by the new test) or another reason outside the default close list, this callback only caches the departure. RoomIO._on_participant_disconnected closes the staging session only for CLIENT_INITIATED, ROOM_DELETED, and USER_REJECTED (voice/room_io/types.py:17-21), so no room disconnected event follows, _on_human_agent_room_close never calls _set_result, and the caller can remain on hold indefinitely. Complete the task from this participant callback or explicitly close the staging session for every destination departure.

Useful? React with 👍 / 👎.

@rrfunde

rrfunde commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Addressed review feedback:

  • Completed the task directly inside _on_human_agent_participant_disconnected with WarmTransferFailure.DESTINATION_LEFT when the human agent destination disconnects, ensuring the task resolves and resumes the caller conversation even for disconnect reasons outside the default room close reasons (such as USER_UNAVAILABLE).
  • Handled BaseException during destination origination to guarantee staging session shutdown upon task cancellation.
  • Added unit tests for destination participant disconnect completing the transfer task.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment on lines +332 to +345
with contextlib.suppress(asyncio.InvalidStateError):
self._human_agent_failed_fut.set_result(None)
reason_name = (
rtc.DisconnectReason.Name(participant.disconnect_reason)
if participant.disconnect_reason is not None
else "UNKNOWN_REASON"
)
self._set_result(
WarmTransferError(
f"destination left: {reason_name}",
code=WarmTransferFailure.DESTINATION_LEFT,
disconnect_reason=participant.disconnect_reason,
call_status=self._destination_call_status,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Failed transfer leaks child session

When disconnection and origination finish together, _set_result completes the transfer before on_enter stores the returned session. The human-agent session then remains active after the failed transfer.

Learn more

The disconnect callback can run while _originate_human_agent is finishing. It completes the task while _human_agent_sess is still None, so _set_result has no session to shut down. If the dial task also becomes done before asyncio.wait resumes, on_enter treats the dial as successful and assigns its returned session after completion. Nothing subsequently shuts that session down.

Example: The SIP destination answers and immediately hangs up. The disconnect event completes the transfer with DESTINATION_LEFT, while the create-participant request returns in the same event-loop turn. The failed transfer resumes the caller, but its human-agent session and room remain active.

Recommended fix: In on_enter, treat a completed _human_agent_failed_fut or self.done() as failure even when dial_human_agent_task is also done. If the dial task already returned an AgentSession, shut it down instead of assigning it.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0d1adb6770

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +255 to +258
err = WarmTransferError(
"could not dial human agent", code=WarmTransferFailure.DIAL_FAILED
)
self._set_result(err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Chain the original dial exception

When _originate_human_agent() fails, this creates a new WarmTransferError but discards the caught API/Twilio exception. Passing the wrapper to Future.set_exception() does not establish exception chaining, so callers receive DIAL_FAILED with __cause__ is None and cannot inspect the underlying SIP status or failure details. Capture the exception and explicitly attach it as the cause before completing the task.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WarmTransferTask: report a typed failure reason with the underlying SIP evidence preserved

1 participant