langchain - ✅(Solved) Fix `create_agent`: stale `structured_response` from checkpoint causes premature exit on next turn [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
langchain-ai/langchain#36957Fetched 2026-04-23 07:23:04
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
0
Timeline (top)
labeled ×3commented ×1issue_type_added ×1

With create_agent + response_format + a checkpointer, structured_response is restored from the previous turn's checkpoint into state at the start of the next turn. The routing edges use "structured_response" in state as an exit condition, so the agent can exit before the model runs for the new input. When the first model output of turn N+1 fails validation, the retry path is skipped and the caller gets turn N's answer back.

Error Message

Error Message and Stack Trace (if applicable)

Root Cause

With create_agent + response_format + a checkpointer, structured_response is restored from the previous turn's checkpoint into state at the start of the next turn. The routing edges use "structured_response" in state as an exit condition, so the agent can exit before the model runs for the new input. When the first model output of turn N+1 fails validation, the retry path is skipped and the caller gets turn N's answer back.

Fix Action

Fix / Workaround

  • 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.

Other Dependencies

anthropic: 0.77.0 httpx: 0.28.1 jsonpatch: 1.33 langgraph: 1.0.7 openai: 2.16.0 orjson: 3.11.7 packaging: 26.0 pydantic: 2.12.5 pyyaml: 6.0.3 requests: 2.32.5 requests-toolbelt: 1.0.0 tenacity: 9.1.2 tiktoken: 0.12.0 typing-extensions: 4.15.0 uuid-utils: 0.14.1 websockets: 16.0 xxhash: 3.6.0 zstandard: 0.25.0

PR fix notes

PR #36965: fix(langchain): clear stale structured_response from checkpoint on each new turn

Description (problem / solution / changelog)

Closes #36957


When a checkpointer is used, structured_response written to state in turn N is restored from the checkpoint at the start of turn N+1. The routing edges in _make_model_to_tools_edge and _make_model_to_model_edge tested "structured_response" in state, which is truthy for the restored value even before the model runs for the new turn. This caused the agent to exit immediately with the previous turn's answer instead of invoking the model.

Root cause: _build_commands only wrote structured_response to state when it was non-None. On the next turn, the stale value from the checkpoint was still present in state, and the routing edges treated it as a valid exit condition.

Fix: _build_commands now always writes structured_response to state (including None when the model produced no structured output), so the stale checkpoint value is overwritten at the end of every model node execution. The routing edges are updated to test state.get("structured_response") is not None instead of key presence.

Two regression tests are added to TestCheckpointStaleStructuredResponse:

  • test_second_turn_not_short_circuited_by_stale_checkpoint: verifies that turn 2 produces a fresh response, not the stale turn-1 value
  • test_second_turn_with_validation_error_retry: verifies that a validation failure in turn 2 triggers a retry rather than returning the stale turn-1 value

This contribution was developed with AI-agent assistance.

Changed files

  • libs/langchain_v1/langchain/agents/factory.py (modified, +11/-6)
  • libs/langchain_v1/tests/unit_tests/agents/middleware/core/test_framework.py (modified, +1/-0)
  • libs/langchain_v1/tests/unit_tests/agents/test_response_format.py (modified, +69/-0)

Code Example

from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.messages import AIMessage
from langgraph.checkpoint.memory import InMemorySaver
from pydantic import BaseModel, field_validator


class Answer(BaseModel):
    text: str

    @field_validator("text")
    @classmethod
    def not_bad(cls, v: str) -> str:
        if v == "BAD":
            raise ValueError("bad sentinel")
        return v


def msg(text: str, call_id: str) -> AIMessage:
    return AIMessage(content="", tool_calls=[{"name": "Answer", "args": {"text": text}, "id": call_id}])


class FakeModel(GenericFakeChatModel):
    def bind_tools(self, tools, **kwargs):
        return self


model = FakeModel(messages=iter([msg("Hi", "1"), msg("BAD", "2"), msg("Bye", "3")]))

agent = create_agent(
    model=model,
    tools=[],
    response_format=ToolStrategy(schema=Answer, handle_errors=True),
    checkpointer=InMemorySaver(),
)
thread = {"configurable": {"thread_id": "t"}}

r1 = agent.invoke({"messages": [("user", "say hi")]}, config=thread)
r2 = agent.invoke({"messages": [("user", "say bye")]}, config=thread)
print(r1["structured_response"], "|", r2["structured_response"])


"""
**Expected:** turn 2 returns `Answer(text='Bye')` (retry consumes the third queued response).
**Actual:** turn 2 returns `Answer(text='Hi')` — stale from turn 1; third response is never consumed.
"""

---



---

# _make_model_to_tools_edge, step 5 (factory.py:1752-1754)
if "structured_response" in state:
    return end_destination

# _make_model_to_model_edge, step 2 (factory.py:1779-1781)
if "structured_response" in state:
    return end_destination

---

System Information
------------------
> OS:  Darwin
> OS Version:  Darwin Kernel Version 25.4.0: Thu Mar 19 19:33:25 PDT 2026; root:xnu-12377.101.15~1/RELEASE_ARM64_T6041
> Python Version:  3.14.4 (v3.14.4:23116f998f6, Apr  7 2026, 09:45:22) [Clang 17.0.0 (clang-1700.6.4.2)]

Package Information
-------------------
> langchain_core: 1.2.8
> langchain: 1.2.8
> langsmith: 0.7.17
> langchain_anthropic: 1.3.1
> langchain_openai: 1.1.7
> langgraph_sdk: 0.3.3

Optional packages not installed
-------------------------------
> langserve

Other Dependencies
------------------
> anthropic: 0.77.0
> httpx: 0.28.1
> jsonpatch: 1.33
> langgraph: 1.0.7
> openai: 2.16.0
> orjson: 3.11.7
> packaging: 26.0
> pydantic: 2.12.5
> pyyaml: 6.0.3
> requests: 2.32.5
> requests-toolbelt: 1.0.0
> tenacity: 9.1.2
> tiktoken: 0.12.0
> typing-extensions: 4.15.0
> uuid-utils: 0.14.1
> websockets: 16.0
> xxhash: 3.6.0
> zstandard: 0.25.0
RAW_BUFFERClick to expand / collapse

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-xai
  • Other / not sure / general

Related Issues / PRs

No response

Reproduction Steps / Example Code (Python)

from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.messages import AIMessage
from langgraph.checkpoint.memory import InMemorySaver
from pydantic import BaseModel, field_validator


class Answer(BaseModel):
    text: str

    @field_validator("text")
    @classmethod
    def not_bad(cls, v: str) -> str:
        if v == "BAD":
            raise ValueError("bad sentinel")
        return v


def msg(text: str, call_id: str) -> AIMessage:
    return AIMessage(content="", tool_calls=[{"name": "Answer", "args": {"text": text}, "id": call_id}])


class FakeModel(GenericFakeChatModel):
    def bind_tools(self, tools, **kwargs):
        return self


model = FakeModel(messages=iter([msg("Hi", "1"), msg("BAD", "2"), msg("Bye", "3")]))

agent = create_agent(
    model=model,
    tools=[],
    response_format=ToolStrategy(schema=Answer, handle_errors=True),
    checkpointer=InMemorySaver(),
)
thread = {"configurable": {"thread_id": "t"}}

r1 = agent.invoke({"messages": [("user", "say hi")]}, config=thread)
r2 = agent.invoke({"messages": [("user", "say bye")]}, config=thread)
print(r1["structured_response"], "|", r2["structured_response"])


"""
**Expected:** turn 2 returns `Answer(text='Bye')` (retry consumes the third queued response).
**Actual:** turn 2 returns `Answer(text='Hi')` — stale from turn 1; third response is never consumed.
"""

Error Message and Stack Trace (if applicable)

Description

Summary

With create_agent + response_format + a checkpointer, structured_response is restored from the previous turn's checkpoint into state at the start of the next turn. The routing edges use "structured_response" in state as an exit condition, so the agent can exit before the model runs for the new input. When the first model output of turn N+1 fails validation, the retry path is skipped and the caller gets turn N's answer back.

Versions

langchain==1.2.8 (also on main at libs/langchain_v1 1.2.15), langchain-core==1.2.8, langgraph==1.0.7, langgraph-checkpoint==4.0.0, Python 3.14.

Where

libs/langchain_v1/langchain/agents/factory.py, both routing edges exit on key presence, not freshness:

# _make_model_to_tools_edge, step 5 (factory.py:1752-1754)
if "structured_response" in state:
    return end_destination

# _make_model_to_model_edge, step 2 (factory.py:1779-1781)
if "structured_response" in state:
    return end_destination

_build_commands (factory.py:196-199) only writes structured_response when non-None, so no write path clears it between turns.

System Info

Also reproduces against libs/langchain_v1 at 1.2.15 on main.

System Information
------------------
> OS:  Darwin
> OS Version:  Darwin Kernel Version 25.4.0: Thu Mar 19 19:33:25 PDT 2026; root:xnu-12377.101.15~1/RELEASE_ARM64_T6041
> Python Version:  3.14.4 (v3.14.4:23116f998f6, Apr  7 2026, 09:45:22) [Clang 17.0.0 (clang-1700.6.4.2)]

Package Information
-------------------
> langchain_core: 1.2.8
> langchain: 1.2.8
> langsmith: 0.7.17
> langchain_anthropic: 1.3.1
> langchain_openai: 1.1.7
> langgraph_sdk: 0.3.3

Optional packages not installed
-------------------------------
> langserve

Other Dependencies
------------------
> anthropic: 0.77.0
> httpx: 0.28.1
> jsonpatch: 1.33
> langgraph: 1.0.7
> openai: 2.16.0
> orjson: 3.11.7
> packaging: 26.0
> pydantic: 2.12.5
> pyyaml: 6.0.3
> requests: 2.32.5
> requests-toolbelt: 1.0.0
> tenacity: 9.1.2
> tiktoken: 0.12.0
> typing-extensions: 4.15.0
> uuid-utils: 0.14.1
> websockets: 16.0
> xxhash: 3.6.0
> zstandard: 0.25.0

extent analysis

TL;DR

The issue can be fixed by modifying the routing edges in factory.py to check for the freshness of the structured_response instead of just its presence.

Guidance

  • Modify the _make_model_to_tools_edge and _make_model_to_model_edge functions in factory.py to check if the structured_response is fresh, not just present.
  • Consider adding a timestamp or a turn ID to the structured_response to track its freshness.
  • Update the _build_commands function to clear the structured_response when it is None, to prevent stale responses from being reused.
  • Verify that the fix works by running the provided example code and checking that the second turn returns the expected response Answer(text='Bye').

Example

# _make_model_to_tools_edge, step 5 (factory.py:1752-1754)
if "structured_response" in state and state["structured_response"]["turn_id"] == current_turn_id:
    return end_destination

# _make_model_to_model_edge, step 2 (factory.py:1779-1781)
if "structured_response" in state and state["structured_response"]["turn_id"] == current_turn_id:
    return end_destination

Notes

The provided example code and system information suggest that the issue is specific to the langchain package and its interaction with the langchain-core package. The fix should be applied to the factory.py file in the langchain package.

Recommendation

Apply the workaround by modifying the routing edges in factory.py to check for the freshness of the structured_response. This should fix the issue and prevent stale responses from being reused.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING