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 typing import Annotated, Any
from langchain.tools import InjectedToolArg, tool
from langchain.tools import tool
from langgraph.config import get_config
from deerflow.agents.middlewares.input_sanitization_middleware import neutralize_untrusted_tags
@ -187,6 +187,7 @@ def _list_uploaded_files_impl(
@tool
def list_uploaded_files(
runtime: Runtime,
include_outline: Annotated[
bool | list[str],
"Control which files get their document outline (headings/preview) returned. "
@ -198,7 +199,6 @@ def list_uploaded_files(
int,
"Maximum number of files to return (default 20, max 100).",
] = _DEFAULT_MAX_RESULTS,
runtime: Annotated[Runtime, InjectedToolArg] | None = None,
) -> dict:
"""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
assert len(result["files"]) == 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"}