Whetstone.
Advanced AgentState versus context
Module 2, Lesson 216 min

State versus context

Three places to put data, and the exam builds questions out of the two that sit next to each other on the same signature.

The compartments

Three parameters, three lifetimes
create_agent(
    model, tools,
    state_schema=CustomAgentState,   # mutable, per thread
    context_schema=Context,          # read-only, per invocation
    store=store,                     # persistent, across threads
)

State is what the run changes. Nodes and tools write to it, reducers merge those writes, and the checkpointer persists it against the thread id. Extend it by subclassing AgentState, which already carries messages.

Context is what the caller hands in for this particular invocation and nobody mutates. Tenant id, user id, locale, feature flags. The agent reads it and moves on.

Store is long-term memory that has nothing to do with any single conversation. Facts about a user that should still be true next week, on a thread that does not exist yet.

How each one is supplied

Context at invoke time
from dataclasses import dataclass

@dataclass
class Context:
    user_id: str

agent = create_agent(model=..., tools=[], context_schema=Context, checkpointer=...)

result = agent.invoke(
    {"messages": [...]},
    config={"configurable": {"thread_id": "1"}},
    context=Context(user_id="user-123"),
)

Look at that call carefully, because it is the whole lesson in one statement. Three different channels are open at once: the messages payload is state input, config carries the thread key, and context carries typed per-run data.

Reading them from inside

Tools reach all three through ToolRuntime, and middleware reaches them through Runtime:

One object, three compartments
runtime.state["messages"]     # mutable conversation data
runtime.context.user_id       # typed, read-only, per invocation
runtime.store                 # long-term, cross-thread

The question that decides placement

You will not remember three definitions under pressure. Remember one question instead.

Does anything inside this run need to change this value?

Yes means state. No means context, unless it also has to outlive the conversation entirely, which means store. That single test resolves almost every real placement decision, and it also explains the classic mistake: putting a mutable working value like a draft or an escalation flag into context, where nothing can write to it, and then discovering the agent has nowhere to record what it just decided.

Practice

Try it yourself

Quiz

Place the field

A support agent handles one customer per run. Which compartment holds the tenant id, which is supplied by the caller and never changes mid-run?

Three candidates
state_schema=...    # mutable, per-thread, written by nodes and tools
context_schema=...  # supplied per invocation, read not written
store=...           # persistent, spans threads
  1. Astate_schema, because anything the agent reads during the run is state
  2. Bstore, because tenant identity outlives any single conversation thread
  3. Ccontext_schema, because it is per-invocation configuration the agent reads
  4. Dconfig, under configurable, in the same place thread_id is supplied
Show answer

Correct answer: C — context_schema, because it is per-invocation configuration the agent reads

Caller-supplied data that is fixed for the run and only read is context. The last option is the sharpest distractor because thread_id genuinely does live under configurable and it is genuinely per-invocation, so the shapes look identical; config carries framework concerns and the thread key, while typed application data belongs in context_schema.

Recall

The three compartments

A distinction people conflate rather than one they fail to learn, so the useful test is not what each is but what makes one the wrong choice.

Distinguish state, context and store by lifetime and by who writes them.

Reveal answer

State is per-thread and mutable; the graph writes it as nodes and tools execute, and it is what a checkpointer persists. Context is per-invocation and immutable; the caller supplies it at invoke time and the agent only reads it. Store is long-term memory that spans threads entirely, so it is where something survives beyond any single conversation. The quickest disambiguator is: does anything inside the run need to change this value? If yes it is state, and if no it is context.

Quiz

How context reaches the agent

An agent is created with context_schema=Context. How is the value supplied?

Invoking with context
@dataclass
class Context:
    user_id: str
  1. Aagent.invoke({...}, context=Context(user_id="user-123"))
  2. Bagent.invoke({...}, {"configurable": {"user_id": "user-123"}})
  3. Cagent.invoke({"messages": [...], "user_id": "user-123"})
  4. Dcreate_agent(..., context=Context(user_id="user-123"))
Show answer

Correct answer: A — agent.invoke({...}, context=Context(user_id="user-123"))

Context is passed as its own context keyword argument at invoke time, carrying an instance of the declared schema. The second option is the tempting one because configurable is where thread_id goes and the two feel like the same compartment. The third would be a state update, and the fourth would fix the value at construction, which defeats the point of per-invocation data.

Quiz

Reading the pair on the signature

state_schema and context_schema sit next to each other on create_agent. What is the sharpest one-line difference?

  1. Astate_schema is a typed dataclass and context_schema is an untyped dict
  2. Bstate_schema is optional and context_schema is required on create_agent
  3. Cstate_schema describes what changes during the run; context_schema what does not
  4. Dstate_schema is for messages only; context_schema is for every other field you declare
Show answer

Correct answer: C — state_schema describes what changes during the run; context_schema what does not

Mutability during the run is the actual line, and it is the one that decides placement for a field you are unsure about. The last option is tempting because messages are the state field everybody knows, but state is extended with arbitrary fields all the time, which is exactly what subclassing AgentState is for.

Check

Sort eight fields

For a support agent, place these in state, context or store, and write one clause of justification each - tenant id, conversation messages, the current draft reply, the user's timezone, an escalation flag set mid-run, last month's ticket history, the model temperature, and a summary written by compression.

You should see

You placed the escalation flag and the draft in state, tenant id and timezone in context, ticket history in store, and you noticed that model temperature belongs to none of the three because it is set on the model instance.

Sign in to track your progress →