# How the Multi-Agent Discussion API with SSE Streaming Works in OpenMAIC

> Explore the OpenMAIC multi-agent discussion API and learn how SSE streaming delivers real-time LLM tokens, tool calls, and state patches for autonomous agents.

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

---

**The OpenMAIC multi-agent discussion API streams real-time conversation events through a Server-Sent Events (SSE) endpoint, enabling live display of LLM tokens, tool calls, and state patches across multiple autonomous agents.**

This article explains the complete implementation of the multi-agent discussion API with SSE streaming in [THU-MAIC/OpenMAIC](https://github.com/THU-MAIC/OpenMAIC). We'll trace the flow from the **SSE response constructor** through the **discussion engine** to the **client-side consumption**, referencing exact source files and function signatures from the codebase.

---

## SSE Response Construction

At the heart of the streaming system is **`createSSEResponse`** in [`lib/pbl/v2/api/sse.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/pbl/v2/api/sse.ts). This utility wraps any `ReadableStream` into a properly configured HTTP response with SSE headers.

```typescript
export function createSSEResponse(body: ReadableStream<Uint8Array>) {
  return new Response(body, {
    headers: {
      "Content-Type": "text/event-stream; charset=utf-8",
      "Cache-Control": "no-cache, no-transform",
      "Connection": "keep-alive",
    },
  });
}

```

The headers ensure:
- **No caching**: Clients receive every event immediately
- **Keep-alive connection**: The TCP connection persists for the discussion duration
- **UTF-8 encoding**: Supports international characters in agent responses

---

## Event Type System

All SSE events in the discussion API share a discriminator field `type`. The union type **`PBLSSEEvent`** encompasses every possible event, defined alongside specialized interfaces in [`lib/pbl/v2/api/sse.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/pbl/v2/api/sse.ts).

### Core Event Types

| Event Type | Interface | Purpose |
|------------|-----------|---------|
| **token** | `SSETokenEvent` | LLM token streaming for "typing" effect |
| **tool-call** | `SSEToolCallEvent` | Agent requests external tool execution |
| **project-patch** | `SSEProjectPatchEvent` | Incremental workspace state updates |
| **sim-phase** | `SSESimPhaseEvent` | Simulation phase transitions |
| **reset-draft** | `SSEResetDraftEvent` | Clear draft content on restart |
| **error** | `SSEErrorEvent` | Fatal or recoverable error reporting |
| **done** | `SSEDoneEvent` | Signal stream completion |

Example payload structure for a token event:

```typescript
interface SSETokenEvent {
  type: "token";
  token: string;  // Individual LLM output token
}

```

The **project-patch** event deserves special attention. It carries `PBLProjectPatch` objects that the client applies atomically to update slides, canvas elements, or other workspace state without full re-renders.

---

## Multi-Agent Discussion Engine

The discussion orchestration resides in **[`lib/pbl/v2/runtime/discussion.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/pbl/v2/runtime/discussion.ts)**. The **`runDiscussion`** function implements the core loop that coordinates multiple agents through sequential LLM invocations.

### Agent Iteration Pattern

For each configured agent, the engine:

1. **Streams tokens** — Pushes `token` events as the LLM generates output
2. **Detects tool calls** — Emits `tool-call` events when the LLM requests external capabilities
3. **Applies results** — Executes tools server-side, then continues the agent's turn
4. **Broadcasts state changes** — Sends `project-patch` events for any workspace modifications

```typescript
// Conceptual flow from discussion.ts implementation
async function* runDiscussion(config: DiscussionConfig) {
  for (const agent of config.agents) {
    yield { type: "sim-phase", phase: `${agent.id}-thinking` };
    
    const stream = await invokeLLM(agent, context);
    for await (const token of stream) {
      yield { type: "token", token };
    }
    
    if (detectToolCall(stream)) {
      yield { type: "tool-call", tool, args };
      const result = await executeTool(tool, args);
      // Continue with tool result injected into context
    }
    
    // Any workspace mutations become patches
    for (const mutation of collectMutations()) {
      yield { type: "project-patch", patch: mutation };
    }
  }
  
  yield { type: "done" };
}

```

---

## DSL Validation and Scene Integration

Before runtime execution, the discussion action undergoes static validation in **`packages/@openmaic/dsl/src/action.ts`**. The **`createDiscussion`** function enforces two critical constraints:

- **Uniqueness**: Only one discussion action per scene
- **Terminal position**: Must be the final action in the scene sequence

These rules prevent invalid multi-agent configurations that could deadlock or produce undefined ordering.

---

## API Route and Stream Instantiation

The public endpoint in **[`pages/api/pbl/v2/discussion.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/pages/api/pbl/v2/discussion.ts)** bridges HTTP requests to the SSE stream:

```typescript
import { createSSEResponse } from "@/lib/pbl/v2/api/sse";
import { runDiscussion } from "@/lib/pbl/v2/runtime/discussion";

export async function POST(request: Request) {
  const body = await request.json();
  const { sceneId, discussionId } = body;
  
  // Create readable stream from async generator
  const stream = new ReadableStream({
    async start(controller) {
      const encoder = new TextEncoder();
      
      for await (const event of runDiscussion({ sceneId, discussionId })) {
        const sseChunk = `data: ${JSON.stringify(event)}\n\n`;
        controller.enqueue(encoder.encode(sseChunk));
      }
      
      controller.close();
    }
  });
  
  return createSSEResponse(stream);
}

```

Note the **`data:`** prefix and **double newline** — mandatory SSE protocol formatting.

---

## Client-Side Consumption

The front-end consumes the multi-agent discussion API using the browser's **`EventSource`** interface or a fetch-based polyfill for advanced control.

### React Hook Implementation

```typescript
import { useEffect, useRef } from "react";
import type { PBLSSEvent } from "@/lib/pbl/v2/api/sse";

export function useDiscussionSSE(url: string, handlers: EventHandlers) {
  const handlersRef = useRef(handlers);
  handlersRef.current = handlers;

  useEffect(() => {
    const es = new EventSource(url);
    
    es.onmessage = (e) => {
      const data: PBLSSEvent = JSON.parse(e.data);
      
      switch (data.type) {
        case "token":
          handlersRef.current.onToken?.(data.token);
          break;
        case "tool-call":
          handlersRef.current.onToolCall?.(data.tool, data.args);
          break;
        case "project-patch":
          handlersRef.current.onPatch?.(data.patch);
          break;
        case "error":
          handlersRef.current.onError?.(data.code, data.message);
          break;
        case "done":
          es.close();
          handlersRef.current.onComplete?.();
          break;
      }
    };
    
    es.onerror = (err) => {
      console.error("SSE connection failed", err);
      // Auto-retry or degrade based on error type
    };
    
    return () => es.close();
  }, [url]);
}

```

### Unidirectional Design Consideration

Because SSE is **server-to-client only**, user interactions (join, skip, inject message) require separate HTTP POST requests or WebSocket messages. The discussion controller correlates these actions with the active SSE stream through session-scoped state management.

---

## Error Handling and Resilience

The **`error`** event type supports two recovery modes:

- **Recoverable errors**: Client displays toast, stream continues
- **Fatal errors**: Client closes connection, user must restart discussion

Error codes (defined in `SSEErrorEvent`) enable programmatic handling:
- `RATE_LIMITED` — Back off and retry
- `AGENT_TIMEOUT` — Skip current agent, continue with next
- `INVALID_STATE` — Discussion corrupted, restart required

---

## Summary

- **`createSSEResponse`** in [`lib/pbl/v2/api/sse.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/pbl/v2/api/sse.ts) constructs properly headered SSE responses from any `ReadableStream`
- **`PBLSSEvent`** union type provides seven discriminated events: `token`, `tool-call`, `project-patch`, `sim-phase`, `reset-draft`, `error`, and `done`
- **`runDiscussion`** in [`lib/pbl/v2/runtime/discussion.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/pbl/v2/runtime/discussion.ts) orchestrates sequential agent turns, yielding events for token streaming and tool execution
- **`createDiscussion`** enforces DSL constraints: one discussion per scene, always terminal
- The API route transforms the async generator into SSE-formatted chunks with `data:` prefixes
- Clients use `EventSource` for real-time UI updates, with separate channels for user-initiated actions

---

## Frequently Asked Questions

### What is the advantage of SSE over WebSockets for multi-agent discussions?

**SSE provides automatic reconnection, simpler one-directional semantics, and native browser support without additional libraries.** Since OpenMAIC's discussion flow is primarily server-to-client (agents generate content, client displays it), SSE's unidirectional design matches the architecture perfectly. WebSockets would add unnecessary complexity for bidirectional needs that HTTP POST requests already satisfy.

### How does the client handle dropped SSE connections mid-discussion?

**The `EventSource` interface automatically attempts reconnection with exponential backoff.** On reconnect, the client receives a `reset-draft` event if the server cleared state, or resumes from the last checkpoint. The client can also detect sequence gaps through event ordering and request a full state refresh via separate API call.

### Can tool calls block other agents from proceeding?

**No, the discussion engine processes tool calls within a single agent's turn without blocking the overall stream.** The `tool-call` event notifies the client of pending work, the server executes asynchronously, and results feed back into the same agent's context. Other agents wait only for the current agent's complete turn, not individual tool latencies.

### Where are the agent personalities and discussion configurations defined?

**Agent configurations reside in the scene definition processed by `createDiscussion` in `packages/@openmaic/dsl/src/action.ts`.** Each agent specifies a system prompt, available tools, and turn-taking rules. The runtime in [`lib/pbl/v2/runtime/discussion.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/pbl/v2/runtime/discussion.ts) instantiates these configurations into executable agent loops.