Submission checklist
Package (Required)
Related Issues / PRs
Reproduction Steps / Example Code (Python)
"""A `before_model` hook that returns `jump_to="model"` crashes `create_agent`.
Self-contained: stub chat model, no API key, no network. Run as-is with only
`langchain` installed.
Observed on langchain 1.3.18 / langchain-core 1.6.1 / langgraph 1.2.11:
graph nodes: ['__end__', '__start__', 'RestartOnce.before_model', 'model', 'tools']
...
File ".../langgraph/graph/_branch.py", line 203, in <listcomp>
r if isinstance(r, Send) else self.ends[r] for r in result
~~~~~~~~~^^^
KeyError: 'RestartOnce.before_model'
During task with name 'RestartOnce.before_model'
The node exists in the graph; it is missing from the conditional edge's path map.
Putting another `before_model` middleware ahead of this one, or changing the hook
to `after_model` or `before_agent`, makes the same jump work.
"""
from typing import Any
from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware, hook_config
from langchain_core.language_models import BaseChatModel
from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.outputs import ChatGeneration, ChatResult
from langchain_core.tools import tool
class StubModel(BaseChatModel):
"""Answers once, makes no tool calls, needs no credentials."""
@property
def _llm_type(self) -> str:
return "stub"
def _generate(self, messages, stop=None, run_manager=None, **kwargs) -> ChatResult:
return ChatResult(generations=[ChatGeneration(message=AIMessage(content="ok"))])
def bind_tools(self, tools, **kwargs):
return self
@tool
def my_tool(value: str) -> str:
"""A great tool."""
return value.upper()
calls: list[str] = []
class RestartOnce(AgentMiddleware):
@hook_config(can_jump_to=["model"])
def before_model(self, state, runtime) -> dict[str, Any] | None:
calls.append("before_model")
if len(calls) == 1:
return {"jump_to": "model"} # re-enter the model loop once
return None
agent = create_agent(model=StubModel(), tools=[my_tool], middleware=[RestartOnce()])
print("graph nodes:", sorted(agent.get_graph().nodes))
print(agent.invoke({"messages": [HumanMessage("hello")]}))
Error Message and Stack Trace (if applicable)
graph nodes: ['RestartOnce.before_model', '__end__', '__start__', 'model', 'tools']
Traceback (most recent call last):
File "repro.py", line 65, in <module>
print(agent.invoke({"messages": [HumanMessage("hello")]}))
File ".../langgraph/pregel/main.py", line 3913, in invoke
File ".../langgraph/pregel/main.py", line 2967, in stream
File ".../langgraph/pregel/_runner.py", line 207, in tick
File ".../langgraph/pregel/_retry.py", line 617, in run_with_retry
File ".../langgraph/_internal/_runnable.py", line 709, in invoke
File ".../langgraph/_internal/_runnable.py", line 447, in invoke
File ".../langgraph/graph/_branch.py", line 167, in _route
return self._finish(writer, input, result, config)
File ".../langgraph/graph/_branch.py", line 203, in <listcomp>
r if isinstance(r, Send) else self.ends[r] for r in result
~~~~~~~~~^^^
KeyError: 'RestartOnce.before_model'
During task with name 'RestartOnce.before_model'
Description
JumpTo is Literal["tools", "model", "end"], and hook_config(can_jump_to=...) accepts any of the three on any hook — there is no per-hook restriction in the types, in the decorator, or in the docs. Yet a before_model hook that actually returns {"jump_to": "model"} crashes the agent at runtime with a bare KeyError naming its own graph node, raised from inside LangGraph's branch resolution, with nothing pointing back at the middleware or at can_jump_to.
The cause is in _add_middleware_edge. The model loop is entered at the first before_model node, so model_destination (the loop entry node) is that node's own name. The guard name != model_destination then drops it from the conditional edge's path map, while jump_edge still resolves "model" to it — so LangGraph is asked for a destination that was never declared. This is the same shape as #38351, one function over: a router can return the loop entry node while the path map omits it.
Only one configuration breaks; every neighbouring one works:
| configuration |
result |
single before_model, jumps to "model" |
KeyError |
first of two before_model, jumps |
KeyError |
second of two before_model, jumps |
works |
after_model jumps to "model" |
works |
before_agent jumps to "model" |
works |
A guardrail or validation middleware that rewrites state and wants the pre-model pipeline re-run is the natural use of this jump, and today it is simply unavailable — with an error that gives the user nothing to search for.
Expected behaviour: either the jump re-enters the loop at its entry node, exactly as the identical jump from after_model already does, or create_agent rejects the configuration while building the graph. Not an unexplained KeyError at run time.
Suggested fix: declare the self-destination, by replacing the name != model_destination guard with a model_destination not in destinations de-duplication. One line.
Verified locally against master: the four working shapes above still pass, the two failing ones now pass, and a combinatorial sweep over middleware configurations drops from 42 KeyErrors to zero. The langchain unit suite reports 1104 passed with the regression test and 1103 with the implementation change alone, so nothing else moves; ruff and mypy are clean.
A patch with that fix and a regression test is ready and linked to this issue. The assignment gate will auto-close that PR, and reopen_on_assignment will bring it back the moment this issue is assigned. Could a maintainer assign this to me?
System Info
Reproduced on a clean throwaway venv with only langchain installed from PyPI:
- langchain 1.3.18
- langchain-core 1.6.1
- langgraph 1.2.11
- Python 3.11.13
- macOS 26.6.2 (arm64)
Also reproduced on master at 49da581.
Social handles (optional)
No response
Submission checklist
Package (Required)
Related Issues / PRs
_make_model_to_tools_edge: the router could return the model destination whilemodel_to_tools_destinationsomitted it. That fix covered themodel_to_toolsedge; the middleware edge built by_add_middleware_edgestill has it.Reproduction Steps / Example Code (Python)
Error Message and Stack Trace (if applicable)
Description
JumpToisLiteral["tools", "model", "end"], andhook_config(can_jump_to=...)accepts any of the three on any hook — there is no per-hook restriction in the types, in the decorator, or in the docs. Yet abefore_modelhook that actually returns{"jump_to": "model"}crashes the agent at runtime with a bareKeyErrornaming its own graph node, raised from inside LangGraph's branch resolution, with nothing pointing back at the middleware or atcan_jump_to.The cause is in
_add_middleware_edge. The model loop is entered at the firstbefore_modelnode, somodel_destination(the loop entry node) is that node's own name. The guardname != model_destinationthen drops it from the conditional edge's path map, whilejump_edgestill resolves"model"to it — so LangGraph is asked for a destination that was never declared. This is the same shape as #38351, one function over: a router can return the loop entry node while the path map omits it.Only one configuration breaks; every neighbouring one works:
before_model, jumps to"model"KeyErrorbefore_model, jumpsKeyErrorbefore_model, jumpsafter_modeljumps to"model"before_agentjumps to"model"A guardrail or validation middleware that rewrites state and wants the pre-model pipeline re-run is the natural use of this jump, and today it is simply unavailable — with an error that gives the user nothing to search for.
Expected behaviour: either the jump re-enters the loop at its entry node, exactly as the identical jump from
after_modelalready does, orcreate_agentrejects the configuration while building the graph. Not an unexplainedKeyErrorat run time.Suggested fix: declare the self-destination, by replacing the
name != model_destinationguard with amodel_destination not in destinationsde-duplication. One line.Verified locally against
master: the four working shapes above still pass, the two failing ones now pass, and a combinatorial sweep over middleware configurations drops from 42KeyErrors to zero. Thelangchainunit suite reports 1104 passed with the regression test and 1103 with the implementation change alone, so nothing else moves;ruffandmypyare clean.A patch with that fix and a regression test is ready and linked to this issue. The assignment gate will auto-close that PR, and
reopen_on_assignmentwill bring it back the moment this issue is assigned. Could a maintainer assign this to me?System Info
Reproduced on a clean throwaway venv with only
langchaininstalled from PyPI:Also reproduced on
masterat 49da581.Social handles (optional)
No response