Edit state for dynamic planning

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:

  • Each leg can be recomputed independently.

  • plans for unchanged legs remain intact (no rework needed).

Docs: see Use subgraphs.

How to apply an edit while keeping other legs the same

  1. 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.
  2. Call graph.update_state(selected_state.config, values={"trips": updated_trips}) where updated_trips has the edited leg (e.g., citya→cityb→cityccityd→cityb→cityc).
    • Because trips has no reducer, the edit overwrites only that channel as provided.
  3. 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?

    • Keep trips without a reducer so you can overwrite it cleanly on edits.

    • Use a reducer (merge) for plans so partial updates affect only the edited leg.

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