fix(tools): exclude injected runtime from list_uploaded_files schema (#4375) (#4376)

Declaring the injected runtime arg as `Annotated[Runtime, InjectedToolArg] | None`
made the top-level annotation a Union, so LangChain no longer treated it as
injected. It leaked into the model-facing schema and pydantic raised
PydanticInvalidForJsonSchema on the ToolRuntime dataclass the moment the tool
was bound to a model. The tool is bound by default for the lead agent, so any
default run on an OpenAI-compatible provider failed at tool-bind time.

Declare runtime as a bare Runtime first param, matching every other built-in
tool (present_files, view_image, task, ...), which LangChain auto-injects and
auto-excludes from the schema. Add a schema regression test that binds the tool.
This commit is contained in:
Aari 2026-07-23 08:22:15 +08:00 committed by GitHub
parent 0d4d0cb17d
commit 7b330101d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 32 additions and 2 deletions

View File

@ -12,7 +12,7 @@ from collections import Counter
from pathlib import Path from pathlib import Path
from typing import Annotated, Any from typing import Annotated, Any
from langchain.tools import InjectedToolArg, tool from langchain.tools import tool
from langgraph.config import get_config from langgraph.config import get_config
from deerflow.agents.middlewares.input_sanitization_middleware import neutralize_untrusted_tags from deerflow.agents.middlewares.input_sanitization_middleware import neutralize_untrusted_tags
@ -187,6 +187,7 @@ def _list_uploaded_files_impl(
@tool @tool
def list_uploaded_files( def list_uploaded_files(
runtime: Runtime,
include_outline: Annotated[ include_outline: Annotated[
bool | list[str], bool | list[str],
"Control which files get their document outline (headings/preview) returned. " "Control which files get their document outline (headings/preview) returned. "
@ -198,7 +199,6 @@ def list_uploaded_files(
int, int,
"Maximum number of files to return (default 20, max 100).", "Maximum number of files to return (default 20, max 100).",
] = _DEFAULT_MAX_RESULTS, ] = _DEFAULT_MAX_RESULTS,
runtime: Annotated[Runtime, InjectedToolArg] | None = None,
) -> dict: ) -> dict:
"""Discover historical uploaded files available in this thread. """Discover historical uploaded files available in this thread.

View File

@ -840,3 +840,33 @@ def test_all_string_fields_in_result_are_neutralized(tmp_path):
# Sanity: the result is non-empty and well-formed # Sanity: the result is non-empty and well-formed
assert len(result["files"]) == 1 assert len(result["files"]) == 1
assert result["total_count"] == 1 assert result["total_count"] == 1
# ---------------------------------------------------------------------------
# @tool schema — regression for #4375
# ---------------------------------------------------------------------------
class TestToolSchema:
"""Guard the model-facing schema of the @tool wrapper.
The injected ``runtime`` argument must not leak into the schema sent to the
LLM. Declaring it as ``Annotated[Runtime, InjectedToolArg] | None`` (issue
#4375) made the top-level annotation a Union, so LangChain no longer treated
it as injected and pydantic raised ``PydanticInvalidForJsonSchema`` on the
ToolRuntime dataclass the moment the tool was bound to a model.
"""
def test_runtime_excluded_from_model_facing_args(self):
from deerflow.tools.builtins.list_uploaded_files_tool import list_uploaded_files
assert set(list_uploaded_files.args) == {"include_outline", "max_results"}
assert "runtime" not in list_uploaded_files.args
def test_openai_schema_generation_succeeds(self):
from langchain_core.utils.function_calling import convert_to_openai_tool
from deerflow.tools.builtins.list_uploaded_files_tool import list_uploaded_files
# This raised PydanticInvalidForJsonSchema before the fix.
oai = convert_to_openai_tool(list_uploaded_files)
params = oai["function"]["parameters"]["properties"]
assert set(params) == {"include_outline", "max_results"}