From a77abbb46db30e9d5d957137ec0a1150362a54f0 Mon Sep 17 00:00:00 2001 From: Josh Park <50765702+JoshParkSJ@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:08:24 -0400 Subject: [PATCH 1/3] fix(chat): keep the exchange open while paused at a debug breakpoint Under `uipath debug` the chat runtime sits inside the debug runtime, so it sees each UiPathBreakpointResult first. A breakpoint result is SUSPENDED with no triggers, which fell through to the completion branch and sent endExchange to CAS. In step mode that closed the turn at the first pause, before the agent had replied, so the reply never showed in the chat. Pass breakpoint results up to the debugger without ending the exchange. JAR-10180 Co-Authored-By: Claude Opus 5.5 --- pyproject.toml | 2 +- src/uipath/runtime/chat/runtime.py | 8 ++- tests/test_chat.py | 98 ++++++++++++++++++++++++++++++ uv.lock | 2 +- 4 files changed, 107 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ebe7206..a6db49a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-runtime" -version = "0.13.5" +version = "0.13.6" description = "Runtime abstractions and interfaces for building agents and automation scripts in the UiPath ecosystem" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/src/uipath/runtime/chat/runtime.py b/src/uipath/runtime/chat/runtime.py index 9eb31a7..2f6d743 100644 --- a/src/uipath/runtime/chat/runtime.py +++ b/src/uipath/runtime/chat/runtime.py @@ -13,6 +13,7 @@ UiPathStreamOptions, ) from uipath.runtime.chat.protocol import UiPathChatMetaEventProtocol, UiPathChatProtocol +from uipath.runtime.debug import UiPathBreakpointResult from uipath.runtime.errors import UiPathBaseRuntimeError from uipath.runtime.errors.contract import UiPathErrorCategory from uipath.runtime.events import ( @@ -98,7 +99,12 @@ async def stream( if isinstance(event, UiPathRuntimeResult): runtime_result = event - if ( + if isinstance(runtime_result, UiPathBreakpointResult): + # Paused by the debugger, which resumes this runtime + # later: the exchange is still in progress. + yield event + execution_completed = True + elif ( runtime_result.status == UiPathRuntimeStatus.SUSPENDED and runtime_result.triggers ): diff --git a/tests/test_chat.py b/tests/test_chat.py index f17bad5..f453682 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -17,6 +17,7 @@ ) from uipath.runtime import ( + UiPathBreakpointResult, UiPathExecuteOptions, UiPathRuntimeResult, UiPathRuntimeStatus, @@ -28,6 +29,7 @@ UiPathChatRuntime, ) from uipath.runtime.chat.runtime import _parse_confirmation +from uipath.runtime.debug import UiPathDebugProtocol, UiPathDebugRuntime from uipath.runtime.events import ( UiPathRuntimeConversationMetaEvent, UiPathRuntimeEvent, @@ -225,6 +227,7 @@ async def test_chat_runtime_streams_and_emits_messages(): # Bridge lifecycle cast(AsyncMock, bridge.connect).assert_awaited_once() cast(AsyncMock, bridge.disconnect).assert_awaited_once() + cast(AsyncMock, bridge.emit_exchange_end_event).assert_awaited_once() assert cast(AsyncMock, bridge.emit_message_event).await_count == 3 @@ -437,6 +440,101 @@ async def test_chat_runtime_yields_events_during_suspension_flow(): assert event.status != UiPathRuntimeStatus.SUSPENDED +class BreakpointMockRuntime: + """Mock runtime that pauses at a debug breakpoint before replying.""" + + async def dispose(self) -> None: + pass + + async def execute( + self, + input: dict[str, Any] | None = None, + options: UiPathExecuteOptions | None = None, + ) -> UiPathRuntimeResult: + raise NotImplementedError() + + async def stream( + self, + input: dict[str, Any] | None = None, + options: UiPathStreamOptions | None = None, + ) -> AsyncGenerator[UiPathRuntimeEvent, None]: + if not (options and options.resume): + yield UiPathBreakpointResult( + breakpoint_node="agent", + breakpoint_type="before", + current_state={}, + next_nodes=["agent"], + ) + return + + yield UiPathRuntimeMessageEvent( + payload=UiPathConversationMessageEvent( + message_id="msg-0", + start=UiPathConversationMessageStartEvent( + role="assistant", + timestamp="2025-01-01T00:00:00.000Z", + ), + ) + ) + yield UiPathRuntimeResult(status=UiPathRuntimeStatus.SUCCESSFUL) + + async def get_schema(self) -> UiPathRuntimeSchema: + raise NotImplementedError() + + +@pytest.mark.asyncio +async def test_chat_runtime_keeps_exchange_open_at_breakpoint(): + """A debug breakpoint pauses the exchange, so it must not end it.""" + bridge = make_chat_bridge_mock() + chat_runtime = UiPathChatRuntime( + delegate=BreakpointMockRuntime(), chat_bridge=bridge + ) + + events = [event async for event in chat_runtime.stream({})] + + assert len(events) == 1 + assert isinstance(events[0], UiPathBreakpointResult) + cast(AsyncMock, bridge.emit_exchange_end_event).assert_not_awaited() + + +@pytest.mark.asyncio +async def test_chat_runtime_under_debugger_ends_exchange_after_reply(): + """Stepping past a breakpoint ends the exchange once, after the reply.""" + chat_bridge = make_chat_bridge_mock() + debug_bridge: Mock = Mock(spec=UiPathDebugProtocol) + debug_bridge.connect = AsyncMock() + debug_bridge.emit_execution_started = AsyncMock() + debug_bridge.emit_execution_completed = AsyncMock() + debug_bridge.emit_breakpoint_hit = AsyncMock() + debug_bridge.wait_for_resume = AsyncMock() + debug_bridge.get_breakpoints = Mock(return_value="*") + + calls = Mock() + calls.attach_mock( + cast(AsyncMock, chat_bridge.emit_message_event), "emit_message_event" + ) + calls.attach_mock( + cast(AsyncMock, chat_bridge.emit_exchange_end_event), "emit_exchange_end_event" + ) + + # Same nesting as `uipath debug` for a conversational job. + debug_runtime = UiPathDebugRuntime( + delegate=UiPathChatRuntime( + delegate=BreakpointMockRuntime(), chat_bridge=chat_bridge + ), + debug_bridge=cast(UiPathDebugProtocol, debug_bridge), + ) + + result = await debug_runtime.execute({}) + + assert result.status == UiPathRuntimeStatus.SUCCESSFUL + debug_bridge.emit_breakpoint_hit.assert_awaited_once() + assert [name for name, _, _ in calls.mock_calls] == [ + "emit_message_event", + "emit_exchange_end_event", + ] + + class MultiTriggerMockRuntime: """Mock runtime that suspends with multiple API triggers.""" diff --git a/uv.lock b/uv.lock index 7106406..f4a278a 100644 --- a/uv.lock +++ b/uv.lock @@ -1153,7 +1153,7 @@ wheels = [ [[package]] name = "uipath-runtime" -version = "0.13.5" +version = "0.13.6" source = { editable = "." } dependencies = [ { name = "chardet" }, From b11dea0ea607f90987ed3b760cd3eb67b962ac9e Mon Sep 17 00:00:00 2001 From: Josh Park <50765702+JoshParkSJ@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:11:18 -0400 Subject: [PATCH 2/3] chore(chat): reword the breakpoint comment Co-Authored-By: Claude Opus 5.5 --- src/uipath/runtime/chat/runtime.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uipath/runtime/chat/runtime.py b/src/uipath/runtime/chat/runtime.py index 2f6d743..4625576 100644 --- a/src/uipath/runtime/chat/runtime.py +++ b/src/uipath/runtime/chat/runtime.py @@ -100,8 +100,8 @@ async def stream( runtime_result = event if isinstance(runtime_result, UiPathBreakpointResult): - # Paused by the debugger, which resumes this runtime - # later: the exchange is still in progress. + # A breakpoint pauses the run. The debugger will resume it, + # so keep the exchange open. yield event execution_completed = True elif ( From 4f3622032af7be4820a939f34141e01da68b949c Mon Sep 17 00:00:00 2001 From: Josh Park <50765702+JoshParkSJ@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:17:02 -0400 Subject: [PATCH 3/3] chore(chat): say why the breakpoint branch keeps the exchange open Co-Authored-By: Claude Opus 5.5 --- src/uipath/runtime/chat/runtime.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/uipath/runtime/chat/runtime.py b/src/uipath/runtime/chat/runtime.py index 4625576..8622acb 100644 --- a/src/uipath/runtime/chat/runtime.py +++ b/src/uipath/runtime/chat/runtime.py @@ -100,8 +100,9 @@ async def stream( runtime_result = event if isinstance(runtime_result, UiPathBreakpointResult): - # A breakpoint pauses the run. The debugger will resume it, - # so keep the exchange open. + # A breakpoint only pauses the run. Ending the exchange + # here would close the turn in CAS before the agent has + # finished replying. yield event execution_completed = True elif (