Skip to content

Commit 5e35317

Browse files
cristipufuclaude
andcommitted
feat: add WebDebugBridge for auto-polling resume triggers in web server
Introduce WebDebugBridge that wraps RUN-mode runs with UiPathDebugRuntime, enabling auto-polling for non-API resume triggers (Queue/Timer) instead of leaving suspended runs stuck. RunService now accepts a debug_bridge_factory so both the web server and Textual app can inject their own bridge. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6e0f8fd commit 5e35317

5 files changed

Lines changed: 149 additions & 7 deletions

File tree

src/uipath/dev/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from uipath.dev.models.chat import get_user_message, get_user_message_event
2626
from uipath.dev.models.data import ChatData, LogData, TraceData
2727
from uipath.dev.services import RunService
28+
from uipath.dev.services.debug_bridge import TextualDebugBridge
2829
from uipath.dev.ui.panels import NewRunPanel, RunDetailsPanel, RunHistoryPanel
2930

3031

@@ -71,6 +72,7 @@ def __init__(
7172
on_log=self._on_log_for_ui,
7273
on_trace=self._on_trace_for_ui,
7374
on_chat=self._on_chat_for_ui,
75+
debug_bridge_factory=lambda mode: TextualDebugBridge(),
7476
)
7577

7678
# Just defaults for convenience

src/uipath/dev/server/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from uipath.dev.models.data import ChatData, LogData, TraceData
1717
from uipath.dev.models.execution import ExecutionRun
18+
from uipath.dev.server.debug_bridge import WebDebugBridge
1819
from uipath.dev.services.run_service import RunService
1920

2021
logger = logging.getLogger(__name__)
@@ -75,6 +76,7 @@ def __init__(
7576
on_log=self._on_log,
7677
on_trace=self._on_trace,
7778
on_chat=self._on_chat,
79+
debug_bridge_factory=lambda mode: WebDebugBridge(mode=mode),
7880
)
7981

8082
def create_app(self) -> Any:
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""Debug bridge implementation for the web server."""
2+
3+
import asyncio
4+
import logging
5+
from typing import Any, Callable, Literal
6+
7+
from uipath.runtime.debug import UiPathBreakpointResult, UiPathDebugQuitError
8+
from uipath.runtime.events import UiPathRuntimeStateEvent
9+
from uipath.runtime.result import UiPathRuntimeResult
10+
from uipath.runtime.resumable import UiPathResumeTriggerType
11+
12+
from uipath.dev.models.execution import ExecutionMode
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
class WebDebugBridge:
18+
"""Bridge between the web server and UiPathDebugRuntime.
19+
20+
In RUN mode: no breakpoints, auto-resumes the initial debug pause.
21+
In DEBUG mode: same step-mode behavior as TextualDebugBridge.
22+
"""
23+
24+
def __init__(self, mode: ExecutionMode = ExecutionMode.RUN):
25+
self._mode = mode
26+
self._auto_resume = mode == ExecutionMode.RUN
27+
self._resume_event = asyncio.Event()
28+
self._resume_data: dict[str, Any] | None = None
29+
self._terminate_event = asyncio.Event()
30+
self._breakpoints: list[str] | Literal["*"] = (
31+
[] if mode == ExecutionMode.RUN else "*"
32+
)
33+
34+
# Callbacks (wired by RunService)
35+
self.on_execution_started: Callable[[], None] | None = None
36+
self.on_state_update: Callable[[UiPathRuntimeStateEvent], None] | None = None
37+
self.on_breakpoint_hit: Callable[[UiPathBreakpointResult], None] | None = None
38+
self.on_execution_completed: Callable[[UiPathRuntimeResult], None] | None = None
39+
self.on_execution_error: Callable[[str], None] | None = None
40+
41+
# ------------------------------------------------------------------
42+
# UiPathDebugProtocol implementation
43+
# ------------------------------------------------------------------
44+
45+
async def connect(self) -> None:
46+
logger.debug("WebDebugBridge connected (mode=%s)", self._mode)
47+
48+
async def disconnect(self) -> None:
49+
self._resume_event.set()
50+
self._terminate_event.set()
51+
logger.debug("WebDebugBridge disconnected")
52+
53+
async def emit_execution_started(self, **kwargs: Any) -> None:
54+
logger.debug("Execution started")
55+
if self.on_execution_started:
56+
self.on_execution_started()
57+
58+
async def emit_state_update(self, state_event: UiPathRuntimeStateEvent) -> None:
59+
logger.debug("State update: %s", state_event.node_name)
60+
if self.on_state_update:
61+
self.on_state_update(state_event)
62+
63+
async def emit_breakpoint_hit(
64+
self, breakpoint_result: UiPathBreakpointResult
65+
) -> None:
66+
logger.debug("Breakpoint hit: %s", breakpoint_result)
67+
if self.on_breakpoint_hit:
68+
self.on_breakpoint_hit(breakpoint_result)
69+
70+
async def emit_execution_suspended(
71+
self, runtime_result: UiPathRuntimeResult
72+
) -> None:
73+
logger.debug("Execution suspended")
74+
if runtime_result.trigger is None:
75+
return
76+
77+
if runtime_result.trigger.trigger_type == UiPathResumeTriggerType.API:
78+
if self.on_breakpoint_hit:
79+
self.on_breakpoint_hit(
80+
UiPathBreakpointResult(
81+
breakpoint_node="<suspended>",
82+
breakpoint_type="before",
83+
current_state=runtime_result.output,
84+
next_nodes=[],
85+
)
86+
)
87+
88+
async def emit_execution_resumed(self, resume_data: Any) -> None:
89+
logger.debug("Execution resumed")
90+
91+
async def emit_execution_completed(
92+
self, runtime_result: UiPathRuntimeResult
93+
) -> None:
94+
logger.debug("Execution completed")
95+
if self.on_execution_completed:
96+
self.on_execution_completed(runtime_result)
97+
98+
async def emit_execution_error(self, error: str) -> None:
99+
logger.error("Execution error: %s", error)
100+
if self.on_execution_error:
101+
self.on_execution_error(error)
102+
103+
async def wait_for_resume(self) -> Any:
104+
if self._auto_resume:
105+
self._auto_resume = False # Only auto-resume the first (initial) pause
106+
return {}
107+
108+
self._resume_event.clear()
109+
await self._resume_event.wait()
110+
111+
if self._terminate_event.is_set():
112+
raise UiPathDebugQuitError("Debug session quit requested")
113+
114+
return self._resume_data
115+
116+
async def wait_for_terminate(self) -> None:
117+
await self._terminate_event.wait()
118+
119+
def get_breakpoints(self) -> list[str] | Literal["*"]:
120+
return self._breakpoints
121+
122+
# ------------------------------------------------------------------
123+
# Control methods (called from RunService / WS handler)
124+
# ------------------------------------------------------------------
125+
126+
def resume(self, resume_data: Any) -> None:
127+
self._resume_data = resume_data or {}
128+
self._resume_event.set()
129+
130+
def quit(self) -> None:
131+
self._terminate_event.set()
132+
self._resume_event.set()
133+
134+
def set_breakpoints(self, breakpoints: list[str] | Literal["*"]) -> None:
135+
self._breakpoints = breakpoints

src/uipath/dev/server/ws/handler.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ async def _handle_chat_message(server: Any, run_id: str, text: str) -> None:
9494
except json.JSONDecodeError:
9595
run.resume_data = text
9696

97-
if run.mode == ExecutionMode.DEBUG:
97+
debug_bridge = server.run_service.get_debug_bridge(run.id)
98+
if debug_bridge:
9899
asyncio.create_task(server.run_service.resume_debug(run, run.resume_data))
99100
else:
100101
asyncio.create_task(server.run_service.execute(run))

src/uipath/dev/services/run_service.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,19 @@
1818
UiPathRuntimeStatus,
1919
UiPathStreamOptions,
2020
)
21-
from uipath.runtime.debug import UiPathDebugRuntime
21+
from uipath.runtime.debug import UiPathDebugProtocol, UiPathDebugRuntime
2222
from uipath.runtime.errors import UiPathErrorContract, UiPathRuntimeError
2323
from uipath.runtime.events import UiPathRuntimeMessageEvent, UiPathRuntimeStateEvent
2424

2525
from uipath.dev.infrastructure import RunContextExporter, RunContextLogHandler
2626
from uipath.dev.models.data import ChatData, LogData, TraceData
2727
from uipath.dev.models.execution import ExecutionMode, ExecutionRun
28-
from uipath.dev.services.debug_bridge import TextualDebugBridge
2928

3029
RunUpdatedCallback = Callable[[ExecutionRun], None]
3130
LogCallback = Callable[[LogData], None]
3231
TraceCallback = Callable[[TraceData], None]
3332
ChatCallback = Callable[[ChatData], None]
33+
DebugBridgeFactory = Callable[[ExecutionMode], UiPathDebugProtocol]
3434

3535

3636
class RunService:
@@ -50,6 +50,7 @@ def __init__(
5050
on_log: LogCallback | None = None,
5151
on_trace: TraceCallback | None = None,
5252
on_chat: ChatCallback | None = None,
53+
debug_bridge_factory: DebugBridgeFactory | None = None,
5354
) -> None:
5455
"""Initialize RunService with runtime factory and trace manager."""
5556
self.runtime_factory = runtime_factory
@@ -60,6 +61,7 @@ def __init__(
6061
self.on_log = on_log
6162
self.on_trace = on_trace
6263
self.on_chat = on_chat
64+
self._debug_bridge_factory = debug_bridge_factory
6365

6466
self.trace_manager.add_span_exporter(
6567
RunContextExporter(
@@ -69,7 +71,7 @@ def __init__(
6971
batch=False,
7072
)
7173

72-
self.debug_bridges: dict[str, TextualDebugBridge] = {}
74+
self.debug_bridges: dict[str, UiPathDebugProtocol] = {}
7375

7476
def register_run(self, run: ExecutionRun) -> None:
7577
"""Register a new run and emit an initial update."""
@@ -111,8 +113,8 @@ async def execute(self, run: ExecutionRun) -> None:
111113

112114
runtime: UiPathRuntimeProtocol
113115

114-
if run.mode == ExecutionMode.DEBUG:
115-
debug_bridge = TextualDebugBridge()
116+
if run.mode in (ExecutionMode.DEBUG, ExecutionMode.RUN) and self._debug_bridge_factory:
117+
debug_bridge = self._debug_bridge_factory(run.mode)
116118

117119
debug_bridge.on_state_update = lambda state: self._handle_state_update(
118120
run.id, state
@@ -274,7 +276,7 @@ def handle_trace(self, trace_data: TraceData) -> None:
274276
if self.on_trace is not None:
275277
self.on_trace(trace_data)
276278

277-
def get_debug_bridge(self, run_id: str) -> TextualDebugBridge | None:
279+
def get_debug_bridge(self, run_id: str) -> UiPathDebugProtocol | None:
278280
"""Get the debug bridge for a run."""
279281
return self.debug_bridges.get(run_id)
280282

0 commit comments

Comments
 (0)