Skip to content

feat(a2a): [3/5] call UiPath conversational agents as A2A tools [PRODEV-1742] - #1139

Draft
ionmincu wants to merge 1 commit into
mainfrom
feat/prodev-1742-conversational-a2a
Draft

ionmincu wants to merge 1 commit into
mainfrom
feat/prodev-1742-conversational-a2a

Conversation

@ionmincu

@ionmincu ionmincu commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

PR stack (PRODEV-1742), merge in this order

  1. [1/5] UiPath/Agents#6529: storage schema: conversationalAgent tool type, plus its process binding in the packager. Wait for the @uipath/agents-storage-schemas 1.44.0 and @uipath/tool-agent 2.2.0 (packager) releases.
  2. [2/5] feat(agent): [2/5] conversational agent tool config [PRODEV-1742] uipath-python#1931: runtime model: AgentToolType.CONVERSATIONAL_AGENT and AgentConversationalAgentToolResourceConfig. Wait for the uipath 2.14.37 PyPI release.
  3. [3/5] feat(a2a): [3/5] call UiPath conversational agents as A2A tools [PRODEV-1742] #1139: runtime tool: resolve the release and call AgentHub a2a/{folderKey}/{releaseId} over A2A. Run uv lock first, then wait for the uipath-langchain 0.18.33 release. ← this PR
  4. [4/5] UiPath/uipath-agents-python#800: runtime graph: build the A2A tool for conversationalAgent tools (never a job tool), bump dependencies. Run uv lock first, then deploy the runtime.
  5. [5/5] UiPath/flow-workbench#4734: Flow: pick conversational agents as tools, write the conversationalAgent tool resource. Can merge any time after [1/5] is released, with the flag off. First bump @uipath/agents-storage-schemas to ^1.44.0 and @uipath/tool-agent to ^2.2.0 (older packagers emit no process binding for this tool), and set the flag default back to false.

Turn on the Flow flag canvas.nodes.agent-tool-conversational-a2a only 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 a2a variant

