Skip to content

feat: add client side tools to mapper and runtime#819

Open
norman-le wants to merge 2 commits into
mainfrom
feat/jar-9629-client-side-tools-cas-urt
Open

feat: add client side tools to mapper and runtime#819
norman-le wants to merge 2 commits into
mainfrom
feat/jar-9629-client-side-tools-cas-urt

Conversation

@norman-le
Copy link
Copy Markdown
Contributor

@norman-le norman-le commented May 1, 2026

## uipath-python

- Added ClientSide tool type to agent model so the runtime can distinguish client-side tools from server tools
- Bridge emits executingToolCall event on durable interrupt when is_execution_phase marker is set, enabling the client to know when to execute the tool
- Bridge routes endToolCall from the client back to the agent, completing the interrupt/resume cycle

## uipath-langchain-python

- Client-side tool factory creates StructuredTool instances that use @durable_interrupt to pause the graph and wait for client execution
- @mockable wraps @durable_interrupt (as a separate outer function) so evals can simulate client-side tools without hitting GraphInterrupt
- CLIENT_SIDE_TOOL_MARKER defined in hitl.py as the single source of truth for identifying client-side tools across the codebase
- Error-handling wrapper (RunnableCallableWithTool) now preserves .tool reference for client-side tools, enabling runtime discovery
- Runtime discovers client-side tools from the compiled graph via shared _iter_graph_tools() iterator, used by both confirmation and client-side tool detection
- MessageMapper emits executingToolCall only for server tools without interrupts; client-side and confirmation tools get it from the bridge instead
- MessageMapper suppresses endToolCall for client-side tool results since the client already produced the result
- MessageMapper sets isClientSideTool and outputSchema on startToolCall events so the client knows to show the input form
- 9 new tests covering tool discovery, executingToolCall emission rules, and endToolCall suppression

## uipath-agents-python

- Graph builder strips CLIENT_SIDE_TOOL_MARKER during evals so @durable_interrupt is bypassed and @mockable can intercept

## AgentInterfaces

- CAS tracks is_client_side_tool on tool calls in the database and forwards client-originated endToolCall to the agent
- CAS validates client-side tool declarations against the agent definition and guards against unauthorized endToolCall for non-client-side tools
- React SDK listens for executingToolCall events and renders a form (ClientSideToolWidget) using the tool's output schema for the user to fill in
- ClientSideToolWidget reuses the existing ToolConfirmationFormContent component
- Added type schemas for executingToolCall events and client-side tool declarations

Videos in CAS localhost (Agent Builder changes already published, but this will be the debug experience as well when the sdk is upgraded):

Without tool confirmation for client side tool:

Screen.Recording.2026-05-13.at.9.32.26.PM.mov

With tool confirmation for client side tool:

Screen.Recording.2026-05-13.at.9.27.56.PM.mov

URT Eval:
image

Related to changes in other PRs:
CAS: https://github.com/UiPath/AgentInterfaces/pull/949
uipath-langchain-python (tool definition): #819
uipath-python (bridge changes): UiPath/uipath-python#1609
uipath-agents-python (skipping for evals, so it can be mocked): https://github.com/UiPath/uipath-agents-python/pull/485
Another Agents PR fixing some issues with debug: https://github.com/UiPath/Agents/pull/5250

Copilot AI review requested due to automatic review settings May 1, 2026 21:16
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class support for “client-side tools” (tools executed by the client SDK) so the runtime can advertise them in tool-call start events and adjust tool-call lifecycle events accordingly.

Changes:

  • Introduces a new client_side_tool factory that suspends execution via @durable_interrupt and resumes with client-provided results.
  • Extends tool creation to support AgentClientSideToolResourceConfig.
  • Enhances runtime + message mapping to (a) discover client-side tools/output schemas from the compiled graph, (b) include client-side metadata in tool-call start events, (c) emit executingToolCall for server-side tools without confirmation, and (d) suppress endToolCall events for client-side tools.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/uipath_langchain/runtime/runtime.py Discovers client-side tools from compiled graph nodes and passes them into the message mapper.
src/uipath_langchain/runtime/messages.py Adds client-side tool metadata to tool-call start events, emits executingToolCall for non-confirmation server tools, suppresses endToolCall for client-side tools.
src/uipath_langchain/chat/hitl.py Marks confirmation interrupts as “execution phase” triggers for server-side tools (not client-side).
src/uipath_langchain/agent/tools/tool_factory.py Wires new client-side tool resource config into tool creation.
src/uipath_langchain/agent/tools/client_side_tool.py Implements the client-side tool behavior using @durable_interrupt + ToolMessage result annotation.

Comment on lines 71 to 73
self.chat.tools_requiring_confirmation = self._get_tool_confirmation_info()
self.chat.client_side_tools = self._get_client_side_tools()
self._middleware_node_names: set[str] = self._detect_middleware_nodes()
Comment on lines +458 to +462
# Emit executingToolCall from MessageMapper since there's no durable interrupt
# to trigger it from the runtime loop.
if not require_confirmation and not is_client_side:
events.append(
UiPathConversationMessageEvent(
Comment on lines +505 to +510
# Suppress endToolCall for client-side tools — the client already has the result (it produced it).
is_client_side = message.response_metadata.get("uipath_client_tool", False)
events: list[UiPathConversationMessageEvent] = []

if not is_client_side:
events.append(
Comment thread src/uipath_langchain/chat/hitl.py Outdated
Comment on lines +129 to +132
# If this is a server-side tool (not client-side), execution follows immediately
# after confirmation — mark this as the execution trigger so the bridge emits
# executingToolCall. For client-side tools, the execution interrupt sets this instead.
is_execution_trigger = not (tool.metadata or {}).get("uipath_client_tool", False)

from .utils import sanitize_tool_name

logger = getLogger(__name__)

content = str(output) if output is not None else ""
if isinstance(output, dict):
content = json.dumps(output)
Comment on lines +125 to +127
elif isinstance(resource, AgentClientSideToolResourceConfig):
return create_client_side_tool(resource)

params = [p for p in original_sig.parameters.values() if p.name != "kwargs"] + [
inspect.Parameter("kwargs", inspect.Parameter.VAR_KEYWORD, annotation=Any),
]
client_side_tool_fn.__signature__ = original_sig.replace(parameters=params)
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.

Hm a bit confused what this is for

output=content_value,
is_error=message.status == "error",
# Suppress endToolCall for client-side tools — the client already has the result (it produced it).
is_client_side = message.response_metadata.get("uipath_client_tool", False)
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.

use CLIENT_SIDE_TOOL_MARKER

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 90%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants