Support to initialize StateGraph with instance of the class

Hi there!

I’m taking course - AI Agents in LangGraph - DeepLearning.AI where Harrison Chase explains how to build AI agent using langgraph. In his implementation he is doing the following:

class Agent:

def __init__(self, model, tools, system=""):
        self.system = system
        graph = StateGraph(AgentState)

...... 

        def call_openai(self, state: AgentState):
            messages = state['messages']
            if self.system:
                 messages = [SystemMessage(content=self.system)] + messages

....
  

From what I saw in documentation there is no way to implement:

class Agent:
def __init__(self, model, tools, system=""):
        self.system = system
        state = AgentState(messages=[SystemMessage(system)])
        graph = StateGraph(state)

...... 

        def call_openai(self, state: AgentState):
            messages = state['messages']
       

....


Basically graph is using checkpoints to rebuild graph in certain state. But I dont have access to start my graph with in certain state. It would be at least in this case easier to init state to have system message, which I need every execution as a first message anytime. At the end adding system message in each llm invocation looks a bit odd.

Most probably, I’m lucking understanding and context, could please someone share insights why langgraph is implemented in certain way?

Interesting question. I’m also learning LangGraph and this made me think about the tradeoff between keeping the graph state reproducible through checkpoints versus initializing it with runtime-specific context like a system message. My understanding is that adding the system message during execution helps keep the saved state cleaner and more reusable, but I’d also like to know if there’s a recommended pattern for cases where the system prompt should be part of the initial state. Looking forward to hearing the community’s perspective.

hi @justOleh and @matthew

yes, this is an interesting thing.

Why a schema and not an instance?

StateGraph takes a type because at build time LangGraph only needs the shape of the state - which keys exist and which reducer each one uses. The graph is compiled once per application, but state values live per thread. One compiled graph serves thousands of conversations, each with its own checkpointed state, so initial values cannot belong to the builder - they belong to the thread.

You actually can start a graph in a certain state.
The input to the first invoke on a thread is the initial state. You can pass a SystemMessage as the first message:

graph.invoke(
    {"messages": [SystemMessage(content="You are a helpful assistant."), HumanMessage(content="Hi!")]},
    config,
)

With the course’s operator.add reducer (or the recommended add_messages one) it gets checkpointed once and stays at position 0 for all later turns - just note add_messages is preferred nowadays since it deduplicates by message ID. And if you want to seed state before any run at all, use graph.update_state(config, values) - it writes a checkpoint as if a node had produced those values, then you invoke normally.

So why does the course add the system message on every LLM call?

It is a config vs data separation. If the prompt is baked into checkpointed state, it is frozen at thread creation - ship a v2 of your prompt and every existing conversation still runs the old one. Prepending it transiently at call time (note the course code never returns the system message into state, so the reducer never duplicates it) keeps checkpoints clean: they store only what the user and model actually said, so you can replay a thread under a different prompt or model. It also allows different prompts per node over the same shared messages.

The patterns:

  1. Runtime context, langgraph >= 0.6 - exactly the feature you are asking for: run-scoped values that are not checkpointed:
@dataclass
class Context:
    system_prompt: str

def call_model(state: AgentState, runtime: Runtime[Context]):
    messages = [SystemMessage(content=runtime.context.system_prompt)] + state["messages"]
    return {"messages": [model.invoke(messages)]}

builder = StateGraph(AgentState, context_schema=Context)
graph.invoke({"messages": [...]}, context=Context(system_prompt="You are..."))
  1. With create_agent you do not hand-roll this at all: the system_prompt parameter for a static prompt, or the dynamic_prompt middleware for one that reads state, runtime context or the store.

initial state per thread goes through the first invoke input or update_state; the system prompt should stay out of state - use context_schema plus Runtime, or system_prompt / dynamic_prompt on create_agent.

Docs: Context overview - Docs by LangChain and Context engineering in agents - Docs by LangChain

Hi @pawel-twardziak,

Thank you for explanation and info.
Quite useful information, not all clear yet, but hopefully will make learning more efficient.
Many thanks!

hi @justOleh

happy to help. Feel free to ask if you have any questions.