Skip to content

Commit 61f7a2f

Browse files
committed
feat: add debug runtime
1 parent 72abea4 commit 61f7a2f

File tree

7 files changed

+619
-4
lines changed

7 files changed

+619
-4
lines changed

src/uipath/runtime/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""UiPath Runtime Package."""
22

3-
from uipath.runtime.base import UiPathBaseRuntime
3+
from uipath.runtime.base import UiPathBaseRuntime, UiPathStreamNotSupportedError
44
from uipath.runtime.context import UiPathRuntimeContext
55
from uipath.runtime.events import UiPathRuntimeEvent
66
from uipath.runtime.factory import UiPathRuntimeExecutor, UiPathRuntimeFactory
@@ -10,6 +10,7 @@
1010
UiPathResumeTrigger,
1111
UiPathResumeTriggerType,
1212
UiPathRuntimeResult,
13+
UiPathRuntimeStatus,
1314
)
1415

1516
__all__ = [
@@ -18,9 +19,11 @@
1819
"UiPathRuntimeFactory",
1920
"UiPathRuntimeExecutor",
2021
"UiPathRuntimeResult",
22+
"UiPathRuntimeStatus",
2123
"UiPathRuntimeEvent",
2224
"UiPathBreakpointResult",
2325
"UiPathApiTrigger",
2426
"UiPathResumeTrigger",
2527
"UiPathResumeTriggerType",
28+
"UiPathStreamNotSupportedError",
2629
]

src/uipath/runtime/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
logger = logging.getLogger(__name__)
2626

2727

28-
class UiPathRuntimeStreamNotSupportedError(NotImplementedError):
28+
class UiPathStreamNotSupportedError(NotImplementedError):
2929
"""Raised when a runtime does not support streaming."""
3030

3131
pass
@@ -130,7 +130,7 @@ async def stream(
130130
Final yield: UiPathRuntimeResult (or its subclass UiPathBreakpointResult)
131131
132132
Raises:
133-
UiPathRuntimeStreamNotSupportedError: If the runtime doesn't support streaming
133+
UiPathStreamNotSupportedError: If the runtime doesn't support streaming
134134
RuntimeError: If execution fails
135135
136136
Example:
@@ -146,7 +146,7 @@ async def stream(
146146
# Handle state update
147147
print(f"State updated by: {event.node_name}")
148148
"""
149-
raise UiPathRuntimeStreamNotSupportedError(
149+
raise UiPathStreamNotSupportedError(
150150
f"{self.__class__.__name__} does not implement streaming. "
151151
"Use execute() instead."
152152
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Initialization module for the debug package."""
2+
3+
from uipath.runtime.debug.bridge import UiPathDebugBridge
4+
from uipath.runtime.debug.exception import (
5+
UiPathDebugQuitError,
6+
)
7+
from uipath.runtime.debug.runtime import UiPathDebugRuntime
8+
9+
__all__ = [
10+
"UiPathDebugQuitError",
11+
"UiPathDebugBridge",
12+
"UiPathDebugRuntime",
13+
]

src/uipath/runtime/debug/bridge.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Abstract debug bridge interface."""
2+
3+
from abc import ABC, abstractmethod
4+
from typing import Any, List, Literal
5+
6+
from uipath.runtime import (
7+
UiPathBreakpointResult,
8+
UiPathRuntimeResult,
9+
)
10+
from uipath.runtime.events import UiPathRuntimeStateEvent
11+
12+
13+
class UiPathDebugBridge(ABC):
14+
"""Abstract interface for debug communication.
15+
16+
Implementations: SignalR, Console, WebSocket, etc.
17+
"""
18+
19+
@abstractmethod
20+
async def connect(self) -> None:
21+
"""Establish connection to debugger."""
22+
pass
23+
24+
@abstractmethod
25+
async def disconnect(self) -> None:
26+
"""Close connection to debugger."""
27+
pass
28+
29+
@abstractmethod
30+
async def emit_execution_started(self, **kwargs) -> None:
31+
"""Notify debugger that execution started."""
32+
pass
33+
34+
@abstractmethod
35+
async def emit_state_update(self, state_event: UiPathRuntimeStateEvent) -> None:
36+
"""Notify debugger of runtime state update."""
37+
pass
38+
39+
@abstractmethod
40+
async def emit_breakpoint_hit(
41+
self, breakpoint_result: UiPathBreakpointResult
42+
) -> None:
43+
"""Notify debugger that a breakpoint was hit."""
44+
pass
45+
46+
@abstractmethod
47+
async def emit_execution_completed(
48+
self,
49+
runtime_result: UiPathRuntimeResult,
50+
) -> None:
51+
"""Notify debugger that execution completed."""
52+
pass
53+
54+
@abstractmethod
55+
async def emit_execution_error(
56+
self,
57+
error: str,
58+
) -> None:
59+
"""Notify debugger that an error occurred."""
60+
pass
61+
62+
@abstractmethod
63+
async def wait_for_resume(self) -> Any:
64+
"""Wait for resume command from debugger."""
65+
pass
66+
67+
@abstractmethod
68+
def get_breakpoints(self) -> List[str] | Literal["*"]:
69+
"""Get nodes to suspend execution at.
70+
71+
Returns:
72+
List of node names to suspend at, or ["*"] for all nodes (step mode)
73+
"""
74+
pass
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""Debug exception definitions."""
2+
3+
4+
class UiPathDebugQuitError(Exception):
5+
"""Raised when user quits the debugger."""
6+
7+
pass
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""Debug runtime implementation."""
2+
3+
import logging
4+
from typing import Generic, Optional, TypeVar
5+
6+
from uipath.runtime import (
7+
UiPathBaseRuntime,
8+
UiPathBreakpointResult,
9+
UiPathRuntimeContext,
10+
UiPathRuntimeFactory,
11+
UiPathRuntimeResult,
12+
UiPathRuntimeStatus,
13+
UiPathStreamNotSupportedError,
14+
)
15+
from uipath.runtime.debug import UiPathDebugBridge, UiPathDebugQuitError
16+
from uipath.runtime.events import (
17+
UiPathRuntimeStateEvent,
18+
)
19+
20+
logger = logging.getLogger(__name__)
21+
22+
T = TypeVar("T", bound=UiPathBaseRuntime)
23+
C = TypeVar("C", bound=UiPathRuntimeContext)
24+
25+
26+
class UiPathDebugRuntime(UiPathBaseRuntime, Generic[T]):
27+
"""Specialized runtime for debug runs that streams events to a debug bridge."""
28+
29+
def __init__(
30+
self,
31+
context: UiPathRuntimeContext,
32+
factory: UiPathRuntimeFactory[T],
33+
debug_bridge: UiPathDebugBridge,
34+
):
35+
"""Initialize the UiPathDebugRuntime."""
36+
super().__init__(context)
37+
self.context: UiPathRuntimeContext = context
38+
self.factory: UiPathRuntimeFactory[T] = factory
39+
self.debug_bridge: UiPathDebugBridge = debug_bridge
40+
self._inner_runtime: Optional[T] = None
41+
42+
async def execute(self) -> Optional[UiPathRuntimeResult]:
43+
"""Execute the workflow with debug support."""
44+
try:
45+
await self.debug_bridge.connect()
46+
47+
self._inner_runtime = self.factory.new_runtime()
48+
49+
if not self._inner_runtime:
50+
raise RuntimeError("Failed to create inner runtime")
51+
52+
await self.debug_bridge.emit_execution_started()
53+
54+
# Try to stream events from inner runtime
55+
try:
56+
self.context.result = await self._stream_and_debug()
57+
except UiPathStreamNotSupportedError:
58+
# Fallback to regular execute if streaming not supported
59+
logger.debug(
60+
f"Runtime {self._inner_runtime.__class__.__name__} does not support "
61+
"streaming, falling back to execute()"
62+
)
63+
self.context.result = await self._inner_runtime.execute()
64+
65+
if self.context.result:
66+
await self.debug_bridge.emit_execution_completed(self.context.result)
67+
68+
return self.context.result
69+
70+
except Exception as e:
71+
# Emit execution error
72+
self.context.result = UiPathRuntimeResult(
73+
status=UiPathRuntimeStatus.FAULTED,
74+
)
75+
await self.debug_bridge.emit_execution_error(
76+
error=str(e),
77+
)
78+
raise
79+
80+
async def _stream_and_debug(self) -> Optional[UiPathRuntimeResult]:
81+
"""Stream events from inner runtime and handle debug interactions."""
82+
if not self._inner_runtime:
83+
return None
84+
85+
final_result: Optional[UiPathRuntimeResult] = None
86+
execution_completed = False
87+
88+
# Starting in paused state - wait for breakpoints and resume
89+
await self.debug_bridge.wait_for_resume()
90+
91+
# Keep streaming until execution completes (not just paused at breakpoint)
92+
while not execution_completed:
93+
# Update breakpoints from debug bridge
94+
self._inner_runtime.context.breakpoints = (
95+
self.debug_bridge.get_breakpoints()
96+
)
97+
# Stream events from inner runtime
98+
async for event in self._inner_runtime.stream():
99+
# Handle final result
100+
if isinstance(event, UiPathRuntimeResult):
101+
final_result = event
102+
103+
# Check if it's a breakpoint result
104+
if isinstance(event, UiPathBreakpointResult):
105+
try:
106+
# Hit a breakpoint - wait for resume and continue
107+
await self.debug_bridge.emit_breakpoint_hit(event)
108+
await self.debug_bridge.wait_for_resume()
109+
110+
self._inner_runtime.context.resume = True
111+
112+
except UiPathDebugQuitError:
113+
final_result = UiPathRuntimeResult(
114+
status=UiPathRuntimeStatus.SUCCESSFUL,
115+
)
116+
execution_completed = True
117+
else:
118+
# Normal completion or suspension with dynamic interrupt
119+
execution_completed = True
120+
# Handle dynamic interrupts if present
121+
# Maybe poll for resume trigger completion here in future
122+
123+
# Handle state update events - send to debug bridge
124+
elif isinstance(event, UiPathRuntimeStateEvent):
125+
await self.debug_bridge.emit_state_update(event)
126+
127+
return final_result
128+
129+
async def validate(self) -> None:
130+
"""Validate runtime configuration."""
131+
if self._inner_runtime:
132+
await self._inner_runtime.validate()
133+
134+
async def cleanup(self) -> None:
135+
"""Cleanup runtime resources."""
136+
try:
137+
if self._inner_runtime:
138+
await self._inner_runtime.cleanup()
139+
finally:
140+
try:
141+
await self.debug_bridge.disconnect()
142+
except Exception as e:
143+
logger.warning(f"Error disconnecting debug bridge: {e}")

0 commit comments

Comments
 (0)