How OrchestratorState Is Defined in OpenMAIC's LangGraph Implementation
OpenMAIC defines OrchestratorState using LangGraph's Annotation.Root API to structure both immutable input fields and mutable conversation state that nodes update during multi-agent orchestration.
The THU-MAIC/OpenMAIC repository implements a sophisticated multi-agent conversation system built on LangGraph. At its core lies a carefully designed state object that controls how the director selects agents and how agent_generate nodes produce responses. Understanding this state definition is essential for anyone extending or debugging the orchestration logic.
Overview of the State Architecture
OpenMAIC's orchestration relies on LangGraph's StateGraph pattern. The OrchestratorState declaration in lib/orchestration/director-graph.ts serves as the contract between the graph's nodes, determining what data flows through the system and how that data evolves.
The state splits into two conceptual categories:
- Input-only fields — Set once during graph initialization and never modified by nodes
- Mutable fields — Updated by the director and agent nodes as the conversation progresses
Defining OrchestratorState with Annotation.Root
The complete state definition uses TypeScript with LangGraph's annotation system:
// lib/orchestration/director-graph.ts
const OrchestratorState = Annotation.Root({
// ── Input (set once) ──
messages: Annotation<StatelessChatRequest['messages']>,
storeState: Annotation<StatelessChatRequest['storeState']>,
availableAgentIds: Annotation<string[]>,
languageModel: Annotation<LanguageModel>,
thinkingConfig: Annotation<ThinkingConfig | null>,
discussionContext: Annotation<{ topic: string; prompt?: string } | null>,
triggerAgentId: Annotation<string | null>,
userProfile: Annotation<{ nickname?: string; bio?: string } | null>,
agentConfigOverrides: Annotation<Record<string, AgentConfig>>,
// ── Mutable (updated by nodes) ──
currentAgentId: Annotation<string | null>,
turnCount: Annotation<number>,
agentResponses: Annotation<AgentTurnSummary[]>({
reducer: (prev, update) => [...prev, ...update],
default: () => [],
}),
whiteboardLedger: Annotation<WhiteboardActionRecord[]>({
reducer: (prev, update) => [...prev, ...update],
default: () => [],
}),
shouldEnd: Annotation<boolean>,
totalActions: Annotation<number>,
});
type OrchestratorStateType = typeof OrchestratorState.State;
The Annotation.Root wrapper creates a type-safe schema that LangGraph uses to validate state transformations at runtime.
Input-Only Fields in OrchestratorState
These fields originate from the client request and remain constant throughout graph execution:
| Field | Purpose | Source Type |
|---|---|---|
messages |
Conversation history for context | StatelessChatRequest['messages'] |
storeState |
Persistent storage state | StatelessChatRequest['storeState'] |
availableAgentIds |
Pool of agents the director can select | string[] |
languageModel |
LLM instance for generation | LanguageModel |
thinkingConfig |
Optional reasoning configuration | ThinkingConfig | null |
discussionContext |
Topic and optional prompt override | { topic: string; prompt?: string } | null |
triggerAgentId |
Specific agent to force first | string | null |
userProfile |
User metadata for personalization | { nickname?: string; bio?: string } | null |
agentConfigOverrides |
Per-agent configuration patches | Record<string, AgentConfig> |
buildInitialState populates these fields from the incoming StatelessChatRequest before the graph executes.
Mutable Fields with Reducers
Mutable fields use reducer functions to specify how node outputs merge into existing state:
agentResponses— AccumulatesAgentTurnSummaryobjects via array spreadwhiteboardLedger— RecordsWhiteboardActionRecordentries with identical append logiccurrentAgentId— Tracks which agent is currently speaking (set by director, cleared after agent finishes)turnCount— Increments with each conversation turnshouldEnd— Boolean flag for early terminationtotalActions— Counter for whiteboard actions performed
The reducer pattern ensures deterministic state updates when multiple nodes contribute to the same field.
Wiring OrchestratorState into the LangGraph
The state definition drives graph construction in createOrchestrationGraph():
export function createOrchestrationGraph() {
const graph = new StateGraph(OrchestratorState)
.addNode('director', directorNode)
.addNode('agent_generate', agentGenerateNode)
.addEdge(START, 'director')
.addConditionalEdges('director', directorCondition, {
agent_generate: 'agent_generate',
[END]: END,
})
.addEdge('agent_generate', END);
return graph.compile();
}
LangGraph uses OrchestratorState to:
- Type-check node input/output shapes
- Apply reducers when merging parallel node results
- Validate that
buildInitialStateprovides required fields
Practical Usage Example
Execute a single orchestration turn from request to response:
import { createOrchestrationGraph, buildInitialState } from '@/lib/orchestration/director-graph';
import { someLanguageModel } from '@/lib/models';
// 1️⃣ Build the LangGraph state from an incoming request
const initialState = buildInitialState(chatRequest, someLanguageModel, thinkingConfig);
// 2️⃣ Compile the orchestration graph
const orchestration = createOrchestrationGraph();
// 3️⃣ Execute one turn (director → possible agent → END)
await orchestration.invoke(initialState);
Inspect mutable results after execution:
const result = await orchestration.invoke(initialState);
console.log('Agent that spoke:', result.currentAgentId); // null after agent finishes
console.log('All agent responses so far:', result.agentResponses);
console.log('Should the conversation end?', result.shouldEnd);
Key Source Files
| File | Role |
|---|---|
lib/orchestration/director-graph.ts |
Defines OrchestratorState, director/agent nodes, and graph construction |
lib/orchestration/registry/store.ts |
Global agent registry that resolveAgent consults |
lib/orchestration/registry/types.ts |
AgentConfig type referenced in state |
lib/orchestration/prompt-builder.ts |
Prompt generation based on state for the director node |
Summary
- OrchestratorState uses
Annotation.Rootto declare a typed LangGraph state object inlib/orchestration/director-graph.ts - Input fields (
messages,availableAgentIds,languageModel, etc.) are populated once bybuildInitialState - Mutable fields (
agentResponses,whiteboardLedger,currentAgentId, etc.) use reducers to accumulate updates across nodes - The state schema enables LangGraph to orchestrate the director → agent_generate flow with compile-time type safety
Frequently Asked Questions
How does OpenMAIC distinguish between input and mutable fields in OrchestratorState?
OpenMAIC uses LangGraph's Annotation API without reducers for input fields, while mutable fields specify reducer functions. Input fields like messages and languageModel have no reducer—nodes cannot modify them. Fields like agentResponses include reducers that append new values to existing arrays, enabling accumulation across graph steps.
What is the purpose of the whiteboardLedger field in OrchestratorState?
The whiteboardLedger field records structured actions agents perform during conversation turns. It uses a reducer that spreads previous and updated arrays: (prev, update) => [...prev, ...update]. This creates an append-only log of WhiteboardActionRecord entries, enabling debugging and state reconstruction of agent tool usage.
How does the director node communicate which agent should speak next?
The director node sets currentAgentId in its return value. LangGraph merges this into state via the default assignment behavior (no reducer needed for single values). If shouldEnd is true or no agent is selected, the directorCondition routing function returns END instead of routing to agent_generate.
Can OrchestratorState be extended with custom fields for new agent capabilities?
Yes. Add fields to the Annotation.Root declaration in lib/orchestration/director-graph.ts, then update buildInitialState to populate any required initial values. For fields that accumulate across turns, include a reducer function. The TypeScript compiler will flag any nodes that fail to return required fields.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →