Skip to content

Conversation

@saksharthakkar
Copy link

Summary

Enables escalation tools (and future interruptible tools) to access runtime.tool_call_id for building proper ToolMessage responses.

Problem

Escalation tools fail with:

TypeError: escalation_tool_fn() missing 1 required positional argument: 'runtime'

The escalation tool needs tool_call_id when returning a Command with ToolMessage:

async def escalation_tool_fn(runtime: ToolRuntime, **kwargs):
    # ... interrupt and resume logic ...
    if outcome == "end":
        return Command(
            update={"messages": [
                ToolMessage(
                    content="...",
                    tool_call_id=runtime.tool_call_id,  # ← needs this
                )
            ]},
            goto=TERMINATE,
        )

Without tool_call_id, LangGraph cannot match the response to the original LLM tool call → broken state.

Solution

  • Detect tools with runtime: ToolRuntime parameter via type hint inspection
  • Inject ToolRuntime with tool_call_id, state, and config when detected
  • Skip injection for regular tools (no signature change = no breakage)

Related

Test plan

  • Unit tests for runtime detection (True for tools with runtime param, False otherwise)
  • Unit tests for async/sync tool execution with runtime injection
  • Unit tests for Command return with correct tool_call_id
  • All 357 existing tests pass

🤖 Generated with Claude Code

saksharthakkar and others added 3 commits December 29, 2025 01:15
Escalation tools require `runtime.tool_call_id` to build proper
ToolMessage responses in Command objects. Without this, LangGraph
cannot match the response to the original tool call.

Changes:
- Detect tools with `runtime: ToolRuntime` param via signature inspection
- Inject ToolRuntime with tool_call_id, state, config when needed
- Regular tools continue to work without runtime injection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Comment on lines +17 to +41
def _import_tool_node() -> Any:
"""Import tool_node module directly to bypass circular import."""
import os

module_path = os.path.join(
os.path.dirname(__file__),
"..",
"..",
"..",
"src",
"uipath_langchain",
"agent",
"tools",
"tool_node.py",
)
module_path = os.path.abspath(module_path)
spec = importlib.util.spec_from_file_location("tool_node", module_path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules["tool_node"] = module
spec.loader.exec_module(module)
return module


_tool_node_module = _import_tool_node()
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm really not a fan of introducing such hacks to "solve" circular imports.
If we have circular imports we should just restructure our modules in order to avoid them.

Comment on lines +65 to +79
def _get_tool_args(
self, call: ToolCall, state: Any, config: RunnableConfig | None
) -> dict[str, Any]:
"""Get tool args, injecting ToolRuntime if needed."""
args = call["args"]
if self._needs_runtime:
runtime = ToolRuntime(
state=state,
tool_call_id=call["id"],
config=config or {},
context=None,
stream_writer=lambda _: None,
store=None,
)
args = {**args, "runtime": runtime}
Copy link
Contributor

@andreitava-uip andreitava-uip Dec 30, 2025

Choose a reason for hiding this comment

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

Personally I would take an entirely different approach here.
I'd say tools themselves should never return messages or commands. A tool should only ever return its output schema.
Keep them completely graph-agnostic.
In the custom tool node design, the idea is for the wrappers to return commands if needed.

If anything, escalation tool should be refactored to no longer be coupled to the graph, which is something that should be in the works already.

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