Skip to content

Commit ee3b2be

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

File tree

7 files changed

+586
-4
lines changed

7 files changed

+586
-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: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""Debug runtime implementation."""
2+
3+
import logging
4+
from typing import Generic, TypeVar
5+
6+
from uipath.runtime import (
7+
UiPathBaseRuntime,
8+
UiPathBreakpointResult,
9+
UiPathRuntimeContext,
10+
UiPathRuntimeResult,
11+
UiPathRuntimeStatus,
12+
UiPathStreamNotSupportedError,
13+
)
14+
from uipath.runtime.debug import UiPathDebugBridge, UiPathDebugQuitError
15+
from uipath.runtime.events import (
16+
UiPathRuntimeStateEvent,
17+
)
18+
19+
logger = logging.getLogger(__name__)
20+
21+
T = TypeVar("T", bound=UiPathBaseRuntime)
22+
C = TypeVar("C", bound=UiPathRuntimeContext)
23+
24+
25+
class UiPathDebugRuntime(UiPathBaseRuntime, Generic[T]):
26+
"""Specialized runtime for debug runs that streams events to a debug bridge."""
27+
28+
def __init__(
29+
self,
30+
context: UiPathRuntimeContext,
31+
delegate: T,
32+
debug_bridge: UiPathDebugBridge,
33+
):
34+
"""Initialize the UiPathDebugRuntime."""
35+
super().__init__(context)
36+
self.context: UiPathRuntimeContext = context
37+
self.delegate: T = delegate
38+
self.debug_bridge: UiPathDebugBridge = debug_bridge
39+
40+
async def execute(self) -> UiPathRuntimeResult:
41+
"""Execute the workflow with debug support."""
42+
try:
43+
await self.debug_bridge.connect()
44+
45+
await self.debug_bridge.emit_execution_started()
46+
47+
result: UiPathRuntimeResult
48+
# Try to stream events from inner runtime
49+
try:
50+
result = await self._stream_and_debug(self.delegate)
51+
except UiPathStreamNotSupportedError:
52+
# Fallback to regular execute if streaming not supported
53+
logger.debug(
54+
f"Runtime {self.delegate.__class__.__name__} does not support "
55+
"streaming, falling back to execute()"
56+
)
57+
result = await self.delegate.execute()
58+
59+
await self.debug_bridge.emit_execution_completed(result)
60+
61+
self.context.result = result
62+
63+
return result
64+
65+
except Exception as e:
66+
# Emit execution error
67+
self.context.result = UiPathRuntimeResult(
68+
status=UiPathRuntimeStatus.FAULTED,
69+
)
70+
await self.debug_bridge.emit_execution_error(
71+
error=str(e),
72+
)
73+
raise
74+
75+
async def _stream_and_debug(self, inner_runtime: T) -> UiPathRuntimeResult:
76+
"""Stream events from inner runtime and handle debug interactions."""
77+
final_result: UiPathRuntimeResult
78+
execution_completed = False
79+
80+
# Starting in paused state - wait for breakpoints and resume
81+
await self.debug_bridge.wait_for_resume()
82+
83+
# Keep streaming until execution completes (not just paused at breakpoint)
84+
while not execution_completed:
85+
# Update breakpoints from debug bridge
86+
inner_runtime.context.breakpoints = self.debug_bridge.get_breakpoints()
87+
# Stream events from inner runtime
88+
async for event in inner_runtime.stream():
89+
# Handle final result
90+
if isinstance(event, UiPathRuntimeResult):
91+
final_result = event
92+
93+
# Check if it's a breakpoint result
94+
if isinstance(event, UiPathBreakpointResult):
95+
try:
96+
# Hit a breakpoint - wait for resume and continue
97+
await self.debug_bridge.emit_breakpoint_hit(event)
98+
await self.debug_bridge.wait_for_resume()
99+
100+
self.delegate.context.resume = True
101+
102+
except UiPathDebugQuitError:
103+
final_result = UiPathRuntimeResult(
104+
status=UiPathRuntimeStatus.SUCCESSFUL,
105+
)
106+
execution_completed = True
107+
else:
108+
# Normal completion or suspension with dynamic interrupt
109+
execution_completed = True
110+
# Handle dynamic interrupts if present
111+
# In the future, poll for resume trigger completion here, using the debug bridge
112+
113+
# Handle state update events - send to debug bridge
114+
elif isinstance(event, UiPathRuntimeStateEvent):
115+
await self.debug_bridge.emit_state_update(event)
116+
117+
return final_result
118+
119+
async def validate(self) -> None:
120+
"""Validate runtime configuration."""
121+
if self.delegate:
122+
await self.delegate.validate()
123+
124+
async def cleanup(self) -> None:
125+
"""Cleanup runtime resources."""
126+
try:
127+
if self.delegate:
128+
await self.delegate.cleanup()
129+
finally:
130+
try:
131+
await self.debug_bridge.disconnect()
132+
except Exception as e:
133+
logger.warning(f"Error disconnecting debug bridge: {e}")

0 commit comments

Comments
 (0)