Skip to content

fix: allow standalone .invoke() on tools with ToolRuntime parameter#7227

Open
Ethan T. (gambletan) wants to merge 1 commit intolangchain-ai:mainfrom
gambletan:fix/tool-runtime-standalone-invoke
Open

fix: allow standalone .invoke() on tools with ToolRuntime parameter#7227
Ethan T. (gambletan) wants to merge 1 commit intolangchain-ai:mainfrom
gambletan:fix/tool-runtime-standalone-invoke

Conversation

@gambletan
Copy link
Copy Markdown

Summary

Fixes #7222

When a @tool-decorated function has a runtime: ToolRuntime parameter, calling .invoke({}) standalone (outside of a LangGraph graph) fails with a Pydantic ValidationError because runtime is treated as a required input field.

Root cause: langchain-core's _parse_input validates tool input against the full args_schema (which includes ToolRuntime as a required field) and does not provide defaults for injected arguments when they are absent.

Fix (two parts):

  1. ToolRuntime.__get_pydantic_core_schema__ — makes Pydantic treat ToolRuntime as optional with a None default during schema validation. Handles the bare ToolRuntime type annotation.

  2. Patch BaseTool._parse_input — at tool_node.py import time, wraps the original method to handle injected arguments (ToolRuntime, InjectedState, InjectedStore) gracefully:

    • When all injected args are present in the input (ToolNode injection path), delegates to the original implementation unchanged.
    • When injected args are missing (standalone invocation path), validates using tool_call_schema (which already excludes injected fields) and re-adds injected keys with None defaults.

Before:

@tool
def my_tool(runtime: ToolRuntime):
    """Test tool"""
    pass

my_tool.invoke({})  # ValidationError: 'runtime' field required

After:

@tool
def my_tool(x: int, runtime: ToolRuntime) -> str:
    """Test tool"""
    if runtime is not None:
        return f"graph mode: {runtime.tool_call_id}"
    return f"standalone: x={x}"

my_tool.invoke({"x": 42})  # "standalone: x=42"

Test plan

  • Standalone .invoke({}) with ToolRuntime parameter succeeds (returns runtime=None)
  • Standalone .invoke() with ToolRuntime + other args works
  • ToolRuntime[MyContext] (generic) standalone invocation works
  • Normal tools (no injected args) are unaffected
  • ToolNode injection path still works (all injected args present)
  • Async .ainvoke() works
  • tool_call_schema still correctly excludes runtime
  • All 43 existing test_tool_node.py tests pass
  • All 16 test_tool_node_validation_error_filtering.py tests pass

🤖 Generated with Claude Code

When a @tool-decorated function has a `runtime: ToolRuntime` parameter,
calling `.invoke({})` standalone (outside a LangGraph graph) fails because
Pydantic requires the `runtime` field and langchain-core's `_parse_input`
does not provide a default for injected arguments.

This commit fixes the issue with two changes:

1. Add `__get_pydantic_core_schema__` to `ToolRuntime` so that Pydantic
   treats it as an optional field with a `None` default during schema
   validation. This handles the bare `ToolRuntime` type annotation.

2. Patch `BaseTool._parse_input` at import time to handle injected
   arguments (ToolRuntime, InjectedState, InjectedStore) gracefully:
   - When all injected args are present (ToolNode path), delegate to
     the original implementation unchanged.
   - When injected args are missing (standalone path), validate using
     `tool_call_schema` (which excludes injected fields) and re-add
     the injected keys with `None` defaults.

This enables the following usage pattern for unit testing and standalone
tool invocation:

    @tool
    def my_tool(x: int, runtime: ToolRuntime) -> str:
        if runtime is not None:
            # running inside a graph
            ...
        # standalone path
        return str(x)

    my_tool.invoke({"x": 42})  # works now

Fixes langchain-ai#7222

Signed-off-by: Tan <alvinttang@gmail.com>
@github-actions
Copy link
Copy Markdown
Contributor

This PR has been automatically closed because you are not assigned to the linked issue.

External contributors must be assigned to an issue before opening a PR for it. Please:

  1. Comment on the linked issue to request assignment from a maintainer
  2. Once assigned, edit your PR description and the PR will be reopened automatically

Maintainers: reopen this PR or remove the missing-issue-link label to bypass this check.

@github-actions github-actions bot closed this Mar 24, 2026
@mdrxy Mason Daugherty (mdrxy) added bypass-issue-check Maintainer override: skip issue-link enforcement and removed missing-issue-link labels Mar 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bypass-issue-check Maintainer override: skip issue-link enforcement external

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Calling .invoke() as standalone on a StructuredTool requires a ToolRuntime object

2 participants