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 — Accumulates AgentTurnSummary objects via array spread
  • whiteboardLedger — Records WhiteboardActionRecord entries with identical append logic
  • currentAgentId — Tracks which agent is currently speaking (set by director, cleared after agent finishes)
  • turnCount — Increments with each conversation turn
  • shouldEnd — Boolean flag for early termination
  • totalActions — 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:

  1. Type-check node input/output shapes
  2. Apply reducers when merging parallel node results
  3. Validate that buildInitialState provides 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.Root to declare a typed LangGraph state object in lib/orchestration/director-graph.ts
  • Input fields (messages, availableAgentIds, languageModel, etc.) are populated once by buildInitialState
  • 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →