From 03b6f731e056d815ba618c98141e97ebf422744d Mon Sep 17 00:00:00 2001 From: Josh Park <50765702+JoshParkSJ@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:30:58 -0400 Subject: [PATCH 1/2] feat: replace conversation interrupts with tool call confirmation [JAR-8666] --- .../scripts/test_check_version_uniqueness.py | 1 - packages/uipath-core/pyproject.toml | 2 +- .../src/uipath/core/chat/__init__.py | 31 +---- .../src/uipath/core/chat/interrupt.py | 112 ------------------ .../src/uipath/core/chat/message.py | 8 -- .../uipath-core/src/uipath/core/chat/tool.py | 27 +++++ packages/uipath-core/uv.lock | 2 +- .../services/test_conversations_service.py | 3 - packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/_cli/_chat/_bridge.py | 102 +++------------- .../eval/models/_conversational_utils.py | 4 - packages/uipath/uv.lock | 4 +- 13 files changed, 55 insertions(+), 245 deletions(-) delete mode 100644 packages/uipath-core/src/uipath/core/chat/interrupt.py diff --git a/.github/scripts/test_check_version_uniqueness.py b/.github/scripts/test_check_version_uniqueness.py index 94a2b3cc0..af4298af1 100644 --- a/.github/scripts/test_check_version_uniqueness.py +++ b/.github/scripts/test_check_version_uniqueness.py @@ -5,7 +5,6 @@ import urllib.error from unittest import mock -import pytest from check_version_uniqueness import ( get_package_info, diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index 1a5c34ab7..387818039 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.11" +version = "0.5.12" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/chat/__init__.py b/packages/uipath-core/src/uipath/core/chat/__init__.py index 476cb9352..c93a7aeb1 100644 --- a/packages/uipath-core/src/uipath/core/chat/__init__.py +++ b/packages/uipath-core/src/uipath/core/chat/__init__.py @@ -77,20 +77,6 @@ UiPathConversationExchangeEvent, UiPathConversationExchangeStartEvent, ) -from .interrupt import ( - InterruptTypeEnum, - UiPathConversationGenericInterruptEndEvent, - UiPathConversationGenericInterruptStartEvent, - UiPathConversationInterrupt, - UiPathConversationInterruptData, - UiPathConversationInterruptEndEvent, - UiPathConversationInterruptEvent, - UiPathConversationInterruptStartEvent, - UiPathConversationToolCallConfirmationEndValue, - UiPathConversationToolCallConfirmationInterruptEndEvent, - UiPathConversationToolCallConfirmationInterruptStartEvent, - UiPathConversationToolCallConfirmationValue, -) from .message import ( UiPathConversationMessage, UiPathConversationMessageData, @@ -108,6 +94,8 @@ ) from .tool import ( UiPathConversationToolCall, + UiPathConversationToolCallConfirmation, + UiPathConversationToolCallConfirmationEvent, UiPathConversationToolCallData, UiPathConversationToolCallEndEvent, UiPathConversationToolCallEvent, @@ -141,19 +129,6 @@ "UiPathConversationMessageEvent", "UiPathConversationMessageData", "UiPathConversationMessage", - # Interrupt - "InterruptTypeEnum", - "UiPathConversationInterruptStartEvent", - "UiPathConversationInterruptEndEvent", - "UiPathConversationInterruptEvent", - "UiPathConversationToolCallConfirmationValue", - "UiPathConversationToolCallConfirmationEndValue", - "UiPathConversationToolCallConfirmationInterruptStartEvent", - "UiPathConversationToolCallConfirmationInterruptEndEvent", - "UiPathConversationGenericInterruptStartEvent", - "UiPathConversationGenericInterruptEndEvent", - "UiPathConversationInterruptData", - "UiPathConversationInterrupt", # Content "UiPathConversationContentPartChunkEvent", "UiPathConversationContentPartStartEvent", @@ -178,6 +153,8 @@ # Tool "UiPathConversationToolCallStartEvent", "UiPathConversationToolCallEndEvent", + "UiPathConversationToolCallConfirmation", + "UiPathConversationToolCallConfirmationEvent", "UiPathConversationToolCallEvent", "UiPathConversationToolCallResult", "UiPathConversationToolCallData", diff --git a/packages/uipath-core/src/uipath/core/chat/interrupt.py b/packages/uipath-core/src/uipath/core/chat/interrupt.py deleted file mode 100644 index a2ce3e13f..000000000 --- a/packages/uipath-core/src/uipath/core/chat/interrupt.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Interrupt events for human-in-the-loop patterns.""" - -from enum import Enum -from typing import Any, Literal, Union - -from pydantic import BaseModel, ConfigDict, Field - - -class InterruptTypeEnum(str, Enum): - """Enum of known interrupt types.""" - - TOOL_CALL_CONFIRMATION = "uipath_cas_tool_call_confirmation" - - -class UiPathConversationToolCallConfirmationValue(BaseModel): - """Schema for tool call confirmation interrupt value.""" - - tool_call_id: str = Field(..., alias="toolCallId") - tool_name: str = Field(..., alias="toolName") - input_schema: Any = Field(..., alias="inputSchema") - input_value: Any | None = Field(None, alias="inputValue") - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationToolCallConfirmationInterruptStartEvent(BaseModel): - """Tool call confirmation interrupt start event with strong typing.""" - - type: Literal["uipath_cas_tool_call_confirmation"] - value: UiPathConversationToolCallConfirmationValue - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationGenericInterruptStartEvent(BaseModel): - """Generic interrupt start event for custom interrupt types.""" - - type: str - value: Any - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -UiPathConversationInterruptStartEvent = Union[ - UiPathConversationToolCallConfirmationInterruptStartEvent, - UiPathConversationGenericInterruptStartEvent, -] - - -class UiPathConversationToolCallConfirmationEndValue(BaseModel): - """Schema for tool call confirmation end value.""" - - approved: bool - input: Any | None = None - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationToolCallConfirmationInterruptEndEvent(BaseModel): - """Tool call confirmation interrupt end event with strong typing.""" - - type: Literal["uipath_cas_tool_call_confirmation"] - value: UiPathConversationToolCallConfirmationEndValue - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationGenericInterruptEndEvent(BaseModel): - """Generic interrupt end event for custom interrupt types.""" - - type: str - value: Any - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -UiPathConversationInterruptEndEvent = Union[ - UiPathConversationToolCallConfirmationInterruptEndEvent, - UiPathConversationGenericInterruptEndEvent, -] - - -class UiPathConversationInterruptEvent(BaseModel): - """Encapsulates interrupt-related events within a message.""" - - interrupt_id: str = Field(..., alias="interruptId") - start: UiPathConversationInterruptStartEvent | None = Field( - None, alias="startInterrupt" - ) - end: UiPathConversationInterruptEndEvent | None = Field(None, alias="endInterrupt") - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationInterruptData(BaseModel): - """Represents the core data of an interrupt within a message - a pause point where the agent needs external input.""" - - type: str - interrupt_value: Any = Field(..., alias="interruptValue") - end_value: Any | None = Field(None, alias="endValue") - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationInterrupt(UiPathConversationInterruptData): - """Represents an interrupt within a message - a pause point where the agent needs external input.""" - - interrupt_id: str = Field(..., alias="interruptId") - created_at: str = Field(..., alias="createdAt") - updated_at: str = Field(..., alias="updatedAt") - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) diff --git a/packages/uipath-core/src/uipath/core/chat/message.py b/packages/uipath-core/src/uipath/core/chat/message.py index 48e79171f..9d6aa248d 100644 --- a/packages/uipath-core/src/uipath/core/chat/message.py +++ b/packages/uipath-core/src/uipath/core/chat/message.py @@ -10,11 +10,6 @@ UiPathConversationContentPartEvent, ) from .error import UiPathConversationErrorEvent -from .interrupt import ( - UiPathConversationInterrupt, - UiPathConversationInterruptData, - UiPathConversationInterruptEvent, -) from .tool import ( UiPathConversationToolCall, UiPathConversationToolCallData, @@ -53,7 +48,6 @@ class UiPathConversationMessageEvent(BaseModel): None, alias="contentPart" ) tool_call: UiPathConversationToolCallEvent | None = Field(None, alias="toolCall") - interrupt: UiPathConversationInterruptEvent | None = None meta_event: dict[str, Any] | None = Field(None, alias="metaEvent") error: UiPathConversationErrorEvent | None = Field(None, alias="messageError") @@ -68,7 +62,6 @@ class UiPathConversationMessageData(BaseModel): ..., alias="contentParts" ) tool_calls: Sequence[UiPathConversationToolCallData] = Field(..., alias="toolCalls") - interrupts: Sequence[UiPathConversationInterruptData] model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) @@ -86,6 +79,5 @@ class UiPathConversationMessage(UiPathConversationMessageData): ..., alias="contentParts" ) tool_calls: Sequence[UiPathConversationToolCall] = Field(..., alias="toolCalls") - interrupts: Sequence[UiPathConversationInterrupt] model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) diff --git a/packages/uipath-core/src/uipath/core/chat/tool.py b/packages/uipath-core/src/uipath/core/chat/tool.py index 9c9e911bd..818f5574f 100644 --- a/packages/uipath-core/src/uipath/core/chat/tool.py +++ b/packages/uipath-core/src/uipath/core/chat/tool.py @@ -25,6 +25,8 @@ class UiPathConversationToolCallStartEvent(BaseModel): timestamp: str | None = None input: dict[str, Any] | None = None metadata: dict[str, Any] | None = Field(None, alias="metaData") + require_confirmation: bool | None = Field(None, alias="requireConfirmation") + input_schema: Any | None = Field(None, alias="inputSchema") model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) @@ -41,6 +43,25 @@ class UiPathConversationToolCallEndEvent(BaseModel): model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) +class UiPathConversationToolCallConfirmationEvent(BaseModel): + """Signals a tool call confirmation (approve/reject) from the client.""" + + approved: bool + input: Any | None = None + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + +class UiPathConversationToolCallConfirmation(BaseModel): + """Represents the stored confirmation state on a tool call.""" + + approved: bool + input: Any | None = None + confirmed_at: str | None = Field(None, alias="confirmedAt") + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + class UiPathConversationToolCallEvent(BaseModel): """Encapsulates the data related to a tool call event.""" @@ -49,6 +70,9 @@ class UiPathConversationToolCallEvent(BaseModel): None, alias="startToolCall" ) end: UiPathConversationToolCallEndEvent | None = Field(None, alias="endToolCall") + confirm: UiPathConversationToolCallConfirmationEvent | None = Field( + None, alias="confirmToolCall" + ) meta_event: dict[str, Any] | None = Field(None, alias="metaEvent") error: UiPathConversationErrorEvent | None = Field(None, alias="toolCallError") @@ -61,6 +85,9 @@ class UiPathConversationToolCallData(BaseModel): name: str input: dict[str, Any] | None = None result: UiPathConversationToolCallResult | None = None + require_confirmation: bool | None = Field(None, alias="requireConfirmation") + input_schema: Any | None = Field(None, alias="inputSchema") + confirmation: UiPathConversationToolCallConfirmation | None = None model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index e17802fc7..807653ed8 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -1007,7 +1007,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.11" +version = "0.5.12" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath-platform/tests/services/test_conversations_service.py b/packages/uipath-platform/tests/services/test_conversations_service.py index 31aa4a653..37e08bdfa 100644 --- a/packages/uipath-platform/tests/services/test_conversations_service.py +++ b/packages/uipath-platform/tests/services/test_conversations_service.py @@ -38,7 +38,6 @@ async def test_retrieve_message( "role": "assistant", "contentParts": [], "toolCalls": [], - "interrupts": [], "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z", }, @@ -95,7 +94,6 @@ async def test_retrieve_message_with_content_parts( } ], "toolCalls": [], - "interrupts": [], "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z", }, @@ -145,7 +143,6 @@ async def test_retrieve_message_with_tool_calls( "updatedAt": "2024-01-01T00:00:00Z", } ], - "interrupts": [], "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z", }, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index dce3eb8e9..6a299d6ec 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1056,7 +1056,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.11" +version = "0.5.12" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 7a7bc20ee..626941f70 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.48" +version = "2.10.49" description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath/src/uipath/_cli/_chat/_bridge.py b/packages/uipath/src/uipath/_cli/_chat/_bridge.py index 24c1be024..80e5e2b7a 100644 --- a/packages/uipath/src/uipath/_cli/_chat/_bridge.py +++ b/packages/uipath/src/uipath/_cli/_chat/_bridge.py @@ -4,7 +4,6 @@ import json import logging import os -import uuid from typing import Any from urllib.parse import urlparse @@ -14,13 +13,9 @@ UiPathConversationEvent, UiPathConversationExchangeEndEvent, UiPathConversationExchangeEvent, - UiPathConversationInterruptEndEvent, - UiPathConversationInterruptEvent, UiPathConversationMessageEvent, - UiPathConversationToolCallConfirmationInterruptStartEvent, - UiPathConversationToolCallConfirmationValue, + UiPathConversationToolCallConfirmationEvent, ) -from uipath.core.triggers import UiPathResumeTrigger from uipath.runtime.chat import UiPathChatProtocol from uipath.runtime.context import UiPathRuntimeContext @@ -126,9 +121,8 @@ def __init__( self._client: Any | None = None self._connected_event = asyncio.Event() - # Interrupt state for HITL round-trip - self._interrupt_end_event = asyncio.Event() - self._interrupt_end_value: UiPathConversationInterruptEndEvent | None = None + self._tool_confirmation_event = asyncio.Event() + self._tool_confirmation_value: UiPathConversationToolCallConfirmationEvent | None = None self._current_message_id: str | None = None # Set CAS_WEBSOCKET_DISABLED when using the debugger to prevent websocket errors from @@ -271,7 +265,6 @@ async def emit_message_event( else: await self._client.emit("ConversationEvent", event_data) - # Store the current message ID, used for emitting interrupt events. self._current_message_id = message_event.message_id except Exception as e: @@ -362,68 +355,15 @@ async def emit_exchange_error_event(self, error: Exception) -> None: logger.error(f"Error sending exchange error event to WebSocket: {e}") raise RuntimeError(f"Failed to send exchange error event: {e}") from e - async def emit_interrupt_event(self, resume_trigger: UiPathResumeTrigger): - if self._client and self._connected_event.is_set(): - try: - # Clear previous interrupt state and generate new interrupt_id - self._interrupt_id = str(uuid.uuid4()) - - # Ensure we have a valid message_id - if self._current_message_id is None: - raise RuntimeError( - "Cannot emit interrupt event: no current message_id set" - ) - - # Ensure api_resume is not None - if resume_trigger.api_resume is None: - raise RuntimeError( - "Cannot emit interrupt event: api_resume is None" - ) - - interrupt_event = UiPathConversationEvent( - conversation_id=self.conversation_id, - exchange=UiPathConversationExchangeEvent( - exchange_id=self.exchange_id, - message=UiPathConversationMessageEvent( - message_id=self._current_message_id, - interrupt=UiPathConversationInterruptEvent( - interrupt_id=self._interrupt_id, - start=UiPathConversationToolCallConfirmationInterruptStartEvent( - type="uipath_cas_tool_call_confirmation", - value=UiPathConversationToolCallConfirmationValue( - **resume_trigger.api_resume.request - ), - ), - ), - ), - ), - ) - - event_data = interrupt_event.model_dump( - mode="json", exclude_none=True, by_alias=True - ) - if self._websocket_disabled: - logger.info( - f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}" - ) - else: - await self._client.emit("ConversationEvent", event_data) - except Exception as e: - logger.warning(f"Error sending interrupt event: {e}") + async def wait_for_tool_confirmation(self) -> dict[str, Any]: + """Wait for a confirmToolCall event to be received.""" + self._tool_confirmation_event.clear() + self._tool_confirmation_value = None - async def wait_for_resume(self) -> dict[str, Any]: - """Wait for the interrupt_end event to be received. + await self._tool_confirmation_event.wait() - Returns: - Resume data from the interrupt end event - """ - self._interrupt_end_event.clear() - self._interrupt_end_value = None - - await self._interrupt_end_event.wait() - - if self._interrupt_end_value: - return self._interrupt_end_value.model_dump(mode="python", by_alias=False) + if self._tool_confirmation_value: + return self._tool_confirmation_value.model_dump(mode="python", by_alias=False) return {} @property @@ -455,20 +395,14 @@ async def _handle_conversation_event( try: parsed_event = UiPathConversationEvent(**event) - if ( - parsed_event.exchange - and parsed_event.exchange.message - and parsed_event.exchange.message.interrupt - and parsed_event.exchange.message.interrupt.end - ): - interrupt = parsed_event.exchange.message.interrupt - - if interrupt.interrupt_id == self._interrupt_id: - logger.info( - f"Received endInterrupt for interrupt_id: {self._interrupt_id}" - ) - self._interrupt_end_value = interrupt.end - self._interrupt_end_event.set() + message = parsed_event.exchange.message if parsed_event.exchange else None + if message and message.tool_call and message.tool_call.confirm: + confirm = message.tool_call.confirm + logger.info( + f"Received confirmToolCall for tool_call_id: {message.tool_call.tool_call_id}, approved: {confirm.approved}" + ) + self._tool_confirmation_value = confirm + self._tool_confirmation_event.set() except Exception as e: logger.warning(f"Error parsing conversation event: {e}") diff --git a/packages/uipath/src/uipath/eval/models/_conversational_utils.py b/packages/uipath/src/uipath/eval/models/_conversational_utils.py index d4dbf0cdd..9e3523acc 100644 --- a/packages/uipath/src/uipath/eval/models/_conversational_utils.py +++ b/packages/uipath/src/uipath/eval/models/_conversational_utils.py @@ -168,7 +168,6 @@ def legacy_conversational_eval_input_to_uipath_message_list( role="user", content_parts=content_parts, tool_calls=[], - interrupts=[], created_at=timestamp, updated_at=timestamp, ) @@ -215,7 +214,6 @@ def legacy_conversational_eval_input_to_uipath_message_list( role="assistant", content_parts=content_parts, tool_calls=tool_calls, - interrupts=[], created_at=timestamp, updated_at=timestamp, ) @@ -259,7 +257,6 @@ def legacy_conversational_eval_input_to_uipath_message_list( role="user", content_parts=content_parts, tool_calls=[], - interrupts=[], created_at=timestamp, updated_at=timestamp, ) @@ -301,7 +298,6 @@ def legacy_conversational_eval_output_to_uipath_message_data_list( role="assistant", content_parts=content_parts, tool_calls=tool_calls, - interrupts=[], ) ) diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 93c875332..c8ab797c3 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.48" +version = "2.10.49" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2650,7 +2650,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.11" +version = "0.5.12" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, From 319a5e6aa058cb2d25ae11b6d9086918b86ae97f Mon Sep 17 00:00:00 2001 From: Josh Park <50765702+JoshParkSJ@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:46:54 -0400 Subject: [PATCH 2/2] fix: nitpicks --- .../uipath/src/uipath/_cli/_chat/_bridge.py | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/packages/uipath/src/uipath/_cli/_chat/_bridge.py b/packages/uipath/src/uipath/_cli/_chat/_bridge.py index 80e5e2b7a..2a382a59e 100644 --- a/packages/uipath/src/uipath/_cli/_chat/_bridge.py +++ b/packages/uipath/src/uipath/_cli/_chat/_bridge.py @@ -16,6 +16,7 @@ UiPathConversationMessageEvent, UiPathConversationToolCallConfirmationEvent, ) +from uipath.core.triggers import UiPathResumeTrigger from uipath.runtime.chat import UiPathChatProtocol from uipath.runtime.context import UiPathRuntimeContext @@ -122,7 +123,9 @@ def __init__( self._connected_event = asyncio.Event() self._tool_confirmation_event = asyncio.Event() - self._tool_confirmation_value: UiPathConversationToolCallConfirmationEvent | None = None + self._tool_confirmation_value: ( + UiPathConversationToolCallConfirmationEvent | None + ) = None self._current_message_id: str | None = None # Set CAS_WEBSOCKET_DISABLED when using the debugger to prevent websocket errors from @@ -265,6 +268,7 @@ async def emit_message_event( else: await self._client.emit("ConversationEvent", event_data) + # Store the current message ID, used for emitting interrupt events. self._current_message_id = message_event.message_id except Exception as e: @@ -355,7 +359,26 @@ async def emit_exchange_error_event(self, error: Exception) -> None: logger.error(f"Error sending exchange error event to WebSocket: {e}") raise RuntimeError(f"Failed to send exchange error event: {e}") from e - async def wait_for_tool_confirmation(self) -> dict[str, Any]: + async def emit_interrupt_event(self, resume_trigger: UiPathResumeTrigger): + """No-op. + + Tool confirmation — the only interrupt pattern CAS uses today — is + handled end-to-end via ``startToolCall`` with ``requireConfirmation: + true`` paired with ``wait_for_resume()``. This is deliberately + simpler than the old interrupt-based flow: CAS needs + ``requireConfirmation`` on the tool call event itself to render the + confirmation UI, so a parallel ``startInterrupt`` event would be + redundant. + + The only hypothetical reason to put work here is a generic, + non-tool-call agent interrupt (e.g. a coded agent calling + ``interrupt("do you want to continue?")``). Nothing uses that today + and it's not a near-term requirement — the method is kept for + generic flexibility. + """ + return None + + async def wait_for_resume(self) -> dict[str, Any]: """Wait for a confirmToolCall event to be received.""" self._tool_confirmation_event.clear() self._tool_confirmation_value = None @@ -363,7 +386,9 @@ async def wait_for_tool_confirmation(self) -> dict[str, Any]: await self._tool_confirmation_event.wait() if self._tool_confirmation_value: - return self._tool_confirmation_value.model_dump(mode="python", by_alias=False) + return self._tool_confirmation_value.model_dump( + mode="python", by_alias=False + ) return {} @property @@ -395,11 +420,14 @@ async def _handle_conversation_event( try: parsed_event = UiPathConversationEvent(**event) - message = parsed_event.exchange.message if parsed_event.exchange else None - if message and message.tool_call and message.tool_call.confirm: - confirm = message.tool_call.confirm + if ( + parsed_event.exchange + and parsed_event.exchange.message + and (tool_call := parsed_event.exchange.message.tool_call) + and (confirm := tool_call.confirm) + ): logger.info( - f"Received confirmToolCall for tool_call_id: {message.tool_call.tool_call_id}, approved: {confirm.approved}" + f"Received confirmToolCall for tool_call_id: {tool_call.tool_call_id}, approved: {confirm.approved}" ) self._tool_confirmation_value = confirm self._tool_confirmation_event.set()