mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-23 01:55:06 +02:00
fix(langgraph): validate ID-mapped resumes before committing them
A resume passed as Command(resume={interrupt_id: value}) is already in
the scratchpad when interrupt() runs, so it took the replay branch, which
queued the RESUME write before validation. A rejected value was therefore
persisted and every later correction re-validated it.
This commit is contained in:
@@ -982,9 +982,10 @@ def interrupt(
|
||||
# find previous resume values
|
||||
if scratchpad.resume:
|
||||
if idx < len(scratchpad.resume):
|
||||
conf[CONFIG_KEY_SEND]([(RESUME, scratchpad.resume)])
|
||||
v = scratchpad.resume[idx]
|
||||
return adapter.validate_python(v) if adapter else v
|
||||
validated = adapter.validate_python(v) if adapter else v
|
||||
conf[CONFIG_KEY_SEND]([(RESUME, scratchpad.resume)])
|
||||
return validated
|
||||
# find current resume value
|
||||
v = scratchpad.get_null_resume(True)
|
||||
if v is not None:
|
||||
|
||||
@@ -163,8 +163,9 @@ def test_interrupt_response_schema(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("resume_style", ["null", "map"])
|
||||
def test_interrupt_response_schema_rejects_invalid_resume(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
sync_checkpointer: BaseCheckpointSaver, resume_style: str
|
||||
) -> None:
|
||||
class State(TypedDict):
|
||||
answer: Any
|
||||
@@ -180,10 +181,14 @@ def test_interrupt_response_schema_rejects_invalid_resume(
|
||||
)
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
graph.invoke({"answer": None}, config)
|
||||
[pending] = graph.get_state(config).tasks[0].interrupts
|
||||
|
||||
def resume(value: dict[str, Any]) -> Command:
|
||||
return Command(resume=value if resume_style == "null" else {pending.id: value})
|
||||
|
||||
with pytest.raises(ValidationError, match="approved"):
|
||||
graph.invoke(Command(resume={"approved": "nope"}), config)
|
||||
graph.invoke(resume({"approved": "nope"}), config)
|
||||
|
||||
assert graph.invoke(Command(resume={"approved": False}), config) == {
|
||||
assert graph.invoke(resume({"approved": False}), config) == {
|
||||
"answer": Decision(approved=False)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user