Concepts

State schema

BaseState channels, reducers, and LangGraph managed values shared across hierarchy graphs.

Every graph in langgraph-hierarchies shares a common state schema. Nodes read and write channels on this schema; reducers define how concurrent writes merge.

BaseState

BaseState is a TypedDict declared in langgraph_hierarchies.state.schema. Pass it as state_schema when constructing any graph factory:

from langgraph_hierarchies.state.schema import BaseState, create_base_state_defaults

orchestrator = OrchestratorAgent(
    state_schema=BaseState,
    context_schema=BaseContext,
)

Channels

ChannelReducerPurpose
messagesmerge by message IDConversation history for the current agent
todo_listdict mergeActive TODO items for the current agent
todo_listsnamed dict merge(reserved — no nodes in this release read or write this field)
chat_with_operatormerge by message ID(reserved — no nodes in this release read or write this field)
current_agent_argslast-write winsTask args passed when a subagent is invoked
current_agent_reportlast-write winsReport written by an agent before returning to its supervisor
current_tool_calllast-write winsTool call that triggered a subagent invocation
is_finishedlogical ORRoot task completed (finish_task)
is_cancelledlogical OR(reserved — no nodes in this release read or write this field)
progresshighest counts winPer-agent execution counters
iteration_numberlast-write winsCurrent ReAct iteration
max_iterationslast-write winsIteration budget for the current agent
file_refsmerge by ID(reserved — no nodes in this release read or write this field)
__subagent_stack__last-write winsStack of parent state snapshots during nested invocation
remaining_steps(managed)LangGraph global recursion budget — see below

reduce_messages merges by message id: matching IDs are replaced, new IDs are appended. Messages without an ID receive a generated UUID. This lets parallel tool branches update the same conversation without duplicating entries.

Internal channel used by CompiledGraph to snapshot parent state on subagent entry and restore it on exit. You normally do not write to this channel from your own nodes. See Subagents.

Root defaults

create_base_state_defaults() returns empty values for every channel except remaining_steps (which LangGraph manages):

from langgraph_hierarchies.state.schema import create_base_state_defaults

root = orchestrator.compile_as_root(
    state_defaults=create_base_state_defaults(),
)

result = root.invoke(
    create_base_state_defaults(),
    config=RunnableConfig(recursion_limit=50),
    context=build_context(),
)

Pass RunnableConfig at invocation time to set LangGraph’s global recursion_limit (see LangGraph recursion budget below).

When compiled with compile_as_root(), the wrapper’s after_entry_hook fills any missing keys from state_defaults before the first node runs.

Iteration budget (max_iterations)

Each ReAct agent tracks how many reasoning steps it has taken via iteration_number and enforces a per-agent cap via max_iterations. This is the iteration budget you typically configure.

Set the cap when constructing a ReactGraph:

agent = ReactGraph(
    name="researcher",
    max_iterations=20,
    ...
)

SubagentPolicy.max_iterations can override the cap when a subagent is invoked. Each compiled subgraph maintains its own independent iteration_number / max_iterations pair — a parent and child do not share a single ReAct iteration counter at the state level.

LangGraph recursion budget (remaining_steps)

remaining_steps is annotated with LangGraph’s RemainingSteps managed value:

from langgraph.managed import RemainingSteps

class BaseState(TypedDict):
    ...
    remaining_steps: RemainingSteps

LangGraph owns this channel. It is populated from recursion_limit in RunnableConfig and decremented on every node execution across the graph — not just ReAct reasoning steps. You may read it inside nodes or routing functions to detect an approaching limit, but you must never include it in node return values or Send payloads — LangGraph will log a warning and ignore the write.

Each compiled subgraph maintains its own independent remaining_steps counter. Tune per-agent reasoning limits with max_iterations; use recursion_limit only as a global safety net for total node executions.

Runtime context (separate from state)

Mutable state flows through graph channels. Immutable runtime dependencies (model, file store, thread ID) live in BaseContext and are injected via context= at invocation time:

from langgraph_hierarchies.state.context import BaseContext

@dataclass
class BaseContext:
    thread_id: str = ""
    model: BaseChatModel | None = None
    ...

CompiledGraph merges Runtime(context=...) into the LangGraph config so runtime.context.model is available in every node. See CompiledGraph.

Extending state

Subclass or extend BaseState with additional Annotated fields and custom reducers when your hierarchy needs domain-specific channels (for example pipeline_artifact). Keep cross-boundary handoff fields explicit — prefer merging named artifact keys via SubagentPolicy.merge_fields over passing full message history.