# How OpenMAIC Uses LangGraph for Multi-Agent Orchestration: A Deep Dive into the StateGraph Implementation

> Discover how OpenMAIC uses LangGraph StateGraph for deterministic agent orchestration. Explore the director node and LLM execution in this deep dive.

- Repository: [MAIC/OpenMAIC](https://github.com/THU-MAIC/OpenMAIC)
- Tags: deep-dive
- Published: 2026-08-30

---

**OpenMAIC leverages LangGraph's StateGraph to implement a deterministic, state-driven orchestration loop where a director node decides which agent speaks next and an agent-generate node executes LLM-driven responses, with all logic centralized in [`lib/orchestration/director-graph.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/director-graph.ts).**

OpenMAIC is a multi-agent chat system that uses **LangGraph agent orchestration** to manage complex conversational flows between multiple AI agents. By building on LangGraph's `StateGraph` abstraction, the platform achieves explicit control over state transitions while allowing LLMs to act strictly as decision engines when routing is required. The orchestration layer is implemented in TypeScript and relies on a typed state schema to manage conversation context across turns.

## LangGraph StateGraph Architecture in OpenMAIC

The orchestration layer centers on [`lib/orchestration/director-graph.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/director-graph.ts), which defines the `OrchestratorState` and wires together the graph topology. This file implements a deterministic loop where each execution cycle represents one turn in the conversation.

### Defining the OrchestratorState Schema

The graph state is created using **LangGraph's `Annotation.Root`** to define a strongly-typed `OrchestratorState`. According to the source code in lines 48-77 of [`director-graph.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/director-graph.ts), this state holds immutable request data—such as the message history, model configuration, and available agent list—alongside mutable fields that nodes update during execution, including `currentAgentId`, `turnCount`, and `agentResponses`.

The `Annotation` system ensures type safety throughout graph execution, allowing nodes to read configuration constants while updating only the fields they explicitly control.

### The Director Node Decision Logic

The **director node** functions as the traffic controller for the multi-agent system. Implemented between lines 91-123 and 150-190 of [`director-graph.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/director-graph.ts), this node determines which agent should speak next or whether to cue the user.

For **single-agent** conversations, the director skips LLM calls entirely. It dispatches the sole agent on turn zero, then immediately cues the user for follow-up, avoiding unnecessary inference overhead.

For **multi-agent** scenarios, the director follows a conditional path:

1. If a `triggerAgentId` is provided in the request, it fast-paths the first turn to that specific agent.
2. Otherwise, it constructs a system prompt via `buildDirectorPrompt` and invokes the language model through `AISdkLangGraphAdapter`.
3. The LLM response is parsed by `parseDirectorDecision` to extract either the next `agentId`, a `USER` cue signal, or an end-of-conversation flag.

Throughout this process, the director streams intermediate events—such as `thinking` status updates and `cue_user` notifications—to the client using `StatelessEvent` chunks.

### The Agent-Generate Node Execution

Once the director selects an agent, the graph transitions to the **agent-generate node**, implemented in `runAgentGeneration` (lines 335-440). This node handles the actual LLM interaction for the selected agent.

The execution flow retrieves the agent configuration via `resolveAgent`, which merges global definitions from [`lib/orchestration/registry/store.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/registry/store.ts) with request-scoped overrides. It then constructs a structured system prompt using `buildStructuredPrompt`, injecting the agent's personality, allowed actions, conversation context, and whiteboard ledger.

The node streams model output chunk-by-chunk through `adapter.streamGenerate`, parsing structured JSON fragments with `parseStructuredChunk`. It emits three primary event types:

- `text_delta` for incremental text generation
- `action` for detected tool calls or whiteboard operations
- `agent_end` to signal completion

Whiteboard actions are recorded during this phase for later replay, enabling persistent shared state across agent turns.

### Graph Topology and Workflow

The function `createOrchestrationGraph` assembles the topology at the end of [`director-graph.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/director-graph.ts) (lines 84-95). The compiled graph follows this deterministic flow:

```

START → director ──(end)──→ END
            │
            └─(next)→ agent_generate ──→ END

```

The conditional routing is handled by `directorCondition`, which checks the director's output to determine whether to proceed to `agent_generate` or terminate the graph. Notably, the graph executes **a single director→agent cycle per request**, with the client responsible for serializing multiple requests to achieve multi-turn dialogues. This design choice simplifies server-side state management while maintaining conversational continuity through the client-side state object.

## Implementing the Orchestration Graph

The following examples demonstrate how to instantiate and run the orchestration graph in an OpenMAIC deployment.

### Creating and Invoking the Graph

```typescript
import { createOrchestrationGraph, buildInitialState } from '@/lib/orchestration/director-graph';
import { myLanguageModel } from '@/lib/models';

// Build the initial graph state from the incoming request.
const initialState = buildInitialState(chatRequest, myLanguageModel);

// Compile the graph once per server instance and run execution.
const graph = createOrchestrationGraph();
const result = await graph.invoke(initialState, {
  // writer is an SSE-compatible callback used by director and agent nodes.
  writer: (event) => sseStream.write(JSON.stringify(event)),
});

```

### Director Decision Implementation

Inside the `directorNode` function, the decision-making logic integrates with the AI SDK adapter:

```typescript
// Prepare the director prompt...
const adapter = new AISdkLangGraphAdapter(state.languageModel, state.thinkingConfig);
const result = await adapter._generate(
  [new SystemMessage(prompt), new HumanMessage('Decide which agent should speak next.')],
  { signal: config.signal },
);

const decision = parseDirectorDecision(result.generations[0]?.text ?? '');
if (decision.shouldEnd) return { shouldEnd: true };

if (decision.nextAgentId === 'USER') {
  write({ type: 'cue_user', data: { fromAgentId: state.currentAgentId } });
  return { shouldEnd: true };
}

return { currentAgentId: decision.nextAgentId, shouldEnd: false };

```

### Streaming Agent Generation

The `runAgentGeneration` function handles real-time streaming of agent responses:

```typescript
for await (const chunk of adapter.streamGenerate(lcMessages, { signal: config.signal })) {
  if (chunk.type === 'delta') {
    const parseResult = parseStructuredChunk(chunk.content, parserState);
    
    // Emit text deltas
    for (const entry of parseResult.ordered) {
      if (entry.type === 'text') {
        const text = parseResult.textChunks[entry.index].replace(/^>+\s?/gm, '');
        write({ type: 'text_delta', data: { content: text, messageId } });
      }
      
      // Emit action events for tool use
      if (entry.type === 'action') {
        const ac = parseResult.actions[entry.index];
        write({ type: 'action', data: { ...ac, agentId, messageId } });
      }
    }
  }
}

```

## Summary

OpenMAIC's **LangGraph agent orchestration** implementation provides a robust, type-safe framework for multi-agent conversations:

- **StateGraph Foundation**: Uses `Annotation.Root` in [`lib/orchestration/director-graph.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/director-graph.ts) to define a strongly-typed `OrchestratorState` that separates immutable request data from mutable turn-specific fields.
- **Deterministic Control Flow**: The director node makes explicit routing decisions—either fast-pathing single agents, parsing LLM decisions for multi-agent routing, or terminating when cueing the user.
- **Streaming Architecture**: Both director and agent-generate nodes emit real-time events (`thinking`, `text_delta`, `action`) through Server-Sent Events (SSE), enabling responsive UIs.
- **Modular Agent Registry**: Agent configurations are resolved via [`lib/orchestration/registry/store.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/registry/store.ts), supporting global defaults and per-request overrides through `resolveAgent`.
- **Single-Cycle Execution**: The compiled graph runs one director→agent cycle per invocation, with state persistence handled externally to simplify server logic.

## Frequently Asked Questions

### What is the role of the director node in OpenMAIC's LangGraph implementation?

The **director node** serves as the orchestration controller that determines which agent speaks next or when to return control to the user. Located in [`lib/orchestration/director-graph.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/director-graph.ts), it either fast-paths single-agent conversations without LLM calls or constructs prompts to query the language model for multi-agent routing decisions. It streams `thinking` events during deliberation and emits `cue_user` signals when the conversation requires human input.

### How does OpenMAIC handle state management across multiple conversation turns?

OpenMAIC manages state through LangGraph's `StateGraph` pattern with a defined `OrchestratorState` interface. However, rather than maintaining long-running server-side sessions, the graph executes a **single cycle per request**. The client serializes the state object between turns, sending it back to the server with each new user message. This approach minimizes server complexity while leveraging LangGraph's type-safe state reduction within individual executions.

### Can developers customize agent behavior without modifying the core orchestration logic?

Yes. Agent configurations reside in [`lib/orchestration/registry/store.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/registry/store.ts) as global definitions, but the `resolveAgent` function merges these with request-scoped overrides. Developers can modify agent personalities, allowed actions, and system prompts through the registry or by passing override parameters in the chat request. The orchestration graph remains agnostic to these specifics, processing agents generically through the `buildStructuredPrompt` and `runAgentGeneration` functions.

### What file contains the main LangGraph orchestration logic for OpenMAIC?

The core orchestration logic is centralized in **[`lib/orchestration/director-graph.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/director-graph.ts)**. This file contains the state definition (`OrchestratorState`), the director node implementation, the agent-generate node (`runAgentGeneration`), and the `createOrchestrationGraph` function that wires the topology. Supporting files include [`lib/orchestration/registry/store.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/registry/store.ts) for agent definitions and [`lib/orchestration/ai-sdk-adapter.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/ai-sdk-adapter.ts) for model integration.