I am making a travel planner - plenty of examples available. But this one has a twist. It can do a multi-trip plan. like
citya → cityb → cityc
However these trips need to be editable. The other leg must remain the same. Like the user (for the trip above), he might decide
cityd → cityb → cityc
I am having hard time in updating the state (which will be a list of trips where each trip has a start, destination, date etc.) . What is the best way to do so? Do I need a separate tool that manages this dynamic state i.e. the addition/removal of trips? Simple reducers might not work. The current solution that i have in my mind is a react-style LLM (with no tool-calling via bind tools) and parsing the structured output, but I would like to avoid that.
Hi @mgathena
great question!
IMHO, you don’t need a separate “state management tool.” Model trips in your LangGraph state with stable IDs, use a checkpointer, and edit state via graph.update_state(...) at the right checkpoint. Combine this with per-leg subgraphs so you can recompute only the edited leg while keeping the others unchanged.
What to model
-
trips (source of truth): a list of legs the user wants, each with a stable leg_id and fields like start, dest, date.
-
plans (results): a dictionary keyed by leg_id that holds the computed plan for each leg. Using a dict lets you update one leg without touching the others.
from typing_extensions import TypedDict, NotRequired
from typing import Annotated
from operator import or_ # Python 3.9+ dict merge
class Trip(TypedDict):
leg_id: str
start: str
dest: str
date: str
class Plan(TypedDict):
summary: str
steps: list[str]
class State(TypedDict):
# trips: canonical requested legs (no reducer => overwrite on updates)
trips: list[Trip]
# plans: computed results, keyed by leg_id (merge reducer to update one leg)
plans: Annotated[dict[str, Plan], or_]
Why this shape?
-
trips has no reducer, so user edits can replace the list or an element cleanly.
-
plans uses a merge reducer, so returning {"plans": {leg_id: new_plan}} updates only that leg’s plan without affecting others.
Build the graph with per-leg subgraphs
-
Create a subgraph that plans a single leg: it reads one Trip and returns {"plans": {trip.leg_id: plan}}.
-
The parent graph orchestrates over trips (sequentially or with a foreach pattern) invoking the subgraph per leg.
Benefits:
Docs: see Use subgraphs.
How to apply an edit while keeping other legs the same
- Pick the checkpoint just before the subgraph invocation for the leg you want to change.
- Use
graph.get_state_history(config) to find the checkpoint_id preceding that leg.
- Call
graph.update_state(selected_state.config, values={"trips": updated_trips}) where updated_trips has the edited leg (e.g., citya→cityb→cityc → cityd→cityb→cityc).
- Because
trips has no reducer, the edit overwrites only that channel as provided.
- Resume from that checkpoint:
graph.invoke(None, new_config).
- Only the edited leg’s subgraph runs; other legs’
plans remain as they were.
Docs: Persistence - Update state and Use time-travel.
# helper to replace one leg in-place by leg_id
def replace_leg(trips: list[Trip], leg_id: str, new_trip: Trip) -> list[Trip]:
return [new_trip if t["leg_id"] == leg_id else t for t in trips]
# 1) locate a checkpoint before the target leg’s subgraph
history = list(graph.get_state_history(config))
selected_state = pick_checkpoint_before_leg(history, leg_id) # your logic here
# 2) update trips
curr = graph.get_state(selected_state.config).values
updated = replace_leg(curr["trips"], leg_id, new_trip)
new_config = graph.update_state(selected_state.config, {"trips": updated})
# 3) resume execution (only the affected leg re-runs)
graph.invoke(None, new_config)
FAQ
-
Do I need a separate “state tool”? No. Use LangGraph’s checkpointer and update_state. This is the intended mechanism for interactive, editable state.
-
Why not a React-style LLM planner? Unnecessary. Editing via update_state plus subgraphs keeps logic explicit, testable, and avoids fragile parsing.
-
What about reducers?
-
Can I pause for a user edit mid-run? Yes - add an interrupt(...) before committing a leg; accept the user change, call update_state, then resume. See Enable human intervention.
Summary
-
Represent requests (trips) and results (plans) separately.
-
Use per-leg subgraphs and a dict-merge reducer for plans.
-
On edit, fork at the checkpoint before that leg, update_state with the new leg, and resume. Other legs remain unchanged.
References:
1 Like
Wow, exhaustive answer. Good points for thought. I really need to re-design my agent. Thanks. Skipping reducers and maintaining a list makes much more sense. But I do wish to skip the time-travel option.
One small question. How do i populate this state? graph.update_state will only work outside the agent itself, right? Within the agent, do I use a tool-calling LLM or a LLM with structured output to parse the user’s intent and the returned flight options (from a tool/api)