From 6f4fac228ca50a847804f964fe654fe0b247cd96 Mon Sep 17 00:00:00 2001 From: Josh Park <50765702+JoshParkSJ@users.noreply.github.com> Date: Tue, 30 Jun 2026 09:55:40 -0400 Subject: [PATCH] fix: preserve voice session end detail --- .../src/uipath/_cli/_chat/_voice_bridge.py | 18 +++++++++++-- .../tests/cli/chat/test_voice_bridge.py | 26 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/uipath/src/uipath/_cli/_chat/_voice_bridge.py b/packages/uipath/src/uipath/_cli/_chat/_voice_bridge.py index b575404b3..0b0ab7ffb 100644 --- a/packages/uipath/src/uipath/_cli/_chat/_voice_bridge.py +++ b/packages/uipath/src/uipath/_cli/_chat/_voice_bridge.py @@ -82,6 +82,12 @@ def __init__( self._done = asyncio.Event() self._in_flight: set[asyncio.Task[None]] = set() self._end_reason: VoiceSessionEndReason | None = None + self._end_detail: dict[str, Any] = {} + + @property + def end_detail(self) -> dict[str, Any]: + """CAS payload from voice_session_ended, preserved for the job runtime.""" + return self._end_detail async def run(self) -> VoiceSessionEndReason: """Connect, dispatch tool calls until session ends, then disconnect. @@ -214,8 +220,16 @@ async def _execute_tool_call(self, call: UiPathVoiceToolCallRequest) -> None: tool_result.is_error, ) - async def _handle_session_ended(self, _data: Any, *_: Any) -> None: - logger.info("[Voice] voice_session_ended received") + async def _handle_session_ended(self, data: Any = None, *_: Any) -> None: + detail = data if isinstance(data, dict) else {} + self._end_detail = detail + logger.info( + "[Voice] voice_session_ended received " + "(endedBy=%s, callEnded=%s, reason=%s)", + detail.get("endedBy"), + detail.get("callEnded"), + detail.get("reason"), + ) self._end_session(VoiceSessionEndReason.COMPLETED) diff --git a/packages/uipath/tests/cli/chat/test_voice_bridge.py b/packages/uipath/tests/cli/chat/test_voice_bridge.py index a9c8c1baf..21b0248cc 100644 --- a/packages/uipath/tests/cli/chat/test_voice_bridge.py +++ b/packages/uipath/tests/cli/chat/test_voice_bridge.py @@ -46,6 +46,32 @@ async def test_session_ended_sets_completed(self) -> None: await session._handle_session_ended(None) assert session._end_reason == VoiceSessionEndReason.COMPLETED + async def test_session_ended_preserves_payload_opaquely(self) -> None: + session = _make_session() + call_context = {"type": "phone", "id": "CA123", "conversationId": "conv-1"} + payload = { + "callContext": call_context, + "conversationId": "conv-1", + "endedBy": "agent", + "callEnded": False, + "reason": "agent_completed", + "someFutureKey": {"nested": True}, + } + + await session._handle_session_ended(payload) + + assert session.end_detail == payload + assert session.end_detail["callContext"] == call_context + assert session.end_detail["endedBy"] == "agent" + assert session.end_detail["callEnded"] is False + assert session._end_reason == VoiceSessionEndReason.COMPLETED + + async def test_session_ended_non_dict_payload_is_empty_detail(self) -> None: + session = _make_session() + await session._handle_session_ended("not-a-dict") + assert session.end_detail == {} + assert session._end_reason == VoiceSessionEndReason.COMPLETED + async def test_disconnect_sets_disconnected(self) -> None: session = _make_session() await session._handle_disconnect()