Skip to content

Commit 3d9d8eb

Browse files
committed
feat: add debug runtime
1 parent 72abea4 commit 3d9d8eb

File tree

7 files changed

+621
-4
lines changed

7 files changed

+621
-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: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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) -> 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+
result: UiPathRuntimeResult
55+
# Try to stream events from inner runtime
56+
try:
57+
result = await self._stream_and_debug()
58+
except UiPathStreamNotSupportedError:
59+
# Fallback to regular execute if streaming not supported
60+
logger.debug(
61+
f"Runtime {self._inner_runtime.__class__.__name__} does not support "
62+
"streaming, falling back to execute()"
63+
)
64+
result = await self._inner_runtime.execute()
65+
66+
await self.debug_bridge.emit_execution_completed(result)
67+
68+
self.context.result = result
69+
70+
return result
71+
72+
except Exception as e:
73+
# Emit execution error
74+
self.context.result = UiPathRuntimeResult(
75+
status=UiPathRuntimeStatus.FAULTED,
76+
)
77+
await self.debug_bridge.emit_execution_error(
78+
error=str(e),
79+
)
80+
raise
81+
82+
async def _stream_and_debug(self) -> Optional[UiPathRuntimeResult]:
83+
"""Stream events from inner runtime and handle debug interactions."""
84+
if not self._inner_runtime:
85+
return None
86+
87+
final_result: Optional[UiPathRuntimeResult] = None
88+
execution_completed = False
89+
90+
# Starting in paused state - wait for breakpoints and resume
91+
await self.debug_bridge.wait_for_resume()
92+
93+
# Keep streaming until execution completes (not just paused at breakpoint)
94+
while not execution_completed:
95+
# Update breakpoints from debug bridge
96+
self._inner_runtime.context.breakpoints = (
97+
self.debug_bridge.get_breakpoints()
98+
)
99+
# Stream events from inner runtime
100+
async for event in self._inner_runtime.stream():
101+
# Handle final result
102+
if isinstance(event, UiPathRuntimeResult):
103+
final_result = event
104+
105+
# Check if it's a breakpoint result
106+
if isinstance(event, UiPathBreakpointResult):
107+
try:
108+
# Hit a breakpoint - wait for resume and continue
109+
await self.debug_bridge.emit_breakpoint_hit(event)
110+
await self.debug_bridge.wait_for_resume()
111+
112+
self._inner_runtime.context.resume = True
113+
114+
except UiPathDebugQuitError:
115+
final_result = UiPathRuntimeResult(
116+
status=UiPathRuntimeStatus.SUCCESSFUL,
117+
)
118+
execution_completed = True
119+
else:
120+
# Normal completion or suspension with dynamic interrupt
121+
execution_completed = True
122+
# Handle dynamic interrupts if present
123+
# In the future, poll for resume trigger completion here, using the debug bridge
124+
125+
# Handle state update events - send to debug bridge
126+
elif isinstance(event, UiPathRuntimeStateEvent):
127+
await self.debug_bridge.emit_state_update(event)
128+
129+
return final_result
130+
131+
async def validate(self) -> None:
132+
"""Validate runtime configuration."""
133+
if self._inner_runtime:
134+
await self._inner_runtime.validate()
135+
136+
async def cleanup(self) -> None:
137+
"""Cleanup runtime resources."""
138+
try:
139+
if self._inner_runtime:
140+
await self._inner_runtime.cleanup()
141+
finally:
142+
try:
143+
await self.debug_bridge.disconnect()
144+
except Exception as e:
145+
logger.warning(f"Error disconnecting debug bridge: {e}")

0 commit comments

Comments
 (0)