Skip to content

Commit b2b7b4e

Browse files
committed
fix: handle debug with api triggers
1 parent 305f867 commit b2b7b4e

File tree

8 files changed

+191
-134
lines changed

8 files changed

+191
-134
lines changed

pyproject.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
[project]
22
name = "uipath-dev"
3-
version = "0.0.11"
3+
version = "0.0.12"
44
description = "UiPath Developer Console"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
77
dependencies = [
8-
"uipath-core>=0.0.9, <0.1.0",
9-
"uipath-runtime>=0.1.3, <0.2.0",
8+
"uipath-runtime>=0.2.0, <0.3.0",
109
"textual>=6.7.1, <7.0.0",
1110
"pyperclip>=1.11.0, <2.0.0",
1211
]

src/uipath/dev/__init__.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,21 @@ async def handle_chat_input(self, event: Input.Submitted) -> None:
137137

138138
details_panel = self.query_one("#details-panel", RunDetailsPanel)
139139
if details_panel and details_panel.current_run:
140-
status = details_panel.current_run.status
140+
current_run = details_panel.current_run
141+
status = current_run.status
141142
if status == "running":
142143
self.app.notify(
143144
"Wait for agent response...", timeout=1.5, severity="warning"
144145
)
145146
return
146147

147-
if details_panel.current_run.status == "suspended":
148-
details_panel.current_run.resume_data = {"value": user_text}
148+
if current_run.status == "suspended":
149+
resume_input: dict[str, Any] | str = {}
150+
try:
151+
resume_input = json.loads(user_text)
152+
except json.JSONDecodeError:
153+
resume_input = user_text
154+
current_run.resume_data = resume_input
149155
else:
150156
msg = get_user_message(user_text)
151157
msg_ev = get_user_message_event(user_text)
@@ -154,13 +160,18 @@ async def handle_chat_input(self, event: Input.Submitted) -> None:
154160
ChatMessage(
155161
event=msg_ev,
156162
message=msg,
157-
run_id=details_panel.current_run.id,
163+
run_id=current_run.id,
158164
)
159165
)
160-
details_panel.current_run.add_event(msg_ev)
161-
details_panel.current_run.input_data = {"messages": [msg]}
166+
current_run.add_event(msg_ev)
167+
current_run.input_data = {"messages": [msg]}
162168

163-
asyncio.create_task(self._execute_runtime(details_panel.current_run))
169+
if current_run.mode == ExecutionMode.DEBUG:
170+
asyncio.create_task(
171+
self._resume_runtime(current_run, current_run.resume_data)
172+
)
173+
else:
174+
asyncio.create_task(self._execute_runtime(current_run))
164175

165176
event.input.clear()
166177

@@ -244,6 +255,12 @@ async def _execute_runtime(self, run: ExecutionRun) -> None:
244255
"""Wrapper that delegates execution to RunService."""
245256
await self.run_service.execute(run)
246257

258+
async def _resume_runtime(
259+
self, run: ExecutionRun, resume_data: dict[str, Any] | str | None = None
260+
) -> None:
261+
"""Wrapper that delegates execution to RunService."""
262+
await self.run_service.resume_debug(run, resume_data)
263+
247264
def _on_run_updated(self, run: ExecutionRun) -> None:
248265
"""Called whenever a run changes (status, times, logs, traces)."""
249266
# Update the run in history

src/uipath/dev/models/chat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def add(
143143

144144
existing_tool_call.result = UiPathConversationToolCallResult(
145145
timestamp=tc_event.end.timestamp,
146-
value=tc_event.end.result,
146+
value=tc_event.end.output,
147147
is_error=tc_event.end.is_error,
148148
cancelled=tc_event.end.cancelled,
149149
)

src/uipath/dev/models/execution.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def __init__(
3636
self.entrypoint = entrypoint
3737
self.input_data = input_data
3838
self.mode = mode
39-
self.resume_data: dict[str, Any] | None = None
39+
self.resume_data: dict[str, Any] | str | None = None
4040
self.output_data: dict[str, Any] | str | None = None
4141
self.start_time = datetime.now()
4242
self.end_time: datetime | None = None
@@ -98,4 +98,6 @@ def messages(self) -> list[UiPathConversationMessage]:
9898

9999
def add_event(self, event: Any) -> UiPathConversationMessage | None:
100100
"""Add a conversation event to the run's chat aggregator."""
101+
if event is None:
102+
return None
101103
return self.chat_events.add(cast(UiPathConversationMessageEvent, event))

src/uipath/dev/services/debug_bridge.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from uipath.runtime.debug import UiPathBreakpointResult, UiPathDebugQuitError
88
from uipath.runtime.events import UiPathRuntimeStateEvent
99
from uipath.runtime.result import UiPathRuntimeResult
10+
from uipath.runtime.resumable import UiPathResumeTriggerType
1011

1112
logger = logging.getLogger(__name__)
1213

@@ -18,6 +19,7 @@ def __init__(self):
1819
"""Initialize the debug bridge."""
1920
self._connected = False
2021
self._resume_event = asyncio.Event()
22+
self._resume_data: dict[str, Any] | None = None
2123
self._terminate_event = asyncio.Event()
2224
self._breakpoints: list[str] | Literal["*"] = "*" # Default: step mode
2325

@@ -60,6 +62,29 @@ async def emit_breakpoint_hit(
6062
if self.on_breakpoint_hit:
6163
self.on_breakpoint_hit(breakpoint_result)
6264

65+
async def emit_execution_suspended(
66+
self, runtime_result: UiPathRuntimeResult
67+
) -> None:
68+
"""Notify debugger that execution is suspended."""
69+
logger.debug("Execution suspended")
70+
if runtime_result.trigger is None:
71+
return
72+
73+
if runtime_result.trigger.trigger_type == UiPathResumeTriggerType.API:
74+
if self.on_breakpoint_hit:
75+
self.on_breakpoint_hit(
76+
UiPathBreakpointResult(
77+
breakpoint_node="<suspended>",
78+
breakpoint_type="before",
79+
current_state=runtime_result.output,
80+
next_nodes=[],
81+
)
82+
)
83+
84+
async def emit_execution_resumed(self, resume_data: Any) -> None:
85+
"""Notify debugger that execution resumed."""
86+
logger.debug("Execution resumed")
87+
6388
async def emit_execution_completed(
6489
self, runtime_result: UiPathRuntimeResult
6590
) -> None:
@@ -86,12 +111,15 @@ async def wait_for_resume(self) -> Any:
86111
if self._terminate_event.is_set():
87112
raise UiPathDebugQuitError("Debug session quit requested")
88113

114+
return self._resume_data
115+
89116
async def wait_for_terminate(self) -> None:
90117
"""Wait for terminate command from debugger."""
91118
await self._terminate_event.wait()
92119

93-
def resume(self) -> None:
120+
def resume(self, resume_data: dict[str, Any] | str | None = None) -> None:
94121
"""Signal that execution should resume (called from UI buttons)."""
122+
self._resume_data = resume_data or {}
95123
self._resume_event.set()
96124

97125
def quit(self) -> None:

src/uipath/dev/services/run_service.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,10 @@ def get_run(self, run_id: str) -> ExecutionRun | None:
8686
return self.runs.get(run_id)
8787

8888
async def execute(self, run: ExecutionRun) -> None:
89-
"""Execute or resume a run.
90-
91-
This is the extracted version of the old `_execute_runtime` method.
92-
"""
89+
"""Execute or resume a run."""
9390
new_runtime: UiPathRuntimeProtocol | None = None
9491
try:
95-
execution_input: dict[str, Any] | None = {}
92+
execution_input: dict[str, Any] | str | None = {}
9693
execution_options: UiPathExecuteOptions = UiPathExecuteOptions()
9794

9895
if run.status == "suspended":
@@ -179,14 +176,15 @@ async def execute(self, run: ExecutionRun) -> None:
179176
):
180177
run.status = "suspended"
181178
else:
182-
if result.output is None:
183-
run.output_data = {}
184-
elif isinstance(result.output, BaseModel):
185-
run.output_data = result.output.model_dump()
186-
else:
187-
run.output_data = result.output
188179
run.status = "completed"
189180

181+
if result.output is None:
182+
run.output_data = {}
183+
elif isinstance(result.output, BaseModel):
184+
run.output_data = result.output.model_dump()
185+
else:
186+
run.output_data = result.output
187+
190188
if run.output_data:
191189
self._add_info_log(run, f"Execution result: {run.output_data}")
192190

@@ -218,6 +216,16 @@ async def execute(self, run: ExecutionRun) -> None:
218216
if run.id in self.debug_bridges:
219217
del self.debug_bridges[run.id]
220218

219+
def resume_debug(
220+
self, run: ExecutionRun, resume_data: dict[str, Any] | str | None = None
221+
) -> None:
222+
"""Resume debug execution from a breakpoint."""
223+
debug_bridge = self.debug_bridges.get(run.id)
224+
if debug_bridge:
225+
run.status = "running"
226+
self._emit_run_updated(run)
227+
debug_bridge.resume(resume_data)
228+
221229
def step_debug(self, run: ExecutionRun) -> None:
222230
"""Step to next breakpoint in debug mode."""
223231
debug_bridge = self.debug_bridges.get(run.id)

src/uipath/dev/ui/panels/run_details_panel.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ def add_chat_message(
436436

437437
if not self.current_run or chat_msg.run_id != self.current_run.id:
438438
return
439+
439440
self._chat_panel.add_chat_message(chat_msg)
440441

441442
def add_trace(self, trace_msg: TraceMessage):

0 commit comments

Comments
 (0)