#40767·langchain

PIIMiddleware(apply_to_output=True) blanks parameters on tool calls, leading to a GraphRecursion Error.

Author: jclauson32Created Sep 22, 2026Updated Sep 28, 2026
Labelsbugcorelangchainexternal

Submission checklist

  • This is a bug, not a usage question.
  • I added a clear and descriptive title that summarizes this issue.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangChain rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).
  • This is not related to the langchain-community package.
  • I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS.

Package (Required)

  • langchain
  • langchain-openai
  • langchain-anthropic
  • langchain-classic
  • langchain-core
  • langchain-model-profiles
  • langchain-tests
  • langchain-text-splitters
  • langchain-chroma
  • langchain-deepseek
  • langchain-exa
  • langchain-fireworks
  • langchain-groq
  • langchain-huggingface
  • langchain-mistralai
  • langchain-nomic
  • langchain-ollama
  • langchain-openrouter
  • langchain-perplexity
  • langchain-qdrant
  • langchain-typesafe
  • langchain-xai
  • Other / not sure / general

Related Issues / PRs

Issues:

  • #37659 PIIMiddleware state hooks, opposite direction (under-redacts, not a duplicate)
  • #35514 same symptom (empty args {}), different cause, no middleware involved
  • #40392 same per-index block state in _compat_bridge, different function

PRs:

  • #37616 added the streaming redaction path this bug lives in
  • #38021 same de-aliasing fix in the same file: _isolate_usage copies so the emitted event can't share the source message's dicts

Reproduction Steps / Example Code (Python)

python
import time

from langchain.agents import create_agent
from langchain.agents.middleware import PIIMiddleware
from langchain.tools import tool
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.messages import AIMessageChunk
from langchain_core.outputs import ChatGenerationChunk

ARGS = ["", '{"city"', ': "Bos', 'ton"}']  # {"city": "Boston"} split across chunks


class FakeStreamingModel(BaseChatModel):
    """Streams a tool call in chunks, paced like a real provider."""

    idx: int = 0

    @property
    def _llm_type(self) -> str:
        return "fake-streaming-model"

    def bind_tools(self, tools, **kwargs):
        return self

    def _stream(self, messages, stop=None, run_manager=None, **kwargs):
        i = self.idx
        object.__setattr__(self, "idx", i + 1)
        if i:  # second turn: plain text, so the run terminates
            yield ChatGenerationChunk(message=AIMessageChunk(content="Done."))
            return
        for n, part in enumerate(ARGS):
            # The pauses are required. The PII stream transformer runs on the
            # consumer side, so an instantaneous model assembles the whole
            # message before the mux pumps an event and the bug stays hidden.
            time.sleep(0.01)
            yield ChatGenerationChunk(
                message=AIMessageChunk(
                    content="",
                    tool_call_chunks=[
                        {
                            "name": "get_weather" if n == 0 else None,
                            "args": part,
                            "id": "call_1" if n == 0 else None,
                            "index": 0,
                        }
                    ],
                )
            )
        time.sleep(0.05)  # latency to simulate real provider

    def _generate(self, messages, stop=None, run_manager=None, **kwargs):
        raise NotImplementedError("unused: the v3 streaming path calls _stream")


@tool
def get_weather(city: str) -> str:
    """Get weather for a given city."""
    return f"It's always sunny in {city}!"


agent = create_agent(
    model=FakeStreamingModel(),  # replace with real
    tools=[get_weather],
    name="weather_agent",
    middleware=[PIIMiddleware("credit_card", strategy="hash", apply_to_output=True)],
).with_config(recursion_limit=25)

stream = agent.stream_events(
    {"messages": [{"role": "user", "content": "What's the weather in Boston?"}]},
    version="v3",
)
for tool_call in stream.tool_calls:
    print(tool_call.tool_name, tool_call.input)
    assert tool_call.input == {
        "city": "Boston"
    }, f"expected {{'city': 'Boston'}}, got {tool_call.input}"

Error Message and Stack Trace (if applicable)

bash
N/A. This is a deterministic behavior bug rather than an exception path.

Actual behavior:
- tool_calls[*].args is emptied: the model emits {"city": "Boston"}, the agent executes {}
- the blanked args reach graph state, not just the streamed output, so the tool runs
  with no arguments and the ToolMessage comes back "city: Field required"
- this happens with no PII present anywhere in the run, under every strategy
- with a real provider the model cannot act on that error, retries the same call, and
  the run ends in GraphRecursionError

Description

  • I'm trying to use PIIMiddleware(apply_to_output=True) (Also an issue on apply_to_tool_results) on an agent with tools, consumed through stream_events(version="v3").
  • I expect tool calls to execute with the arguments the model produced, with any PII inside those arguments redacted.
  • Instead the arguments of every tool call are blanked to {}, whether or not they contain PII. The tool raises a validation error, the model is told to try again, nothing it does changes the outcome, and the run ends in GraphRecursionError. Message text is unaffected.

System Info

System Information

OS: Windows OS Version: 10.0.26200 Python Version: 3.13.9 (tags/v3.13.9:8183fa5, Oct 14 2025, 14:09:13) [MSC v.1944 64 bit (AMD64)]

Package Information

langchain_core: 1.6.4 langchain: 1.4.2 langsmith: 0.14.0 langchain_openai: 1.6.3 langchain_protocol: 0.0.19 langgraph_sdk: 0.4.5

Optional packages not installed

deepagents deepagents-cli

Other Dependencies

anyio: 4.15.1 certifi: 2026.7.22 distro: 1.9.0 httpx: 0.28.1 httpx2: 2.13.0 jsonpatch: 1.33 langgraph: 1.2.12 openai: 3.17.0 orjson: 3.12.0 packaging: 26.3 pydantic: 2.13.5 pytest: 9.1.1 pyyaml: 6.0.3 requests: 2.34.2 requests-toolbelt: 1.0.0 sniffio: 1.3.1 tenacity: 9.1.4 tiktoken: 0.14.0 typing-extensions: 4.16.0 uuid-utils: 0.17.1 websockets: 16.1.1 xxhash: 4.0.1 zstandard: 0.25.0

Social handles (optional)

Linkedin: https://www.linkedin.com/in/joseph-clauson/