How OpenMAIC Uses LangGraph for Multi-Agent Orchestration: A Deep Dive into Stateless StateGraph Design
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. 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:
// 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 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) 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:
- Prompt construction:
buildStructuredPromptcreates the system prompt - Message conversion: Chat history transforms to OpenAI-compatible format
- Streaming execution: Raw LLM chunks flow through a partial-JSON parser
- Event emission: Parsed content streams as
text_delta,action, andagent_endevents
The partial-JSON parser (parseStructuredChunk in 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.
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 demonstrates this pattern:
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
ENDnode 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:
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 |
StateGraph definition, node implementations, state annotations |
lib/orchestration/stateless-generate.ts |
Entry point, graph invocation, partial-JSON streaming |
lib/orchestration/ai-sdk-adapter.ts |
Adapter for ai-compatible language models |
lib/orchestration/prompt-builder.ts |
System prompt construction for agents and director |
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, andagent_endevents 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 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.
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 →