Skip to content

feat(a2a): [3/5] single-conversation mode for the conversational agent tool [PRODEV-1742] - #1175

Draft
ionmincu wants to merge 2 commits into
mainfrom
feat/prodev-1742-conversation-mode
Draft

ionmincu wants to merge 2 commits into
mainfrom
feat/prodev-1742-conversation-mode

Conversation

@ionmincu

@ionmincu ionmincu commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

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

  1. [1/5] UiPath/Agents: schema settings.conversationMode (single | multiple, absent = single). https://github.com/UiPath/Agents/pull/6761
  2. [2/5] UiPath/uipath-python: settings model field. feat(agent): [2/5] conversationMode on the conversational agent tool settings [PRODEV-1742] uipath-python#1955
  3. [3/5] UiPath/uipath-langchain-python: single-mode tool + history-based continuation. feat(a2a): [3/5] single-conversation mode for the conversational agent tool [PRODEV-1742] #1175
  4. [4/5] UiPath/uipath-agents-python: pin bump. https://github.com/UiPath/uipath-agents-python/pull/826
  5. [5/5] UiPath/flow-workbench: "Allow multiple conversations" toggle (default off = single). Needs the [1/5] schema release. https://github.com/UiPath/flow-workbench/pull/5390

Problem

The conversational agent tool always showed the model conversation_id and new_conversation, and its state lives in tools_storage, which is empty at the start of every job. Every chat message of a low-code conversational agent is a new job, so a call with no id started a new conversation even though the hint says it continues the most recent one. Most agents only ever want one conversation with the tool.

What this does

  1. Resolves the mode as config.settings.conversation_mode or "single" (needs [2/5]).

  2. single (default): the tool takes only message (A2aSingleConversationInput, no conversation_id, no new_conversation) and its description adds "Each call continues one ongoing conversation with this agent. Make one call at a time." Every call continues the tool's conversation, within a job, across jobs, and in advanced mode. When that conversation has ended (terminal task state) or AgentHub no longer has it (TaskNotFound / terminal-state error), a new one is started and the result carries a note. If the request itself hit the unavailable conversation, the same message is sent once to the new one, because AgentHub refused it without acting on it. Every single-mode note ends with "The agent has no memory of earlier messages; if your message depends on earlier context, send it again with that context." The model never sees conversation_closed. The result still includes conversation_id, which the history rebuild reads.

  3. multiple: the same arguments, description, error JSON shapes and per-tool behaviour as before (its note text is unchanged).

  4. Cross-message continuity (both modes). When a call starts and the tool state has no current conversation, it is rebuilt from the replayed messages (_conversation_id_from_history in conversational_agent_tool.py). It walks state.messages newest to oldest over this tool's results, matched through the nearest preceding AIMessage.tool_calls entry (tool_call_id to tool name), with ToolMessage.name only as a fallback because replay does not set it. Each result is read as:

    • no conversation (a status="error" message, a plain error string, a cancelled confirmation {"meta": ...}, an interrupted call with empty content, invalid JSON): skipped, so the walk continues to the previous result;
    • a failed request (request_failed): its conversation_id counts, because the conversation is still alive;
    • an ended conversation (task_state of completed / failed / canceled / rejected, or conversation_closed): remembered as ended and skipped, along with any older result of the same conversation;
    • otherwise its conversation is seeded as open and current.

    A result wrapped by the conversational-confirmation "args modified" envelope is unwrapped. A conversation the state already knows (for instance one that ended in this job) is never reopened from history.

  5. Advanced (deepagents) mode. The tool coroutine takes messages: Annotated[Sequence[AnyMessage], InjectedState("messages")], which the deepagents tool node injects, and derives the current conversation from those messages exactly as in 4. The injected argument is declared on the function, not on args_schema, so the model-facing schema stays message only (checked with convert_to_openai_tool). No closure-level registry, so cached tool instances carry nothing between runs. Verified end to end with a real create_advanced_agent graph.

