# How OrchestratorState Is Defined in OpenMAIC's LangGraph Implementation

> Discover how OpenMAIC defines OrchestratorState using LangGraph's Annotation.Root API to manage immutable inputs and mutable conversation state in multi-agent orchestration.

- Repository: [MAIC/OpenMAIC](https://github.com/THU-MAIC/OpenMAIC)
- Tags: internals
- Published: 2026-09-03

---

**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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/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:

```typescript
// 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()`:

```typescript
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:

```typescript
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:

```typescript
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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/director-graph.ts) | Defines `OrchestratorState`, director/agent nodes, and graph construction |
| [`lib/orchestration/registry/store.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/registry/store.ts) | Global agent registry that `resolveAgent` consults |
| [`lib/orchestration/registry/types.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/registry/types.ts) | `AgentConfig` type referenced in state |
| [`lib/orchestration/prompt-builder.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/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.