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_usagecopies so the emitted event can't share the source message's dicts
Reproduction Steps / Example Code (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)
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 GraphRecursionErrorDescription
- I'm trying to use
PIIMiddleware(apply_to_output=True)(Also an issue on apply_to_tool_results) on an agent with tools, consumed throughstream_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 inGraphRecursionError. 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)
Source: langchain-ai/langchain