Repository navigation
Conversation
ionmincu
force-pushed
the
feat/prodev-1742-conversational-a2a
branch
2 times, most recently
from
October 9, 2026 15:40
caf3bca to
6e9c29a
Compare
radugheo
reviewed
Oct 12, 2026
| Tuple of (response_text, task_state, new_task_id, new_context_id). | ||
| """ | ||
| try: | ||
| return await _send_a2a_message_or_raise( |
Collaborator
There was a problem hiding this comment.
I think it's confusing to have both _send_a2a_message_or_raise and _send_a2a_message, especially since _send_a2a_message seems to just wrap the first one in a try-except block to catch the errors.
ionmincu
force-pushed
the
feat/prodev-1742-conversational-a2a
branch
from
October 12, 2026 15:00
6e9c29a to
122bc72
Compare
- New conversational_agent_tool module next to a2a_tool: call an existing
UiPath conversational agent as an A2A tool. ConversationalAgentA2aClient
(same get()/dispose() lifecycle as A2aClient) resolves the conversational
release by process name in the execution folder, like process tools
(properties.folderPath is not used; a solution process binding overrides the
name and the folder), then talks to AgentHub a2a/{folderKey}/{releaseId}.
- The Remote A2A code in a2a_tool.py is untouched; the new module only imports
its read-only helpers (card description, protocol version, text and task
state).
- create_conversational_agent_tools_and_clients and
open_conversational_agent_tools take the conversationalAgent tool resources;
a missing processName errors at call time instead of rejecting the agent, and
requireConversationalConfirmation is applied per tool.
- Address conversations by conversation_id / new_conversation with a capped
per-tool registry, per-conversation locks and terminal-state handling;
advanced mode honors the same arguments without a tool wrapper. Request
errors, including resolving the client, come back as a request_failed result.
- tool_factory leaves the new resource to this module (no job tool).
- Bump uipath to >=2.14.37 and the package to 0.18.34, and refresh uv.lock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ionmincu
force-pushed
the
feat/prodev-1742-conversational-a2a
branch
from
October 12, 2026 15:59
122bc72 to
4594e3b
Compare
|
This branch has not been deployed
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.



PR stack (PRODEV-1742), merge in this order
conversationalAgenttool type, plus itsprocessbinding in the packager. Wait for the@uipath/agents-storage-schemas1.44.0 and@uipath/tool-agent2.2.0 (packager) releases.AgentToolType.CONVERSATIONAL_AGENTandAgentConversationalAgentToolResourceConfig. Wait for the uipath 2.14.37 PyPI release.a2a/{folderKey}/{releaseId}over A2A. Runuv lockfirst, then wait for the uipath-langchain 0.18.34 release. ← this PRconversationalAgenttools (never a job tool), bump dependencies. Runuv lockfirst, then deploy the runtime.conversationalAgenttool resource. Can merge any time after [1/5] is released, with the flag off. First bump@uipath/agents-storage-schemasto^1.44.0and@uipath/tool-agentto^2.2.0(older packagers emit noprocessbinding for this tool), and set the flag default back tofalse.Turn on the Flow flag
canvas.nodes.agent-tool-conversational-a2aonly after [4/5] is deployed.Summary
Lets the agents runtime call an existing UiPath conversational agent as an A2A tool (Jira PRODEV-1742).
Why a tool type, not an
a2avariantAn
a2aresource without aslugmakes older runtimes reject the whole agent (AgentA2aResourceConfigrequiresslug, and_normalize_resourceskeepsa2a). An unknown tooltypegoes throughTOOL_MAP.get(t.lower(), "Unknown"), so older runtimes skip just that tool. A tool type also gets the solutionprocess.<processName>binding from the packager, so@resource_override(process, process_name)takes effect. Precedent:AgentToolType.FLOW/FUNCTION.a2a_tool.pyandtest_a2a_tool.pyare identical to main (git diff origin/mainon both is empty). The conversational agent tool is its own module,agent/tools/a2a/conversational_agent_tool.py, which takesAgentConversationalAgentToolResourceConfig(from uipath-python#1931) and readsproperties.process_nameandcached_agent_card:ConversationalAgentA2aClient: sameget()/dispose()lifecycle asA2aClient.get()resolves the URL with_resolve_conversational_a2a_url(sdk, process_name, get_execution_folder_path()), then builds the A2A client; that block mirrorsA2aClient.get()(a short duplicate, soA2aClientneeds no hooks)._send_message: one send function that raises. It reportsinput_requiredfor a plain message reply (AgentHub's conversational reply shape)._invoke: opens the trace span (the Remote A2A attributes plusconversation_id), resolves the client, sends, and maps failures, including resolving the client, to_A2aRequestErrorwith the typed cause kept for terminal-state detection. No mode flag and no try/except wrapper functions.create_conversational_agent_tools_and_clients(resources, *, is_conversational)andopen_conversational_agent_tools, exported fromuipath_langchain.agent.tools.a2aanduipath_langchain.agent.tools.a2a_tool:A2aStructuredToolWithWrapper,_build_description,_select_protocol_version,_extract_text,_task_state_name,_TERMINAL_TASK_STATES. The cached-card builder is duplicated because the Remote one reads the cached card from the Remote resource, not fromproperties.processNameloads, then errors at call time ("conversational agent tool '' has no processName") as an error ToolMessage.requireConversationalConfirmationis applied per tool insidecreate_conversational_agent_tools_and_clients(..., is_conversational=...)from the tool's own config, gated on the agent being conversational, liketool_factorydoes for other tools.tool_factory._build_tool_for_resourcereturnsNonefor the new resource (test added), so it never reaches the process/job path; the new module builds it.ConversationalAgentA2aClient.get(), likeA2aClient.get()): the folder is the execution folder (get_execution_folder_path(), i.e.UIPATH_FOLDER_PATH), as for process and agent tools and the Remote A2A path;properties.folderPathis not used. A solutionprocess.<processName>binding takes precedence:@resource_overrideremaps both the process name and the folder, the same way it does forclient.processes.invoke_asyncinprocess_tool.py. The folder key comes fromsdk.folders.retrieve_folder_key_asyncfor that one folder, with no fallback. The release is read withGET /orchestrator_/odata/Releases?$filter=Name eq '...'&$select=Id,Name,ProcessType,IsConversational(quotes doubled) viasdk.api_client, and filtered client-side toProcessType == 'Agent'andIsConversational is True, since I could not confirm server-side filtering. The SDK has no releases service, so this is a small private helper decorated with@resource_override(resource_type="process", resource_identifier="process_name"); as a tool resource it gets the solutionprocess.<processName>binding from the packager. The URL goes throughresolve_service_url("agenthub_/a2a/{folderKey}/{releaseId}"), soUIPATH_SERVICE_URL_AGENTHUBis honored; otherwise it is{sdk base url}/agenthub_/a2a/{folderKey}/{releaseId}.ValueErrors so the tool node turns them into error ToolMessages (anAgentRuntimeErrorwould terminate the agent); a test drives this through the real tool-node error handling. Cases: no execution folder set; folder not found (names the process and the folder); "'' in folder '' is not a published conversational agent". Graph construction never touches the network.sanitize_tool_name(resource.name), description from the cached card or the resource description; metadata carriesprocess_nameinstead ofslug. Protocol version comes from the cached card, defaulting to1.0(AgentHub serves 0.3 and 1.0).uipathto>=2.14.37, <2.15.0and the package to0.18.34(main is at 0.18.33).Conversation handles (conversational agent tool only)
Remote A2A keeps
A2aToolInputand its{task_id, context_id}state, because its code is not touched.Why: the tool used to hold one hidden conversation, so the model could not start a fresh one or run two in parallel, and in advanced (deepagents) mode, where tools run without a
UiPathToolNodewrapper, every call opened a new child conversation. It also removes the terminal-task dead end: AgentHub's conversational endpoint hascontextId = conversationId = taskIdand rejects messages to a terminal task, which used to fail every later call.Input (
A2aConversationInput):message(required),conversation_id(continue that conversation),new_conversation(start a separate one;nullmeans false). Both together returnsinvalid_arguments. The tool description says what omitting both does: the most recent open conversation may continue or a new one starts, so passconversation_idto continue a specific one.Result (ToolMessage content, JSON):
{"agent_response", "conversation_id", "task_state"}, plus"note"("The previous conversation was not found; a new conversation was started.") when AgentHub answers with a different id than the one sent.{"error": "conversation_closed" | "invalid_arguments" | "request_failed", "conversation_id": <id or null>, "message": <hint>}; the closed hint says to usenew_conversation=true. Failures to resolve the process, folder or release (including a missingprocessName) arerequest_failedwith the clear message, so they never abort an advanced-mode run.InvalidRequestError; any other error staysrequest_failedand keeps the state.State (ReAct wrapper):
tools_storage[tool.name] = {"current": cid | None, "conversations": {cid: {"task_id", "state": "open" | "closed", "last_used": <counter>}}}. The whole entry is replaced per call (not merged by conversation), which is what lets eviction and closing propagate;merge_dictsreplaces per top-level key, so this is safe. ReAct dispatches one tool call per node visit, so each call sees the previously committed entry and nothing is lost. Capped at 20, evicting the least recently used, closed ones first (and clearingcurrentif it is evicted). The earlier{task_id, context_id}entry is migrated into a one-conversation registry.Resolution:
new_conversationsends no IDs and becomescurrent. An explicitconversation_idis sent as the A2Acontext_id(with itstask_idwhen known); a known closed one returnsconversation_closedwithout a call, an unknown one is still sent (handles come from replayed history), and it becomescurrentonly when there is none. With neither,currentis used, or a new conversation starts. A terminal task closes the conversation and clearscurrent.Advanced mode: tools there run without a wrapper and
tools_storageis not used. Tool instances outlive a run (the runtime factory caches compiled graphs), so nothing is stored: a call with neither argument always starts a new conversation, and the model continues one by passing theconversation_idfrom a previous result (CAS replays tool results as ToolMessages, so the id survives across turns).Concurrency: sends are serialized per conversation with an
asyncio.Lockper conversation id (held per tool, so effectively(tool, cid)). The locks live in aWeakValueDictionary, so ids the model invents do not accumulate. Different conversations run in parallel.The A2A span carries
conversation_idwhen one is known.Lock
uv.lockis refreshed now thatuipath 2.14.37is on PyPI (uipath 2.14.35 -> 2.14.37 and the package version only);uv sync --lockedpasses.Test plan
uv run pytest tests/agent/tools/test_conversational_agent_tool.py tests/agent/tools/test_a2a_tool.py tests/agent/tools/test_a2a_sdk_v1_migration.py tests/agent/tools/test_tool_factory.py tests/test_no_circular_imports.py: 313 passed. The conversational tests moved totest_conversational_agent_tool.py, with new ones for_send_message, client disposal,open_conversational_agent_tools, a failed request marking the span as an error, and a name-onlyprocess.<name>binding resolving without an execution folder.uv run pytest tests/agent/tools/ tests/test_no_circular_imports.py: 1401 passed. Fourinternal_toolstests fail on my Windows machine only (symlink privilege,uip.cmd shim); they do not touch this change.ruff check .,ruff format --check .,python scripts/lint_httpx_client.py,mypy --config-file pyproject.toml .: cleanuv sync --lockedanduv sync --locked --all-extras: pass🤖 Generated with Claude Code