Skip to content

feat: bind HITL escalation recipients to upstream tool outputs [ACTN-10480]#867

Open
kunaluipath wants to merge 1 commit into
mainfrom
feat/custom-assignments
Open

feat: bind HITL escalation recipients to upstream tool outputs [ACTN-10480]#867
kunaluipath wants to merge 1 commit into
mainfrom
feat/custom-assignments

Conversation

@kunaluipath

Copy link
Copy Markdown

Adds support for resolving escalation recipients at runtime from the output of upstream tools, in addition to the existing literal / agent-input bindings. Works for User, Group, Workload, RoundRobin, and CustomAssignees criteria.

  • _extract_tool_output_value: walks state.messages backwards, finds the latest ToolMessage by name, parses content as JSON, extracts a top-level field (fail-loud on missing tool / missing path).
  • _build_tool_output_task_recipient: maps the extracted value to the appropriate TaskRecipient shape per criteria (list to Workload-style, string to single, CustomAssignees comma-split).
  • resolve_recipient_value: new ToolOutputRecipient branch.
  • resolve_channel_recipients: threads tool_messages through; tool-output takes precedence over CustomAssignees aggregation.
  • escalation_wrapper: captures state.messages into tool.metadata so escalation_tool_fn can read it without changing public signatures.
  • create_escalation_tool: auto-augments tool description with a hint listing the tool dependencies for the LLM to plan tool calls.

Backwards-compatible: existing recipients without source parse and resolve identically.

@kunaluipath kunaluipath force-pushed the feat/custom-assignments branch 2 times, most recently from 2541dd2 to bcc79f3 Compare June 2, 2026 02:22
Adds support for resolving escalation recipients at runtime from the
output of upstream tools, in addition to the existing literal /
agent-input bindings. Works for User, Group, Workload, RoundRobin,
and CustomAssignees criteria.

- _extract_tool_output_value: walks state.messages backwards, finds
  the latest ToolMessage by name, parses content as JSON, extracts a
  top-level field (fail-loud on missing tool / missing path).
- _build_tool_output_task_recipient: maps the extracted value to the
  appropriate TaskRecipient shape per criteria (list to Workload-style,
  string to single, CustomAssignees comma-split).
- resolve_recipient_value: new ToolOutputRecipient branch.
- resolve_channel_recipients: threads tool_messages through; tool-output
  takes precedence over CustomAssignees aggregation.
- escalation_wrapper: captures state.messages into tool.metadata so
  escalation_tool_fn can read it without changing public signatures.
- create_escalation_tool: auto-augments tool description with a hint
  listing the tool dependencies for the LLM to plan tool calls.

Backwards-compatible: existing recipients without source parse and
resolve identically.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@kunaluipath kunaluipath force-pushed the feat/custom-assignments branch from bcc79f3 to 04da4eb Compare June 4, 2026 20:32
Copilot AI review requested due to automatic review settings June 4, 2026 20:32
@sonarqubecloud

sonarqubecloud Bot commented Jun 4, 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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the escalation tool’s recipient resolution so escalation recipients can be dynamically derived from prior tool outputs (in addition to literal values and agent-input argument bindings), and threads message history through the tool wrapper to enable that resolution at runtime.

Changes:

  • Added tool-output recipient binding support by extracting fields from the latest matching ToolMessage and mapping that value into TaskRecipient.
  • Introduced resolve_channel_recipients() to correctly aggregate channel recipients (notably CustomAssignees) and to give tool-output bindings precedence when configured.
  • Expanded test coverage for new recipient types (Workload, RoundRobin, CustomAssignees), tool-output extraction behavior, and description augmentation for upstream tool dependencies.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
tests/agent/tools/test_escalation_tool.py Adds tests for new recipient resolution paths (workload/round-robin/custom-assignees), tool-output extraction, channel aggregation, and tool description hints.
src/uipath_langchain/agent/tools/escalation_tool.py Implements tool-output extraction and mapping, adds channel-level recipient resolution, threads message history via metadata, and augments tool descriptions with dependency hints.
src/uipath_langchain/agent/guardrails/actions/escalate_action.py Updates assigned-to metadata handling for additional recipient model types used by guardrail escalation task creation.

Comment on lines +89 to +91
for msg in reversed(tool_messages):
if isinstance(msg, ToolMessage) and getattr(msg, "name", None) == tool_name:
content = msg.content

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks right. Mind checking, @kunaluipath

Comment on lines +134 to +146
if isinstance(value, list):
# Filter to truthy strings — tool outputs may contain nulls/empty entries.
emails = [str(v) for v in value if v]
if not emails:
raise ValueError(
f"Tool-output recipient resolved to an empty list for criteria "
f"{recipient_type.value}."
)
return TaskRecipient(
value=emails[0],
values=emails,
type=TaskRecipientType.WORKLOAD,
)
@dushyant-uipath

Copy link
Copy Markdown
Contributor

tool.metadata["agent_messages"] = list(raw_messages or []) — full history
copy on every call

This copies every message object (potentially large AIMessages, multi-modal
content, etc.) into the tool's metadata dict for every escalation invocation.
The only messages actually needed are ToolMessage instances. Filtering first
would be cheaper and reduce the blast radius of accidental metadata exposure:

tool.metadata["agent_messages"] = [m for m in (raw_messages or []) if
isinstance(m, ToolMessage)]

^ Claude mentioned this - worth checking if doable

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.

3 participants