Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-runtime"
version = "0.10.0"
version = "0.10.1"
description = "Runtime abstractions and interfaces for building agents and automation scripts in the UiPath ecosystem"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
16 changes: 2 additions & 14 deletions src/uipath/runtime/chat/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from uipath.core.chat import (
UiPathConversationMessageEvent,
)
from uipath.core.triggers import UiPathResumeTrigger


class UiPathChatProtocol(Protocol):
Expand All @@ -32,17 +31,6 @@ async def emit_message_event(
"""
...

async def emit_interrupt_event(
Copy link
Copy Markdown
Member

@cristipufu cristipufu Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't remove this, we'll need it in the future. There are multiple types of interrupt, not just tool call confirmation. The runtime contract is fine even if you decide to handle tool call confirmations in another way

Copy link
Copy Markdown
Contributor Author

@JoshParkSJ JoshParkSJ Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We decided to remove interrupts from conversational agents and only support tool confirmation because interrupts are too much a low-level abstraction and has no use case for convo agents other than tool confirmation

Here is the relevant thread but this is the general consensus amongst other conversational agent SDK as well

self,
resume_trigger: UiPathResumeTrigger,
) -> None:
"""Wrap and send an interrupt event.

Args:
resume_trigger: UiPathResumeTrigger to wrap and send
"""
...

async def emit_exchange_end_event(self) -> None:
"""Send an exchange end event."""
...
Expand All @@ -51,6 +39,6 @@ async def emit_exchange_error_event(self, error: Exception) -> None:
"""Emit an exchange error event."""
...

async def wait_for_resume(self) -> dict[str, Any]:
"""Wait for the interrupt_end event to be received."""
async def wait_for_tool_confirmation(self) -> dict[str, Any]:
"""Wait for a confirmToolCall event to be received."""
...
6 changes: 1 addition & 5 deletions src/uipath/runtime/chat/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,7 @@ async def stream(
resume_map: dict[str, Any] = {}

for trigger in api_triggers:
await self.chat_bridge.emit_interrupt_event(trigger)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line reacts to the interruption in the runtime code (be it tool call confirmation or another type of interrupt)

Copy link
Copy Markdown
Contributor Author

@JoshParkSJ JoshParkSJ Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have another interrupt type. We assumed there would be other interrupt use case with convo agents but there isn't

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I'm saying is that you don't need to make any changes to the runtime contracts right now. You can simply leave this code as it is (change the CAS bridge implementation of emit_interrupt_event to no-op).

I want us to be flexible, in case we change our mind again, or decide that we actually need an interrupt event (for coded agents there's nothing preventing the user from doing interrupt("Do you want to continue?") in the coded agent's code. This is something that we'll probably have to handle gracefully at some point


resume_data = (
await self.chat_bridge.wait_for_resume()
)
resume_data = await self.chat_bridge.wait_for_tool_confirmation()

assert trigger.interrupt_id is not None, (
"Trigger interrupt_id cannot be None"
Expand Down
35 changes: 9 additions & 26 deletions tests/test_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ def make_chat_bridge_mock() -> UiPathChatProtocol:
bridge_mock.connect = AsyncMock()
bridge_mock.disconnect = AsyncMock()
bridge_mock.emit_message_event = AsyncMock()
bridge_mock.emit_interrupt_event = AsyncMock()
bridge_mock.wait_for_resume = AsyncMock()
bridge_mock.wait_for_tool_confirmation = AsyncMock()

return cast(UiPathChatProtocol, bridge_mock)

Expand Down Expand Up @@ -309,7 +308,7 @@ async def test_chat_runtime_handles_api_trigger_suspension():
runtime_impl = SuspendingMockRuntime(suspend_at_message=0)
bridge = make_chat_bridge_mock()

cast(AsyncMock, bridge.wait_for_resume).return_value = {"approved": True}
cast(AsyncMock, bridge.wait_for_tool_confirmation).return_value = {"approved": True}

chat_runtime = UiPathChatRuntime(
delegate=runtime_impl,
Expand All @@ -331,8 +330,7 @@ async def test_chat_runtime_handles_api_trigger_suspension():
cast(AsyncMock, bridge.connect).assert_awaited_once()
cast(AsyncMock, bridge.disconnect).assert_awaited_once()

cast(AsyncMock, bridge.emit_interrupt_event).assert_awaited_once()
cast(AsyncMock, bridge.wait_for_resume).assert_awaited_once()
cast(AsyncMock, bridge.wait_for_tool_confirmation).assert_awaited_once()

# Message events emitted (one before suspend, one after resume)
assert cast(AsyncMock, bridge.emit_message_event).await_count == 2
Expand All @@ -345,8 +343,8 @@ async def test_chat_runtime_yields_events_during_suspension_flow():
runtime_impl = SuspendingMockRuntime(suspend_at_message=0)
bridge = make_chat_bridge_mock()

# wait_for_resume returns approval data
cast(AsyncMock, bridge.wait_for_resume).return_value = {"approved": True}
# wait_for_tool_confirmation returns approval data
cast(AsyncMock, bridge.wait_for_tool_confirmation).return_value = {"approved": True}

chat_runtime = UiPathChatRuntime(
delegate=runtime_impl,
Expand Down Expand Up @@ -533,7 +531,7 @@ async def test_chat_runtime_handles_multiple_api_triggers():
bridge = make_chat_bridge_mock()

# Bridge returns approval for each trigger
cast(AsyncMock, bridge.wait_for_resume).side_effect = [
cast(AsyncMock, bridge.wait_for_tool_confirmation).side_effect = [
{"approved": True}, # email-confirm
{"approved": True}, # file-delete
{"approved": True}, # api-call
Expand Down Expand Up @@ -563,14 +561,7 @@ async def test_chat_runtime_handles_multiple_api_triggers():
assert resume_input["api-call"] == {"approved": True}

# Bridge should have been called 3 times (once per trigger)
assert cast(AsyncMock, bridge.emit_interrupt_event).await_count == 3
assert cast(AsyncMock, bridge.wait_for_resume).await_count == 3

# Verify each emit_interrupt_event received a trigger
emit_calls = cast(AsyncMock, bridge.emit_interrupt_event).await_args_list
assert emit_calls[0][0][0].interrupt_id == "email-confirm"
assert emit_calls[1][0][0].interrupt_id == "file-delete"
assert emit_calls[2][0][0].interrupt_id == "api-call"
assert cast(AsyncMock, bridge.wait_for_tool_confirmation).await_count == 3


@pytest.mark.asyncio
Expand All @@ -581,7 +572,7 @@ async def test_chat_runtime_filters_non_api_triggers():
bridge = make_chat_bridge_mock()

# Bridge returns approval for API triggers only
cast(AsyncMock, bridge.wait_for_resume).side_effect = [
cast(AsyncMock, bridge.wait_for_tool_confirmation).side_effect = [
{"approved": True}, # email-confirm
{"approved": True}, # file-delete
]
Expand All @@ -603,12 +594,4 @@ async def test_chat_runtime_filters_non_api_triggers():
assert result.triggers[0].trigger_type == UiPathResumeTriggerType.QUEUE_ITEM

# Bridge should have been called only 2 times (for 2 API triggers)
assert cast(AsyncMock, bridge.emit_interrupt_event).await_count == 2
assert cast(AsyncMock, bridge.wait_for_resume).await_count == 2

# Verify only API triggers were emitted
emit_calls = cast(AsyncMock, bridge.emit_interrupt_event).await_args_list
assert emit_calls[0][0][0].interrupt_id == "email-confirm"
assert emit_calls[0][0][0].trigger_type == UiPathResumeTriggerType.API
assert emit_calls[1][0][0].interrupt_id == "file-delete"
assert emit_calls[1][0][0].trigger_type == UiPathResumeTriggerType.API
assert cast(AsyncMock, bridge.wait_for_tool_confirmation).await_count == 2
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading