Skip to content

feat: confirmToolCall support — startToolCall with requireConfirmation + ToolRunnableCallable [JAR-9208]#703

Open
JoshParkSJ wants to merge 20 commits intomainfrom
josh/cas-tc-fix
Open

feat: confirmToolCall support — startToolCall with requireConfirmation + ToolRunnableCallable [JAR-9208]#703
JoshParkSJ wants to merge 20 commits intomainfrom
josh/cas-tc-fix

Conversation

@JoshParkSJ
Copy link
Copy Markdown
Contributor

@JoshParkSJ JoshParkSJ commented Mar 17, 2026

What changed?

Moves tool confirmation from the old interrupt-based flow to the new confirmToolCall event on startToolCall.

https://uipath.atlassian.net/browse/JAR-9208

How tool confirmation discovery works

  1. Graph build_wrap_tool_error_handling wraps confirmation tools with ConversationalToolRunnableCallable (preserves .tool reference to the underlying BaseTool)
  2. Runtime init_get_tool_confirmation_info walks the compiled graph nodes, finds ConversationalToolRunnableCallable nodes, reads their .tool.metadata, builds {tool_name: input_schema} dict
  3. Streaming — mapper sees a tool call in an AIMessage, checks the dict, emits startToolCall with requireConfirmation: true and inputSchema
  4. Tool execution — tool node runs (after the event was already sent to CAS)

The key constraint: CAS needs requireConfirmation on the startToolCall event itself so it can render a confirmation modal instead of a spinner. This means the runtime must know which tools need confirmation before any tool node executes.

Changes

hitl.py — simplified HITL approval flow:

  • @durable_interrupt in ask_confirmation() now emits a plain dict instead of the old UiPathConversationToolCallConfirmationValue interrupt model
  • Resume payloads use the confirmToolCall shape directly: {"approved": bool, "input": ...} (no more dual-payload parsing for the legacy {type, value} wrapper)
  • @requires_approval decorator now sets REQUIRE_CONVERSATIONAL_CONFIRMATION metadata on the tool, making coded agents discoverable by the runtime
  • get_confirmation_schema() extracted to hitl.py — single place for "does this tool need confirmation?" logic, used by both runtime.py (graph crawling) and tool_node.py (graph build)

tool_node.pyConversationalToolRunnableCallable:

  • wrap_tools_with_error_handling replaces each tool node with a RunnableCallable, which loses the original BaseTool reference
  • For tools with REQUIRE_CONVERSATIONAL_CONFIRMATION metadata, it uses ConversationalToolRunnableCallable instead — a typed subclass that preserves .tool
  • Normal tools get a plain RunnableCallable (no overhead)

messages.py — fix duplicate tool names in streaming + startToolCall always emitted:

  • Bug fix: AIMessageChunk accumulation was doubling tool names (e.g. "search_web""search_websearch_web"). Root cause: when the first streamed chunk arrives, it's assigned to self.current_message. If that chunk contains tool_call_chunks, the accumulation line (self.current_message + message) adds the chunk to itself, and LangChain's merge_lists concatenates string fields. The fix skips accumulation when self.current_message is message (identity check).

  • Why this only surfaced now: OpenAI (ChatOpenAI) populates tool_call_chunks with the tool name on the very first streamed chunk. Claude (ChatAnthropic / UiPathChat) sends an empty first chunk and delivers tool call data in subsequent chunks — so the self-accumulation path was never triggered. Verified empirically with all four combinations:

    OpenAI Claude
    Coded agent (create_agent) First chunk has tool name → bug triggered First chunk empty → no issue
    Low-code agent (UiPath) N/A (uses UiPathChat) First chunk empty → no issue
  • startToolCall is now always emitted for confirmation tools (previously skipped). CAS reads requireConfirmation and inputSchema from this event to render the confirmation UI

  • map_tool_call_to_tool_call_start_event() accepts require_confirmation and input_schema kwargs

runtime.py — confirmation metadata discovery:

  • _get_tool_confirmation_info() walks compiled graph nodes at init to build the {tool_name: input_schema} lookup
  • Supports both coded agents (one tool per node via .tool) and low-code agents (multiple tools per node via .tools_by_name)

Tests

  • Integration test (test_tool_confirmation_discovery.py) verifies that _get_tool_confirmation_info discovers confirmation tools through ConversationalToolRunnableCallable wrappers — guards against silent regressions from LangGraph internal changes or new wrapping layers
  • Mapper tests verify requireConfirmation flag on startToolCall events

Why rip-and-replace?

Zero production usage confirmed via App Insights telemetry (90 days, both CAS and Python agents). The @requires_approval decorator ships but no deployed agent exercises it. Deployment is coordinated: CAS first, then Python. See PR descriptions on companion PRs for full analysis.

Companion PRs:

Three fixes for coded agent tool confirmation:

1. hitl.py: @requires_approval now stamps REQUIRE_CONVERSATIONAL_CONFIRMATION
   metadata so the runtime can discover confirmation tools.

2. runtime.py: _get_tool_confirmation_info handles both graph shapes —
   UiPathToolNode (low-code, bound.tool) and LangGraph ToolNode
   (create_agent, bound.tools_by_name).

3. messages.py: seed chunk accumulator with an empty AIMessageChunk instead
   of the incoming message. Prevents self-merge on the first streaming chunk
   which doubled tool names (e.g. search_websearch_web) and broke the
   confirmation set lookup.
@JoshParkSJ JoshParkSJ force-pushed the josh/cas-tc-fix branch 2 times, most recently from 10e34fd to 1eed1e2 Compare April 14, 2026 13:28
@JoshParkSJ JoshParkSJ changed the title fix: revert deferred tool call [JAR-9208] feat: confirmToolCall support — startToolCall with requireConfirmation + ToolRunnableCallable [JAR-9208] Apr 14, 2026
…ify graph crawling

- Add ToolRunnableCallable subclass that explicitly preserves the BaseTool reference
  through wrap_tools_with_error_handling (replaces ad-hoc setattr)
- Simplify _get_tool_confirmation_info: drop tools_by_name branch, inline logic
- Add integration test that verifies confirmation discovery through the wrapper
- Rename TestConfirmationToolDeferral → TestToolCallConfirmation
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.

2 participants