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

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.

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, 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, 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, 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 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 (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

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:

// 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:

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 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, 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, 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 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. 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 for agent definitions and lib/orchestration/ai-sdk-adapter.ts for model integration.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →