How OpenMAIC Uses LangGraph for Multi-Agent Orchestration
OpenMAIC implements deterministic multi-agent coordination using LangGraph's StateGraph to route conversations between a director node and agent generation nodes in a single-pass, stateless architecture.
OpenMAIC is an open-source framework that leverages LangGraph to manage complex multi-agent chat workflows. The system implements a deterministic state machine where a director node decides which agent speaks next, followed by an agent generation node that streams structured responses back to the client. This architecture ensures predictable execution while maintaining flexibility for both single-agent and multi-agent scenarios without requiring persistent server-side state.
The Two-Stage Orchestration Architecture
OpenMAIC's LangGraph implementation centers on a strict two-node topology that separates decision-making from generation. This design enforces a single-round contract per request, eliminating the need for explicit turn limits while maintaining deterministic execution.
Director Node
The director node (implemented in lib/orchestration/director-graph.ts) determines which participant speaks next. It handles three distinct execution paths:
- Single-agent fast-path: Uses deterministic code without LLM calls when only one agent is configured.
- Trigger-agent shortcut: Immediately routes to a specified trigger agent on the first turn.
- LLM-driven selection: Invokes a language model via the
AISdkLangGraphAdapterto select the next speaker based on conversation context.
The director streams reasoning events—specifically thinking and cue_user types—through LangGraph's custom stream mode via config.writer, allowing real-time visibility into the orchestration logic.
Agent-Generate Node
Once the director selects an agent, control passes to the agent-generate node. This node:
- Constructs a system prompt using
buildStructuredPrompt - Converts chat history into OpenAI-compatible message formats
- Streams LLM output while parsing partial JSON chunks via
parseStructuredChunk - Emits structured events including
text_delta,action, andagent_endto the client
The partial-JSON parser interleaves text content with executable actions, enabling the front-end to render responses incrementally while preparing function calls.
Graph Topology and Execution Flow
The orchestration graph follows a strict linear topology with one conditional branch. As defined in lib/orchestration/director-graph.ts, the compiled StateGraph enforces the following execution contract:
START → director ──(end)──→ END
│
└─(next)→ agent_generate ──→ END
Each request executes at most one director → agent_generate cycle. The client serializes multiple requests to continue longer discussions, making the topology itself responsible for bounding conversation length rather than explicit maxTurns counters.
Building the Compiled Graph
The graph construction uses LangGraph's StateGraph class with explicit state annotations:
// 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 directorCondition function evaluates the current state to determine whether to route to agent_generate or terminate the graph at END.
State Management with Annotations
OpenMAIC uses LangGraph's Annotation.Root system to define type-safe state schemas (lines 48-77 of director-graph.ts). The state separates immutable inputs from mutable execution fields:
Immutable inputs include:
messages: Chat history arrayagents: Available agent configurationslanguageModel: The LLM instance provided per-request
Mutable execution fields include:
currentAgentId: Tracks which agent is currently generatingturnCount: Monitors conversation depthagentResponses: Accumulates outputs for whiteboard actionsshouldEnd: Boolean flag for early termination
This immutable-by-default approach ensures that each request starts from a clean state defined entirely by the incoming StatelessChatRequest, supporting the framework's stateless architecture.
The Director Decision Logic
The director node implements sophisticated routing logic without sacrificing determinism. For single-agent scenarios, the director immediately routes to that agent without LLM invocation. For multi-agent scenarios, it first checks for a triggerAgentId in the request configuration—if present on turn zero, it fast-tracks that agent.
When neither shortcut applies, the director:
- Summarizes conversation history (via
conversation-summary.ts) - Constructs a director-specific prompt using
prompt-builder.ts - Invokes the LLM through the
AISdkLangGraphAdapter(lines 150-190) - Parses the decision via
parseDirectorDecision - Streams the reasoning process through
config.writerasthinkingevents before routing
Agent Generation and Streaming Execution
The agent-generate node handles the actual LLM interaction and response parsing. Located in lib/orchestration/stateless-generate.ts, this node:
- Filters actions: Calls
getEffectiveActionsto determine which tools the current agent may invoke - Streams chunks: Consumes the LLM stream and feeds it to
parseStructuredChunk - Maintains parser state: Uses
createParserState()to track partial JSON across chunks - Emits events: Pushes
StatelessEventobjects throughconfig.writerfor real-time client updates
Processing Streamed Responses
The partial-JSON parser handles interleaved text and action items:
import { parseStructuredChunk, createParserState } from '@/lib/orchestration/stateless-generate';
let parser = createParserState();
for await (const chunk of llmStream) {
const parsed = parseStructuredChunk(chunk, parser);
// parsed.textChunks contains incremental text
// parsed.actions contains structured function calls
}
This approach allows agents to emit both conversational text and executable actions in a single streaming response, with the parser maintaining state across network chunks.
Stateless Request Handling
OpenMAIC treats each HTTP request as an isolated LangGraph execution. The entry point in lib/orchestration/stateless-generate.ts constructs fresh state instances for every invocation:
export async function generate(
request: StatelessChatRequest,
languageModel: LanguageModel,
thinkingConfig?: ThinkingConfig,
) {
const graph = createOrchestrationGraph();
const initialState = buildInitialState(request, languageModel, thinkingConfig);
return await graph.invoke(initialState);
}
The buildInitialState function (defined in director-graph.ts) hydrates the graph's initial context from the request payload, including any dynamically generated agent configurations. Because the compiled graph executes fresh with each request, the server maintains no persistent chat state between turns—all conversation history must be provided by the client in each StatelessChatRequest.
Summary
- OpenMAIC uses LangGraph's StateGraph to implement a deterministic two-node orchestration consisting of a director and an agent-generate node.
- The graph topology explicitly bounds execution to at most one agent turn per request, with the client managing multi-turn conversations through subsequent requests.
- State management relies on
Annotation.Rootto separate immutable request data from mutable execution state, enabling completely stateless server operation. - The director node implements fast-paths for single-agent and trigger-agent scenarios, falling back to LLM-driven selection only when necessary.
- Streaming architecture uses partial-JSON parsing to interleave text deltas and structured actions, emitting real-time events via LangGraph's stream configuration.
- All core logic resides in
lib/orchestration/director-graph.tsandlib/orchestration/stateless-generate.ts, with adapters bridging to the Vercel AI SDK for model compatibility.
Frequently Asked Questions
How does OpenMAIC handle conversation history without server-side state?
OpenMAIC maintains statelessness by requiring the client to provide complete conversation history in each StatelessChatRequest. The buildInitialState function hydrates the LangGraph state from this request payload, and the graph executes as a single pass. When the graph reaches the END node, the server returns the response and discards all state, requiring the client to include previous messages in subsequent requests.
What determines whether the director uses an LLM or code-based routing?
The director node (lines 91-122 in director-graph.ts) uses deterministic code paths when only one agent is configured or when a triggerAgentId is specified for the first turn. It invokes an LLM only when multiple agents are available and no trigger override exists. This hybrid approach minimizes latency for simple scenarios while maintaining flexibility for complex multi-agent coordination.
How does the partial-JSON parser handle malformed or incomplete chunks?
The parseStructuredChunk utility (found in stateless-generate.ts) maintains accumulator state via createParserState(), buffering incomplete JSON across stream chunks. It validates structures incrementally, emitting text_delta events for valid content and action objects only when complete JSON objects are received. If a chunk splits mid-object, the parser retains the partial state until the next chunk arrives, ensuring robust handling of network fragmentation.
Can the orchestration graph handle tool calling or function execution?
Yes, the agent-generate node filters available actions through getEffectiveActions and parses structured tool calls via the partial-JSON parser. The streaming architecture emits action events distinct from text_delta events, allowing the client to handle function execution while the agent continues generating text. However, the current topology confines tool results to the single turn—complex multi-step tool workflows would require client-side orchestration to feed results back into subsequent requests.
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 →