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

> Discover how OpenMAIC leverages LangGraph's StateGraph for efficient multi-agent orchestration. Learn about its stateless design and event streaming for deterministic workflows.

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

---

**OpenMAIC implements deterministic multi-agent chat workflows using LangGraph's `StateGraph`, with a two-node topology (director → agent_generate) that executes at most one cycle per request and streams fine-grained events via custom stream modes.**

OpenMAIC, the open-source multi-agent AI conversation framework from Tsinghua University's MAIC lab, leverages **LangGraph** to orchestrate complex agent interactions without maintaining server-side session state. This article examines how the codebase structures its orchestration graph, handles streaming generation, and enforces bounded execution cycles.

## The Core LangGraph Architecture

At the heart of OpenMAIC's orchestration is a deliberately simple **StateGraph** defined in [`lib/orchestration/director-graph.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/director-graph.ts). The topology follows a single-round contract that guarantees termination:

```

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

```

This design choice means **each HTTP request executes at most one director → agent cycle**. The client—not the server—handles multi-turn conversations by serializing multiple requests. No `maxTurns` parameter is required because the graph structure itself bounds execution.

### Building and Compiling the Graph

The `createOrchestrationGraph()` function constructs the compiled graph:

```ts
// lib/orchestration/director-graph.ts
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();
}

```

The conditional edge from `director` uses `directorCondition` to route either to `agent_generate` (when an agent should speak) or directly to `END` (for user-directed turns or termination).

## State Definition with Annotation.Root

OpenMAIC uses LangGraph's `Annotation.Root` to declare typed state fields. Lines 48-77 of [`lib/orchestration/director-graph.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/director-graph.ts) define:

- **Immutable inputs**: `messages`, `agentIds`, `languageModel`, `config`
- **Mutable fields**: `currentAgentId`, `turnCount`, `agentResponses`, `shouldEnd`

This explicit immutability boundary prevents accidental state mutations while allowing the graph to track progression across the single-cycle execution.

## The Director Node: Routing Logic

The **director node** (`directorNode`) determines which participant speaks next. Its implementation demonstrates three distinct decision paths:

| Scenario | Behavior |
|----------|----------|
| **Single agent** | Pure code path—no LLM invocation |
| **Multi-agent, first turn with trigger** | Fast-tracks the `triggerAgentId` immediately |
| **Multi-agent, general case** | Invokes LLM via `AISdkLangGraphAdapter` to select next speaker |

The LLM-driven decision uses conversation summarization ([`lib/orchestration/summarizers/conversation-summary.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/summarizers/conversation-summary.ts)) to fit context windows, then parses the response with `parseDirectorDecision`. Lines 91-122 and 150-190 contain the core routing implementations.

Director reasoning is streamed to clients through LangGraph's custom stream mode, emitting `thinking` and `cue_user` events via `config.writer`.

## The Agent-Generate Node: Streaming Generation

Once routed, the **agent-generate node** (`agentGenerateNode`) handles actual LLM inference:

1. **Prompt construction**: `buildStructuredPrompt` creates the system prompt
2. **Message conversion**: Chat history transforms to OpenAI-compatible format
3. **Streaming execution**: Raw LLM chunks flow through a **partial-JSON parser**
4. **Event emission**: Parsed content streams as `text_delta`, `action`, and `agent_end` events

The partial-JSON parser (`parseStructuredChunk` in [`lib/orchestration/stateless-generate.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/stateless-generate.ts), lines 31-44 and 135-176) enables **interleaved text and action items**—critical for agents that must emit structured tool calls alongside natural language.

```ts
import { parseStructuredChunk, createParserState } from '@/lib/orchestration/stateless-generate';

let parser = createParserState();
for await (const chunk of llmStream) {
  const parsed = parseStructuredChunk(chunk, parser);
  // parsed.textChunks and parsed.actions contain newly emitted items
}

```

Actions are filtered through `getEffectiveActions` before recording to the whiteboard, enforcing agent capability boundaries.