Changes beyond the title

  • Absent now means single. Existing alpha agent.json files without conversationMode behaved as multiple (the model saw conversation_id and new_conversation); once this is released they expose only message. In single mode a conversation_id or new_conversation that the model still sends (for example copied from earlier turns) is ignored: the ReAct wrapper logs it at info level; in advanced mode the tool schema drops extra arguments before the tool runs, so nothing is logged there.
  • Advanced multiple mode now continues on a call with no id (it used to always start a new conversation, because nothing was stored). It reads the history like the ReAct path, which is what the existing hint promises. Called outside a graph, with no messages, it still starts a new one every time. Tests: test_advanced_multiple_mode_reads_the_history_and_honors_the_handles, test_conversational_stateless_no_args_starts_a_new_conversation_each_time (unchanged).
  • The _converse tail is factored into _record_reply (shared with single mode), with identical behaviour. The test helpers default the resource to multiple, since the existing tests exercise the handle arguments.

Known limits

  • Parallel calls. Two parallel calls to the same single-mode tool in one model turn, before any conversation exists, open two conversations: both see the same state, and in advanced mode the injected messages is a snapshot, so a lock would not help. The description asks the model to make one call at a time. There is no per-conversation lock in main today, concurrent sends are allowed.
  • Subagents. deepagents subagents start from their task prompt only (messages is in deepagents' _EXCLUDED_STATE_KEYS), so a subagent's first call opens its own conversation and later calls in that subagent continue it.
  • Ended before this job. A conversation whose last result in the history shows it ended is not reopened, so the next call opens a new one; unlike the in-job case, that call carries no note.

After the releases

uipath>=2.14.44 is not on PyPI until [2/5] is released. pyproject.toml and the version and specifier lines of uv.lock are updated, but the locked uipath entry still resolves 2.14.40, which lacks AgentConversationalAgentToolSettings, so CI fails until then. Once [2/5] has published 2.14.44, run uv lock --upgrade-package uipath and commit uv.lock.

Verification

  • pytest on test_conversational_agent_tool.py (163), test_a2a_tool.py, test_tool_factory.py, test_a2a_sdk_v1_migration.py, tests/agent/advanced and test_no_circular_imports.py: 592 passed. The new tests failed before the change. They include a round trip through the real UiPathChatMessagesMapper (AIMessage and ToolMessage to CAS events, replayed back), with names that need sanitising.
  • tests/agent as a whole (first pass): only 4 failures, none related: a Windows symlink privilege and uip CLI lookups (test_create_file_tool, test_uipath_cli_tool).
  • Local venv built against the uipath-python branch of [2/5] (editable, not committed).
  • ruff check, ruff format --check, mypy --config-file pyproject.toml . (424 files), scripts/lint_httpx_client.py: clean.

🤖 Generated with Claude Code

…t tool [PRODEV-1742]

Resolve settings.conversation_mode (absent = "single") for the conversational agent tool.

single: the model passes only a message and every call continues the tool's one conversation. A
conversation that ended or that AgentHub no longer has is replaced by a new one, the message is
sent again to it, and the result says so in "note".

multiple: the existing conversation_id / new_conversation arguments and error results.

Both modes rebuild the current conversation from the replayed history when the tool state has none:
each chat message is a new job with empty tools_storage, but the earlier results of this tool are
in the messages. In advanced mode the graph's messages are injected into the tool (InjectedState)
and read the same way.

Bump to 0.18.50 and the uipath floor to 2.14.44.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e history [PRODEV-1742]

The history rebuild stopped at the tool's last result even when it said nothing about a live
conversation, so one failed request, a cancelled confirmation or an interrupted call at the end of
a chat message dropped the thread, while a conversation whose task had ended was seeded as open.
It now walks the tool's results newest first: results without a conversation are skipped, a failed
request still gives its conversation, and a conversation that a later result shows as ended
(terminal task_state, or conversation_closed) is not reopened.

Single mode:
- the description adds "Make one call at a time.", since a parallel call cannot see the other's
  conversation;
- its notes now say the agent has no memory of earlier messages;
- an info log records conversation_id / new_conversation arguments that it ignores.

Tests cover the orderings and a round trip through the real chat message mapper, with names that
need sanitising.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@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

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