Skip to content
Open
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: 2 additions & 0 deletions packages/uipath-core/src/uipath/core/chat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
UiPathSessionStartEvent,
)
from .tool import (
UiPathConversationExecutingToolCallEvent,
UiPathConversationToolCall,
UiPathConversationToolCallConfirmation,
UiPathConversationToolCallConfirmationData,
Expand Down Expand Up @@ -157,6 +158,7 @@
"UiPathConversationCitationData",
"UiPathConversationCitation",
# Tool
"UiPathConversationExecutingToolCallEvent",
"UiPathConversationToolCallStartEvent",
"UiPathConversationToolCallEndEvent",
"UiPathConversationToolCallConfirmation",
Expand Down
17 changes: 17 additions & 0 deletions packages/uipath-core/src/uipath/core/chat/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class UiPathConversationToolCallStartEvent(BaseModel):
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")
is_client_side_tool: bool | None = Field(None, alias="isClientSideTool")
output_schema: Any | None = Field(None, alias="outputSchema")

model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)

Expand All @@ -43,6 +45,18 @@ class UiPathConversationToolCallEndEvent(BaseModel):
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)


class UiPathConversationExecutingToolCallEvent(BaseModel):
"""Signals the client that the tool is about to be executed.
Emitted in all paths. For client-side tools, the client should begin
executing its handler upon receiving this event."""

tool_name: str = Field(..., alias="toolName")
timestamp: str | None = None
input: dict[str, Any] | None = None

model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)


class UiPathConversationToolCallConfirmationEvent(BaseModel):
"""Signals a tool call confirmation (approve/reject) from the client."""

Expand Down Expand Up @@ -82,6 +96,9 @@ class UiPathConversationToolCallEvent(BaseModel):
confirm: UiPathConversationToolCallConfirmationEvent | None = Field(
None, alias="confirmToolCall"
)
executing: UiPathConversationExecutingToolCallEvent | None = Field(
None, alias="executingToolCall"
)
meta_event: dict[str, Any] | None = Field(None, alias="metaEvent")
error: UiPathConversationErrorEvent | None = Field(None, alias="toolCallError")

Expand Down
67 changes: 44 additions & 23 deletions packages/uipath/src/uipath/_cli/_chat/_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
UiPathConversationEvent,
UiPathConversationExchangeEndEvent,
UiPathConversationExchangeEvent,
UiPathConversationExecutingToolCallEvent,
UiPathConversationMessageEvent,
UiPathConversationToolCallConfirmationEvent,
UiPathConversationToolCallEndEvent,
UiPathConversationToolCallEvent,
)
from uipath.core.triggers import UiPathResumeTrigger
from uipath.runtime.chat import UiPathChatProtocol
Expand Down Expand Up @@ -124,7 +127,9 @@ def __init__(

self._tool_confirmation_event = asyncio.Event()
self._tool_confirmation_value: (
UiPathConversationToolCallConfirmationEvent | None
UiPathConversationToolCallConfirmationEvent
| UiPathConversationToolCallEndEvent
| None
) = None
Comment on lines 128 to 133
self._current_message_id: str | None = None

Expand Down Expand Up @@ -360,23 +365,39 @@ async def emit_exchange_error_event(self, error: Exception) -> None:
raise RuntimeError(f"Failed to send exchange error event: {e}") from e

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.
"""Emit an executingToolCall event if the trigger is marked with is_execution_phase.

Called by the runtime loop for every durable interrupt. Emits executingToolCall
for triggers that signal execution is about to begin, The
is_execution_phase marker ensures it fires exactly once per tool call.
"""
return None
request = (
resume_trigger.api_resume.request if resume_trigger.api_resume else None
)
if not request or not isinstance(request, dict):
return

if not request.get("is_execution_phase"):
return

tool_call_id = request.get("tool_call_id")
tool_name = request.get("tool_name")
tool_input = request.get("input")

if not tool_call_id or not tool_name:
return

executing_event = UiPathConversationMessageEvent(
message_id=self._current_message_id,
tool_call=UiPathConversationToolCallEvent(
tool_call_id=tool_call_id,
executing=UiPathConversationExecutingToolCallEvent(
tool_name=tool_name,
input=tool_input,
),
Comment on lines +383 to +397
),
)
await self.emit_message_event(executing_event)

async def wait_for_resume(self) -> dict[str, Any]:
"""Wait for a confirmToolCall event to be received."""
Expand Down Expand Up @@ -424,13 +445,13 @@ async def _handle_conversation_event(
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: {tool_call.tool_call_id}, approved: {confirm.approved}"
)
self._tool_confirmation_value = confirm
self._tool_confirmation_event.set()
if confirm := tool_call.confirm:
self._tool_confirmation_value = confirm
self._tool_confirmation_event.set()
elif end := tool_call.end:
self._tool_confirmation_value = end
self._tool_confirmation_event.set()
except Exception as e:
logger.warning(f"Error parsing conversation event: {e}")

Expand Down
12 changes: 12 additions & 0 deletions packages/uipath/src/uipath/agent/models/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ class AgentToolType(str, CaseInsensitiveEnum):
INTEGRATION = "Integration"
INTERNAL = "Internal"
IXP = "Ixp"
CLIENT_SIDE = "ClientSide"
UNKNOWN = "Unknown" # fallback branch discriminator


Expand Down Expand Up @@ -889,6 +890,15 @@ class AgentInternalToolResourceConfig(BaseAgentToolResourceConfig):
)


class AgentClientSideToolResourceConfig(BaseAgentToolResourceConfig):
"""Resource config for client-side tools executed by the client SDK."""

type: Literal[AgentToolType.CLIENT_SIDE] = AgentToolType.CLIENT_SIDE
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we add an example maybe in agentinterfaces what an example client side tool would look like?

properties: BaseResourceProperties = Field(default_factory=BaseResourceProperties)
output_schema: Optional[Dict[str, Any]] = Field(None, alias="outputSchema")
arguments: Optional[Dict[str, Any]] = Field(default_factory=dict)

Comment on lines +893 to +900

class AgentUnknownToolResourceConfig(BaseAgentToolResourceConfig):
"""Fallback for unknown tool types (parent normalizer sets type='Unknown')."""

Expand All @@ -902,6 +912,7 @@ class AgentUnknownToolResourceConfig(BaseAgentToolResourceConfig):
AgentIntegrationToolResourceConfig,
AgentInternalToolResourceConfig,
AgentIxpExtractionResourceConfig,
AgentClientSideToolResourceConfig,
AgentUnknownToolResourceConfig, # when parent sets type="Unknown"
],
Field(discriminator="type"),
Expand Down Expand Up @@ -1276,6 +1287,7 @@ def _normalize_resources(v: Dict[str, Any]) -> None:
"integration": "Integration",
"internal": "Internal",
"ixp": "Ixp",
"clientside": "ClientSide",
"unknown": "Unknown",
}
CONTEXT_MODE_MAP = {
Expand Down
Loading
Loading