## Stateless Execution Model

OpenMAIC's **stateless design** eliminates server-side session storage. The `generate` entry point in [`lib/orchestration/stateless-generate.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/stateless-generate.ts) demonstrates this pattern:

```ts
export async function generate(
  request: StatelessChatRequest,
  languageModel: LanguageModel,
  thinkingConfig?: ThinkingConfig,
) {
  const graph = createOrchestrationGraph();
  const initialState = buildInitialState(request, languageModel, thinkingConfig);
  return await graph.invoke(initialState);
}

```

Each request:
- Creates a fresh graph instance
- Builds complete initial state from `StatelessChatRequest`
- Streams events back to the client
- Discards all state after `END` node completion

This architecture enables horizontal scaling and simplifies deployment—no Redis, no database, no sticky sessions required.

## Trigger Agent Configuration

Multi-agent scenarios support **trigger agents** for immediate routing:

```ts
const request: StatelessChatRequest = {
  messages: [...],
  storeState: ...,
  config: {
    agentIds: ['teacher', 'assistant'],
    triggerAgentId: 'teacher',  // Dispatch immediately on turn 0
  },
};

```

When `triggerAgentId` matches an available agent on the first turn, the director short-circuits LLM deliberation and routes directly to that agent.

## Key Implementation Files

| File | Responsibility |
|------|--------------|
| [`lib/orchestration/director-graph.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/director-graph.ts) | StateGraph definition, node implementations, state annotations |
| [`lib/orchestration/stateless-generate.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/stateless-generate.ts) | Entry point, graph invocation, partial-JSON streaming |
| [`lib/orchestration/ai-sdk-adapter.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/ai-sdk-adapter.ts) | Adapter for `ai`-compatible language models |
| [`lib/orchestration/prompt-builder.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/prompt-builder.ts) | System prompt construction for agents and director |
| [`lib/orchestration/summarizers/conversation-summary.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/summarizers/conversation-summary.ts) | Context compression for director LLM decisions |

## Summary

- **OpenMAIC uses a fixed two-node StateGraph** (`director` → `agent_generate`) with topology-bound execution cycles
- **Stateless by design**: all request context travels in `StatelessChatRequest`, no server-side persistence
- **Streaming-first architecture**: custom stream modes emit `thinking`, `text_delta`, `action`, and `agent_end` events via SSE
- **Flexible routing**: pure-code single-agent paths, trigger-agent shortcuts, and LLM-driven multi-agent selection
- **Partial-JSON parsing** enables structured action interleaving with natural language generation

## Frequently Asked Questions

### What makes OpenMAIC's LangGraph implementation stateless?

OpenMAIC rebuilds the complete LangGraph state from each incoming `StatelessChatRequest` and discards it after the `END` node fires. No session data persists on the server—all conversation history, agent configurations, and turn counters travel with the request payload. This eliminates server-side storage requirements and enables effortless horizontal scaling.

### Why does OpenMAIC limit the graph to a single director → agent cycle?

The single-cycle constraint simplifies reasoning about termination and prevents runaway LLM costs. The graph topology itself guarantees completion without explicit `maxTurns` checks. Clients implement multi-turn conversations by chaining multiple stateless requests, giving application developers explicit control over conversation depth and cost exposure.

### How does the partial-JSON parser handle streaming agent actions?

The `parseStructuredChunk` utility maintains parser state across streaming chunks, detecting complete JSON objects while buffering incomplete ones. It separates `textChunks` (natural language) from `actions` (structured tool calls), allowing the frontend to render streaming text while preparing action handlers. This enables agents to emit interleaved content without waiting for complete generation.

### Can OpenMAIC work with non-OpenAI language models?

Yes. The `AISdkLangGraphAdapter` in [`lib/orchestration/ai-sdk-adapter.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/ai-sdk-adapter.ts) bridges any `ai`-compatible language model to the LangGraph orchestration layer. The adapter handles message format conversion and streaming abstractions, making the core graph implementation model-agnostic.