An a2a resource without a slug makes older runtimes reject the whole agent (AgentA2aResourceConfig requires slug, and _normalize_resources keeps a2a). An unknown tool type goes through TOOL_MAP.get(t.lower(), "Unknown"), so older runtimes skip just that tool. A tool type also gets the solution process.<processName> binding from the packager, so @resource_override(process, process_name) takes effect. Precedent: AgentToolType.FLOW / FUNCTION.

  • The A2A entry points (create_a2a_tools_and_clients, open_a2a_tools, A2aClient, _create_a2a_tool) accept AgentConversationalAgentToolResourceConfig (from uipath-python#1931) next to AgentA2aResourceConfig, reading properties.process_name, folder_path and cached_agent_card. Client, send and trace span code is reused; URL resolution, labels and the conversation interface differ (see Conversation handles).
  • A conversational agent tool without processName loads, then errors at call time ("conversational agent tool '' has no processName") as an error ToolMessage.
  • requireConversationalConfirmation is applied per tool inside create_a2a_tools_and_clients(..., is_conversational=...) from the tool's own config, gated on the agent being conversational, like tool_factory does for other tools.
  • tool_factory._build_tool_for_resource returns None for the new resource (test added), so it never reaches the process/job path.
  • URL resolution (lazy, first call, like A2aClient.get()): the folder key comes from sdk.folders.retrieve_folder_key_async, trying the configured folder_path first and falling back to the execution folder (UIPATH_FOLDER_PATH) only when it is missing or raises FolderNotFoundException; the fallback logs a warning naming both folders. The release is read with GET /orchestrator_/odata/Releases?$filter=Name eq '...'&$select=Id,Name,ProcessType,IsConversational (quotes doubled) via sdk.api_client, and filtered client-side to ProcessType == 'Agent' and IsConversational 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 solution process.<processName> binding from the packager. The URL goes through resolve_service_url("agenthub_/a2a/{folderKey}/{releaseId}"), so UIPATH_SERVICE_URL_AGENTHUB is honored; otherwise it is {sdk base url}/agenthub_/a2a/{folderKey}/{releaseId}.
  • Errors are plain ValueErrors so the tool node turns them into error ToolMessages (an AgentRuntimeError would terminate the agent); a test drives this through the real tool-node error handling. Cases: no folder configured or executing; folder(s) not found; "'' in folder '' is not a published conversational agent". Graph construction never touches the network.
  • Labels: tool name is sanitize_tool_name(resource.name), description from the cached card or the resource description; metadata carries process_name instead of slug. Protocol version comes from the cached card, defaulting to 1.0 (AgentHub serves 0.3 and 1.0).
  • Bumps uipath to >=2.14.37, <2.15.0 and the package to 0.18.33.

Conversation handles (conversational agent tool only)

Remote A2A keeps A2aToolInput and its {task_id, context_id} state unchanged.

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 UiPathToolNode wrapper, every call opened a new child conversation. It also removes the terminal-task dead end: AgentHub's conversational endpoint has contextId = conversationId = taskId and 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; null means false). Both together returns invalid_arguments. The tool description says what omitting both does: the most recent open conversation may continue or a new one starts, so pass conversation_id to continue a specific one.

Result (ToolMessage content, JSON):

  • success: {"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: {"error": "conversation_closed" | "invalid_arguments" | "request_failed", "conversation_id": <id or null>, "message": <hint>}; the closed hint says to use new_conversation=true. Failures to resolve the process, folder or release (including a missing processName) are request_failed with the clear message, so they never abort an advanced-mode run.
  • A "terminal state" reply closes the conversation only when it is an A2A InvalidRequestError; any other error stays request_failed and 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_dicts replaces 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 clearing current if it is evicted). The earlier {task_id, context_id} entry is migrated into a one-conversation registry.

Resolution: new_conversation sends no IDs and becomes current. An explicit conversation_id is sent as the A2A context_id (with its task_id when known); a known closed one returns conversation_closed without a call, an unknown one is still sent (handles come from replayed history), and it becomes current only when there is none. With neither, current is used, or a new conversation starts. A terminal task closes the conversation and clears current.

Advanced mode: tools there run without a wrapper and tools_storage is 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 the conversation_id from a previous result (CAS replays tool results as ToolMessages, so the id survives across turns).

Concurrency: sends are serialized per conversation with an asyncio.Lock per conversation id (held per tool, so effectively (tool, cid)). The locks live in a WeakValueDictionary, so ids the model invents do not accumulate. Different conversations run in parallel.

The A2A span carries conversation_id when one is known.

Not done

uv.lock is not updated: uipath 2.14.37 is not on PyPI yet. Refresh it (uv lock) once #1931 is released. Tests were run locally against the uipath-python branch installed editable (no override committed).

Test plan

  • uv run pytest 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: 306 passed
  • ruff check ., ruff format --check ., python scripts/lint_httpx_client.py, mypy src plus the two touched test files: clean

🤖 Generated with Claude Code

@ionmincu ionmincu changed the title feat(a2a): call UiPath conversational agents as A2A tools [PRODEV-1742] feat(a2a): [3/5] call UiPath conversational agents as A2A tools [PRODEV-1742] Oct 7, 2026
@ionmincu
ionmincu force-pushed the feat/prodev-1742-conversational-a2a branch from f684ff6 to caf3bca Compare October 9, 2026 12:53
- Call an existing UiPath conversational agent as an A2A tool: resolve the
  folder key and the conversational release, then talk to AgentHub
  a2a/{folderKey}/{releaseId} through the existing A2A client.
- Accept the conversationalAgent tool resource next to the a2a resource; 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.
- tool_factory leaves the new resource to the A2A path (no job tool).
- Bump uipath to >=2.14.37 and the package to 0.18.33.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ionmincu
ionmincu force-pushed the feat/prodev-1742-conversational-a2a branch from caf3bca to 6e9c29a Compare October 9, 2026 15:40
@sonarqubecloud

sonarqubecloud Bot commented Oct 9, 2026

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

This branch has not been deployed

No deployments
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.

1 participant