feat: add client side tools to mapper and runtime [JAR-9629]#856
Open
norman-le wants to merge 5 commits into
Open
feat: add client side tools to mapper and runtime [JAR-9629]#856norman-le wants to merge 5 commits into
norman-le wants to merge 5 commits into
Conversation
|
There was a problem hiding this comment.
Pull request overview
Adds first-class support for “client-side tools” across agent construction, runtime tool discovery, and chat event mapping so the client SDK can execute certain tool calls and send results back through the interrupt/resume cycle.
Changes:
- Introduces a client-side tool factory that uses
@durable_interruptand marks tool/messages with a sharedIS_CONVERSATIONAL_CLIENT_SIDE_TOOLkey. - Enhances runtime tool discovery to extract client-side tool schemas from compiled graphs and exposes them to the message mapper.
- Updates
UiPathChatMessagesMapperto emitexecutingToolCallonly for server tools (no interrupts) and to suppressendToolCallfor client-side tool results; adds tests.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/uipath_langchain/agent/tools/client_side_tool.py |
Adds client-side tool creation, marker metadata, and client declaration validation/filtering. |
src/uipath_langchain/agent/tools/tool_factory.py |
Wires AgentClientSideToolResourceConfig to the new client-side tool factory. |
src/uipath_langchain/agent/tools/tool_node.py |
Preserves .tool reference through error-handling wrapper for client-side tools (for runtime discovery). |
src/uipath_langchain/runtime/runtime.py |
Adds graph tool iterator + client-side tool discovery; injects discovered info into the chat mapper. |
src/uipath_langchain/runtime/messages.py |
Emits executingToolCall only for non-interrupting tools; includes client-side schema fields on start events; suppresses client-side end events. |
src/uipath_langchain/chat/hitl.py |
Defines shared IS_CONVERSATIONAL_CLIENT_SIDE_TOOL marker and adds is_execution_phase flag for interrupts. |
src/uipath_langchain/agent/react/init_node.py |
Validates client-side tool declarations from exchange input and applies per-exchange availability filter. |
src/uipath_langchain/agent/react/agent.py |
Derives client-side tool schemas from built tools and passes them into init-node validation. |
tests/runtime/test_client_side_tool_discovery.py |
Adds integration-guard tests for client-side tool discovery through wrappers. |
tests/runtime/test_chat_message_mapper.py |
Adds tests for executingToolCall emission rules and client-side endToolCall suppression. |
Comment on lines
+50
to
+55
| declared = { | ||
| (t["name"] if isinstance(t, dict) else t): t | ||
| if isinstance(t, dict) | ||
| else {"name": t} | ||
| for t in declared_tools | ||
| } |
Comment on lines
+76
to
+78
| client_tools_input = getattr(state, UIPATH_CLIENT_SIDE_TOOLS_INPUT_KEY, None) | ||
| if client_tools_input is not None and isinstance(client_tools_input, list): | ||
| validate_and_apply_tool_filter(client_tools_input, client_side_tools) |
Comment on lines
+446
to
+451
| ( | ||
| client_tool_info.get("output_schema") | ||
| if isinstance(client_tool_info, dict) | ||
| else client_tool_info | ||
| ) | ||
| if is_client_side |
Comment on lines
+34
to
+110
| def validate_and_apply_tool_filter( | ||
| declared_tools: list[dict[str, Any]], | ||
| agent_tools: dict[str, ClientSideToolInfo], | ||
| ) -> None: | ||
| """Validate client-side tool declarations and set the availability filter. | ||
|
|
||
| Compares the client's declared tools against the agent's tool definitions. | ||
| Raises ValueError if required tools are missing or schemas don't match. | ||
| Sets the available_client_side_tools context variable for tool functions. | ||
|
|
||
| Args: | ||
| declared_tools: List of tool declarations from uipath__client_side_tools input. | ||
| Each item is a dict with 'name' and optional 'inputSchema'/'outputSchema'. | ||
| agent_tools: The agent's client-side tools. | ||
| Dict of {tool_name: ClientSideToolInfo}. | ||
| """ | ||
| declared = { | ||
| (t["name"] if isinstance(t, dict) else t): t | ||
| if isinstance(t, dict) | ||
| else {"name": t} | ||
| for t in declared_tools | ||
| } | ||
|
|
||
| required = set(agent_tools.keys()) | ||
| missing = required - set(declared.keys()) | ||
| if missing: | ||
| raise ValueError( | ||
| f"Missing required client-side tools: {', '.join(sorted(missing))}. " | ||
| f"The client must register handlers for all client-side tools defined by the agent." | ||
| ) | ||
|
|
||
| for name, decl in declared.items(): | ||
| agent_tool = agent_tools.get(name) | ||
| if agent_tool is None: | ||
| continue # Unknown tool, runtime will ignore it | ||
| if decl.get("inputSchema") and agent_tool.get("input_schema"): | ||
| if json.dumps(decl["inputSchema"], sort_keys=True) != json.dumps( | ||
| agent_tool["input_schema"], sort_keys=True | ||
| ): | ||
| raise ValueError( | ||
| f"Client-side tool '{name}' inputSchema does not match agent definition." | ||
| ) | ||
| if decl.get("outputSchema") and agent_tool.get("output_schema"): | ||
| if json.dumps(decl["outputSchema"], sort_keys=True) != json.dumps( | ||
| agent_tool["output_schema"], sort_keys=True | ||
| ): | ||
| raise ValueError( | ||
| f"Client-side tool '{name}' outputSchema does not match agent definition." | ||
| ) | ||
|
|
||
| available_client_side_tools.set(set(declared.keys())) | ||
|
|
||
|
|
||
| def create_client_side_tool( | ||
| resource: AgentClientSideToolResourceConfig, | ||
| ) -> StructuredTool: | ||
| """Create a client-side tool that pauses the graph and waits for the client to execute it. | ||
|
|
||
| The tool uses @durable_interrupt to suspend the graph. The client SDK receives | ||
| an executingToolCall event, runs its registered handler, and sends endToolCall | ||
| back through CAS. The bridge routes that endToolCall to wait_for_resume(), | ||
| which unblocks the graph with the client's result. | ||
| """ | ||
| tool_name = sanitize_tool_name(resource.name) | ||
| input_model = create_model_from_schema(resource.input_schema) | ||
|
|
||
| async def client_side_tool_fn( | ||
| *, tool_call_id: Annotated[str, InjectedToolCallId], **kwargs: Any | ||
| ) -> Any: | ||
| allowed = available_client_side_tools.get() | ||
| if allowed is not None and tool_name not in allowed: | ||
| return ToolMessage( | ||
| content=f"Tool '{tool_name}' is not available — the client has not registered a handler for it.", | ||
| tool_call_id=tool_call_id, | ||
| status="error", | ||
| ) | ||
|
|
| @@ -1,4 +1,4 @@ | |||
| from typing import Callable, Sequence, Type, TypeVar | |||
| from typing import Any, Callable, Sequence, Type, TypeVar | |||
| from uipath.runtime.schema import UiPathRuntimeSchema | ||
|
|
||
| from uipath_langchain.agent.tools.client_side_tool import ClientSideToolInfo | ||
| from uipath_langchain.agent.tools.tool_node import RunnableCallableWithTool |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Had to make new PR (old one here: #819)
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:

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