-
Notifications
You must be signed in to change notification settings - Fork 29
feat: add ToolRuntime injection for interruptible tools #381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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>
| 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() |
There was a problem hiding this comment.
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.
| 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} |
There was a problem hiding this comment.
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.
Summary
Enables escalation tools (and future interruptible tools) to access
runtime.tool_call_idfor building proper ToolMessage responses.Problem
Escalation tools fail with:
The escalation tool needs
tool_call_idwhen returning aCommandwithToolMessage:Without
tool_call_id, LangGraph cannot match the response to the original LLM tool call → broken state.Solution
runtime: ToolRuntimeparameter via type hint inspectionToolRuntimewithtool_call_id,state, andconfigwhen detectedRelated
Test plan
tool_call_id🤖 Generated with Claude Code