# How LangGraph Powers Multi‑Agent Orchestration in OpenMAIC

> Discover how LangGraph powers multi-agent orchestration in OpenMAIC. Learn how its stateful graph engine coordinates AI agent conversations for seamless request routing and streaming responses.

- Repository: [MAIC/OpenMAIC](https://github.com/THU-MAIC/OpenMAIC)
- Tags: how-to-guide
- Published: 2026-09-03

---

**OpenMAIC uses LangGraph's stateful graph engine to coordinate turn‑based conversations across multiple AI agents, routing each request through a director node that selects the next speaker and an agent node that generates streaming responses.**

The THU-MAIC OpenMAIC repository implements a **stateless multi‑agent system** where LangGraph supplies the core orchestration primitives—state annotations, graph topology, conditional edges, and custom streaming hooks. This architecture keeps the server stateless while enabling complex, real‑time dialogues between specialized agents.

## LangGraph's Core Responsibilities in OpenMAIC

### Stateful Graph Foundation

LangGraph provides the **graph execution engine** that manages conversation flow. In [`lib/orchestration/director-graph.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/director-graph.ts), OpenMAIC constructs a `StateGraph` with two primary nodes: `director` and `agent_generate`. The graph processes each request as a single "round" (director → agent → end), with the client responsible for chaining multiple rounds into longer discussions.

### Typed State with Annotations

LangGraph's `Annotation` system defines `OrchestratorState` at `lib/orchestration/director-graph.ts#L48-L78`. This typed state container holds:

- **Static fields**: request payload, available `agents`, `languageModel`
- **Mutable fields**: `turnCount`, `currentAgentId`, `agentResponses`
- **Reducer functions**: for accumulating agent outputs and whiteboard actions

```typescript
// Conceptual structure of OrchestatorState annotations
const OrchestratorState = Annotation.Root({
  request: Annotation<StatelessChatRequest>,
  agents: Annotation<AgentConfig[]>,
  languageModel: Annotation<LanguageModel>,
  turnCount: Annotation<number>,
  currentAgentId: Annotation<string | undefined>,
  agentResponses: Annotation<AgentResponse[]>, // with reducer for accumulation
  // ... additional fields for whiteboard and flow control
});

```

### Node Implementation: Director Logic

The **`director`** node (`lib/orchestration/director-graph.ts#L90-L124`) implements turn‑selection logic:

- **Single‑agent mode**: Pure code path that routes directly to the configured agent
- **Multi‑agent mode**: LLM invocation via LangGraph's streaming APIs to determine which agent (or user) should speak next

The director sets a `shouldEnd` flag that controls graph termination.

### Node Implementation: Agent Generation

The **`agent_generate`** node (`lib/orchestration/director-graph.ts#L30-L44`) executes the selected agent's response generation. It streams three event types through LangGraph's custom writer:

| Event Type | Purpose |
|------------|---------|
| `text_delta` | Incremental text output for real‑time display |
| `action` | Tool or whiteboard actions requested by agent |
| `agent_end` | Signal completion with final state updates |

### Conditional Edge Routing

After the director executes, `directorCondition` (`lib/orchestration/director-graph.ts#L25-L31`) routes the flow:

```typescript
// Simplified conditional edge logic
function directorCondition(state: typeof OrchestratorState.State): "agent_generate" | "__end__" {
  return state.shouldEnd ? "__end__" : "agent_generate";
}

```

This enforces OpenMAIC's **single‑round contract**: each HTTP request processes exactly one director decision and one agent response. Long conversations require sequential client requests with updated `directorState`.

## Streaming Architecture with Custom Writers

LangGraph's **custom stream mode** enables Server‑Sent Events (SSE) for real‑time UI updates. Both nodes receive a `config.writer` callback that pushes `StatelessEvent` chunks immediately:

```typescript
// Stream configuration pattern from director-graph.ts
await graph.stream(initialState, {
  writer: (event: StatelessEvent) => {
    // Forward to SSE client for live thinking stages, partial text, errors
    res.write(`data: ${JSON.stringify(event)}\n\n`);
  },
});

```

This design decouples generation latency from response delivery—the user sees agent "thinking" indicators and incremental output without waiting for full completion.

## Provider Abstraction via AISdkLangGraphAdapter

The **`AISdkLangGraphAdapter`** class in `lib/orchestration/ai-sdk-adapter.ts#L2-L12` bridges LangGraph to the `ai` SDK's `LanguageModel` interface. This adapter exposes:

- **`_generate`** — synchronous completion for single‑shot director decisions
- **`streamGenerate`** — streaming completion for agent response generation

```typescript
// Adapter usage pattern
import { AISdkLangGraphAdapter } from '@/lib/orchestration/ai-sdk-adapter';

// Wrap any ai-compatible model (OpenAI, Anthropic, local, etc.)
const languageModel = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
const adapter = new AISdkLangGraphAdapter(languageModel);

// Use in LangGraph nodes for provider-agnostic LLM calls
const result = await adapter._generate(prompt, options);

```

This abstraction lets OpenMAIC swap LLM providers without modifying orchestration logic.

## Complete Integration Example

```typescript
// lib/orchestration/director-graph.ts — full orchestration setup
import {
  createOrchestrationGraph,
  buildInitialState,
} from '@/lib/orchestration/director-graph';
import { createOpenAI } from '@ai-sdk/openai';

// 1. Configure language model through adapter
const lm = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });

// 2. Prepare stateless request (from client payload)
const request: StatelessChatRequest = {
  messages: [{ role: 'user', content: 'Compare Q1 and Q2 sales forecasts' }],
  storeState: { /* prior conversation context */ },
  config: {
    agentIds: ['data-analyst', 'finance-reviewer'],
    triggerAgentId: 'data-analyst',
  },
  directorState: undefined, // fresh round
};

// 3. Build graph and initial state
const graph = createOrchestrationGraph();
const initState = buildInitialState(request, lm);

// 4. Execute with streaming for live UI updates
const events: StatelessEvent[] = [];
await graph.stream(initState, {
  writer: (event) => {
    events.push(event);
    broadcastToClient(event); // SSE push
  },
});

// 5. Final state contains complete round result
console.log('Selected agent:', events.find(e => e.type === 'agent_start')?.agentId);
console.log('Generated text:', events.filter(e => e.type === 'text_delta').map(e => e.delta).join(''));

```

## Key Design Decisions

**Statelessness at the server level**: Agent configurations, conversation history, and graph state travel with each request. LangGraph's state management operates per‑request, not per‑session. This enables horizontal scaling and eliminates server‑side session storage.

**Single‑round execution model**: The graph always terminates after one agent response. Complex multi‑turn conversations are client‑orchestrated sequences of single‑round requests. This simplifies reasoning about timeouts, retries, and partial failures.

**Structured event streaming**: Rather than raw token streams, LangGraph nodes emit semantic events (`thinking`, `text_delta`, `action`, `error`) that map directly to UI components.

## Summary

- **LangGraph provides the graph engine**: `StateGraph` with `director` and `agent_generate` nodes defined in [`lib/orchestration/director-graph.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/director-graph.ts)
- **Annotations enable typed state**: `OrchestratorState` holds request context, mutable round state, and reducer‑managed accumulators
- **Conditional edges control flow**: `directorCondition` implements single‑round termination based on `shouldEnd` flag
- **Custom writers power SSE streaming**: Real‑time events for thinking stages, partial text, and actions
- **AISdkLangGraphAdapter decouples providers**: Any `ai` SDK `LanguageModel` works with LangGraph's generation APIs

## Frequently Asked Questions

### What makes OpenMAIC's use of LangGraph "stateless"?

The server retains no conversation state between requests. Each HTTP request carries the complete `OrchestratorState`—including message history, agent configs, and prior `directorState`—which LangGraph processes as a fresh graph invocation. The graph itself is stateful during execution (tracking `turnCount`, accumulating responses), but that state is discarded after the single‑round response returns to the client.

### Why does OpenMAIC use single‑round execution instead of persistent graph sessions?

Single‑round execution simplifies deployment and scaling. Timeouts, client disconnections, and server restarts become recoverable—clients retry with the same or updated state. Persistent graph sessions would require sticky routing, session storage, and complex failure handling. The client‑chained round model achieves equivalent conversational depth with simpler operational characteristics.

### How does the director node decide which agent speaks next?

In multi‑agent mode, the director prompts an LLM (via `AISdkLangGraphAdapter`) with the conversation history, available agent descriptions, and turn‑selection instructions. The LLM returns either an `agentId` from the configured pool or a signal to return to the user. The implementation at `lib/orchestration/director-graph.ts#L90-L124` parses this decision and sets `currentAgentId` and `shouldEnd` accordingly.

### Can OpenMAIC work with LLM providers other than OpenAI?

Yes. The `AISdkLangGraphAdapter` in [`lib/orchestration/ai-sdk-adapter.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/orchestration/ai-sdk-adapter.ts) wraps any `ai` SDK‑compatible `LanguageModel`. This includes Anthropic Claude, Google Gemini, local models via Ollama or LM Studio, and custom providers. The orchestration graph operates on the standardized adapter interface, making provider swaps configuration‑only changes.