Skip to content

Commit c2e3a0c

Browse files
andrewwan-uipathradu-mocanu
authored andcommitted
feat: propagate errors from agent runtime to CAS
1 parent 9266564 commit c2e3a0c

File tree

4 files changed

+87
-53
lines changed

4 files changed

+87
-53
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-runtime"
3-
version = "0.9.3"
3+
version = "0.9.22"
44
description = "Runtime abstractions and interfaces for building agents and automation scripts in the UiPath ecosystem"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/runtime/chat/protocol.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ async def emit_exchange_end_event(self) -> None:
4747
"""Send an exchange end event."""
4848
...
4949

50+
async def emit_exchange_error_event(self, error: Exception) -> None:
51+
"""Emit an exchange error event."""
52+
...
53+
5054
async def wait_for_resume(self) -> dict[str, Any]:
5155
"""Wait for the interrupt_end event to be received."""
5256
...

src/uipath/runtime/chat/runtime.py

Lines changed: 81 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
UiPathStreamOptions,
1212
)
1313
from uipath.runtime.chat.protocol import UiPathChatProtocol
14+
from uipath.runtime.errors import UiPathBaseRuntimeError
15+
from uipath.runtime.errors.contract import UiPathErrorCategory
1416
from uipath.runtime.events import (
1517
UiPathRuntimeEvent,
1618
UiPathRuntimeMessageEvent,
@@ -65,62 +67,90 @@ async def stream(
6567
options: UiPathStreamOptions | None = None,
6668
) -> AsyncGenerator[UiPathRuntimeEvent, None]:
6769
"""Stream execution events with chat support."""
68-
await self.chat_bridge.connect()
69-
70-
execution_completed = False
71-
current_input = input
72-
current_options = UiPathStreamOptions(
73-
resume=options.resume if options else False,
74-
breakpoints=options.breakpoints if options else None,
75-
)
76-
77-
while not execution_completed:
78-
async for event in self.delegate.stream(
79-
current_input, options=current_options
80-
):
81-
if isinstance(event, UiPathRuntimeMessageEvent):
82-
if event.payload:
83-
await self.chat_bridge.emit_message_event(event.payload)
84-
85-
if isinstance(event, UiPathRuntimeResult):
86-
runtime_result = event
87-
88-
if (
89-
runtime_result.status == UiPathRuntimeStatus.SUSPENDED
90-
and runtime_result.triggers
91-
):
92-
api_triggers = [
93-
t
94-
for t in runtime_result.triggers
95-
if t.trigger_type == UiPathResumeTriggerType.API
96-
]
97-
98-
if api_triggers:
99-
resume_map: dict[str, Any] = {}
100-
101-
for trigger in api_triggers:
102-
await self.chat_bridge.emit_interrupt_event(trigger)
103-
104-
resume_data = await self.chat_bridge.wait_for_resume()
105-
106-
assert trigger.interrupt_id is not None, (
107-
"Trigger interrupt_id cannot be None"
108-
)
109-
resume_map[trigger.interrupt_id] = resume_data
110-
111-
current_input = resume_map
112-
current_options.resume = True
113-
break
70+
try:
71+
await self.chat_bridge.connect()
72+
73+
execution_completed = False
74+
current_input = input
75+
current_options = UiPathStreamOptions(
76+
resume=options.resume if options else False,
77+
breakpoints=options.breakpoints if options else None,
78+
)
79+
80+
while not execution_completed:
81+
async for event in self.delegate.stream(
82+
current_input, options=current_options
83+
):
84+
if isinstance(event, UiPathRuntimeMessageEvent):
85+
if event.payload:
86+
await self.chat_bridge.emit_message_event(event.payload)
87+
88+
if isinstance(event, UiPathRuntimeResult):
89+
runtime_result = event
90+
91+
if (
92+
runtime_result.status == UiPathRuntimeStatus.SUSPENDED
93+
and runtime_result.triggers
94+
):
95+
api_triggers = [
96+
t
97+
for t in runtime_result.triggers
98+
if t.trigger_type == UiPathResumeTriggerType.API
99+
]
100+
101+
if api_triggers:
102+
resume_map: dict[str, Any] = {}
103+
104+
for trigger in api_triggers:
105+
await self.chat_bridge.emit_interrupt_event(trigger)
106+
107+
resume_data = (
108+
await self.chat_bridge.wait_for_resume()
109+
)
110+
111+
assert trigger.interrupt_id is not None, (
112+
"Trigger interrupt_id cannot be None"
113+
)
114+
resume_map[trigger.interrupt_id] = resume_data
115+
116+
current_input = resume_map
117+
current_options.resume = True
118+
break
119+
else:
120+
# No API triggers - yield result and complete
121+
yield event
122+
execution_completed = True
123+
elif runtime_result.status == UiPathRuntimeStatus.FAULTED:
124+
error = runtime_result.error
125+
faulted_error = UiPathBaseRuntimeError(
126+
code=error.code if error else "UNKNOWN",
127+
title=error.title if error else "Unknown Error",
128+
detail=error.detail if error else "",
129+
category=error.category
130+
if error
131+
else UiPathErrorCategory.UNKNOWN,
132+
status=error.status if error else None,
133+
)
134+
await self._emit_error_event(faulted_error)
135+
yield event
136+
execution_completed = True
114137
else:
115-
# No API triggers - yield result and complete
116138
yield event
117139
execution_completed = True
140+
await self.chat_bridge.emit_exchange_end_event()
118141
else:
119142
yield event
120-
execution_completed = True
121-
await self.chat_bridge.emit_exchange_end_event()
122-
else:
123-
yield event
143+
144+
except Exception as e:
145+
await self._emit_error_event(e)
146+
raise
147+
148+
async def _emit_error_event(self, error: Exception) -> None:
149+
"""Emit an exchange error event to the chat bridge."""
150+
try:
151+
await self.chat_bridge.emit_exchange_error_event(error)
152+
except Exception:
153+
logger.warning("Failed to emit exchange error event", exc_info=True)
124154

125155
async def get_schema(self) -> UiPathRuntimeSchema:
126156
"""Get schema from the delegate runtime."""

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)