- Updated message structures to replace 'StreamStartData' and 'StreamEndData' with 'EventStreamStartData' and 'EventStreamEndData' for better clarity and consistency. - Introduced 'EventMessageStartData' and 'EventMessageEndData' to represent individual message lifecycle events, enhancing the granularity of message tracking. - Refactored the 'streamState' and 'groupTracker' to utilize the new message structures, improving the organization and handling of streaming events. - Enhanced the context management by integrating an ID generator for unique message identifiers, facilitating better tracking of message sequences. - Updated documentation and tests to reflect the new message structures and ensure proper functionality across the system.
317 lines
11 KiB
Markdown
317 lines
11 KiB
Markdown
# Message Streaming Architecture
|
|
|
|
This document explains the hierarchical streaming architecture for Agent/LLM/MCP message delivery.
|
|
|
|
## Overview
|
|
|
|
The streaming system uses a hierarchical structure to handle complex scenarios including:
|
|
|
|
- Single LLM calls with multiple message types (thinking, tool calls, text)
|
|
- Agent logic with multiple sequential operations (LLM → MCP → LLM)
|
|
- Concurrent/parallel calls to multiple LLMs or MCPs
|
|
- Real-time delta updates for streaming responses
|
|
|
|
## Hierarchical Structure
|
|
|
|
```
|
|
Agent Stream (entire conversation)
|
|
└─ ThreadID (concurrent stream, optional: T1, T2, T3...)
|
|
└─ BlockID (output block/section: B1, B2, B3...)
|
|
└─ MessageID (logical message: M1, M2, M3...)
|
|
└─ ChunkID (stream fragment: C1, C2, C3...)
|
|
```
|
|
|
|
## Field Definitions
|
|
|
|
### Message Struct Fields
|
|
|
|
```go
|
|
type Message struct {
|
|
// Core fields
|
|
Type string `json:"type"`
|
|
Props map[string]interface{} `json:"props,omitempty"`
|
|
|
|
// Streaming control
|
|
ChunkID string `json:"chunk_id,omitempty"`
|
|
MessageID string `json:"message_id,omitempty"`
|
|
BlockID string `json:"block_id,omitempty"`
|
|
ThreadID string `json:"thread_id,omitempty"`
|
|
|
|
// Delta control
|
|
Delta bool `json:"delta,omitempty"`
|
|
DeltaPath string `json:"delta_path,omitempty"`
|
|
DeltaAction string `json:"delta_action,omitempty"`
|
|
|
|
// ...
|
|
}
|
|
```
|
|
|
|
### Field Responsibilities
|
|
|
|
| Field | Generated By | Purpose | Example Values | Required |
|
|
| ----------- | -------------------- | ---------------------------------- | ------------------------------------ | ----------------------------------- |
|
|
| `ChunkID` | System (auto) | Deduplication, ordering, debugging | `C1`, `C2`, `C3` | Always |
|
|
| `MessageID` | LLM Provider/Handler | Delta merge target | `M1`, `M2`, `M3` or `thinking_msg_1` | Required for delta scenarios |
|
|
| `BlockID` | Agent Logic | UI block/section rendering | `B1`, `B2`, `B3` or `llm_response_1` | Required when Agent controls blocks |
|
|
| `ThreadID` | Agent Logic | Concurrent stream distinction | `T1`, `T2`, `T3` or `thread_llm1` | Optional (concurrent only) |
|
|
|
|
### Detailed Field Explanation
|
|
|
|
#### ChunkID (Stream Fragment Identifier)
|
|
|
|
- **Purpose**: Uniquely identifies each chunk in the stream
|
|
- **Generated**: Automatically by the system (sequential: M1, M2, M3...)
|
|
- **Used For**:
|
|
- Deduplication (prevent duplicate chunks)
|
|
- Ordering (maintain correct sequence)
|
|
- Debugging (trace message flow)
|
|
- **Scope**: Unique within entire Agent stream
|
|
- **Always Present**: Yes
|
|
|
|
**Example:**
|
|
|
|
```json
|
|
{"chunk_id": "C1", "type": "text", "props": {"content": "Hello"}}
|
|
{"chunk_id": "C2", "type": "text", "props": {"content": " World"}}
|
|
{"chunk_id": "C3", "type": "thinking", "props": {"content": "..."}}
|
|
```
|
|
|
|
#### MessageID (Logical Message Identifier)
|
|
|
|
- **Purpose**: Groups multiple chunks into one logical message via delta merging
|
|
- **Generated**: By LLM Provider or Stream Handler
|
|
- **Used For**:
|
|
- Delta merge target (frontend merges all chunks with same MessageID)
|
|
- Distinguishing different messages within a group
|
|
- **Scope**: Unique within a Group
|
|
- **Present When**: Delta streaming is used
|
|
|
|
**Example:**
|
|
|
|
```json
|
|
// Multiple chunks combine into one "thinking" message
|
|
{"chunk_id": "C1", "message_id": "M1", "type": "thinking", "props": {"content": "Let me"}, "delta": true}
|
|
{"chunk_id": "C2", "message_id": "M1", "type": "thinking", "props": {"content": " think"}, "delta": true}
|
|
{"chunk_id": "C3", "message_id": "M1", "type": "thinking", "props": {"content": "..."}, "delta": true}
|
|
|
|
// Another independent message
|
|
{"chunk_id": "C4", "message_id": "M2", "type": "text", "props": {"content": "Hello"}, "delta": true}
|
|
```
|
|
|
|
#### BlockID (Output Block Identifier)
|
|
|
|
- **Purpose**: Represents one output block/section (e.g., one LLM call, one MCP call)
|
|
- **Generated**: By Agent logic
|
|
- **Used For**:
|
|
- Frontend UI block/section rendering (visual blocks)
|
|
- Distinguishing different operations (LLM vs MCP vs custom logic)
|
|
- Organizing related messages together
|
|
- **Scope**: Unique within entire Agent stream
|
|
- **Present When**: Agent explicitly controls output blocks
|
|
|
|
**Key Concept**: Block represents a semantic unit of work from Agent's perspective, NOT from LLM's perspective. Each block is rendered as a distinct UI section in the frontend.
|
|
|
|
**Example:**
|
|
|
|
```json
|
|
// BLOCK 1: LLM Response (contains thinking + tool_call + text)
|
|
{"chunk_id": "C1", "block_id": "B1", "message_id": "M1", "type": "thinking", ...}
|
|
{"chunk_id": "C2", "block_id": "B1", "message_id": "M2", "type": "tool_call", ...}
|
|
{"chunk_id": "C3", "block_id": "B1", "message_id": "M3", "type": "text", ...}
|
|
|
|
// BLOCK 2: MCP Call
|
|
{"chunk_id": "C4", "block_id": "B2", "message_id": "M4", "type": "loading", ...}
|
|
{"chunk_id": "C5", "block_id": "B2", "message_id": "M5", "type": "text", ...}
|
|
|
|
// BLOCK 3: Another LLM Response
|
|
{"chunk_id": "C6", "block_id": "B3", "message_id": "M6", "type": "text", ...}
|
|
```
|
|
|
|
#### ThreadID (Concurrent Stream Identifier)
|
|
|
|
- **Purpose**: Distinguishes concurrent/parallel output streams
|
|
- **Generated**: By Agent logic when spawning concurrent operations
|
|
- **Used For**:
|
|
- Separating outputs from parallel LLM/MCP calls
|
|
- Maintaining independent streaming contexts
|
|
- **Scope**: Unique within entire Agent stream
|
|
- **Present When**: Agent makes concurrent calls (optional)
|
|
|
|
**Example:**
|
|
|
|
```json
|
|
// Main thread
|
|
{"chunk_id": "C1", "thread_id": "T1", "block_id": "B1", "message_id": "M1", "type": "text", ...}
|
|
|
|
// Parallel MCP calls
|
|
{"chunk_id": "C2", "thread_id": "T2", "block_id": "B2", "message_id": "M2", "type": "text", ...}
|
|
{"chunk_id": "C3", "thread_id": "T3", "block_id": "B3", "message_id": "M3", "type": "text", ...}
|
|
```
|
|
|
|
## Usage Scenarios
|
|
|
|
### Scenario 1: Simple Text Message
|
|
|
|
**No streaming, no grouping**
|
|
|
|
```json
|
|
{
|
|
"chunk_id": "C1",
|
|
"type": "text",
|
|
"props": { "content": "Hello World" }
|
|
}
|
|
```
|
|
|
|
**Fields Used:**
|
|
|
|
- `chunk_id`: C1 (auto-generated)
|
|
- No `message_id`, `block_id`, or `thread_id` needed
|
|
|
|
---
|
|
|
|
### Scenario 2: LLM Streaming Response (Single Message)
|
|
|
|
**LLM streams one text message**
|
|
|
|
```json
|
|
{"chunk_id": "C1", "message_id": "M1", "type": "text", "props": {"content": "Hello"}, "delta": true}
|
|
{"chunk_id": "C2", "message_id": "M1", "type": "text", "props": {"content": " World"}, "delta": true}
|
|
{"chunk_id": "C3", "message_id": "M1", "type": "text", "props": {"content": "!"}, "delta": true}
|
|
```
|
|
|
|
**Fields Used:**
|
|
|
|
- `chunk_id`: C1, C2, C3 (unique per chunk)
|
|
- `message_id`: M1 (same for all, merge target)
|
|
- `delta`: true
|
|
|
|
**Frontend Behavior:**
|
|
|
|
- Merge all chunks with `message_id: "M1"` into one message
|
|
- Display: "Hello World!"
|
|
|
|
---
|
|
|
|
### Scenario 3: Agent-Controlled LLM Call (One Block)
|
|
|
|
**Agent wraps LLM response in an output block**
|
|
|
|
```typescript
|
|
// Agent code starts a block for the LLM response
|
|
// System generates block_id: "B1"
|
|
// LLM returns thinking + tool_call + text
|
|
// Agent ends the block
|
|
```
|
|
|
|
```json
|
|
// LLM chunks within block B1
|
|
{"chunk_id": "C1", "message_id": "M1", "block_id": "B1", "type": "thinking", "props": {...}, "delta": true}
|
|
{"chunk_id": "C2", "message_id": "M1", "block_id": "B1", "type": "thinking", "props": {...}, "delta": true}
|
|
|
|
{"chunk_id": "C3", "message_id": "M2", "block_id": "B1", "type": "tool_call", "props": {...}}
|
|
|
|
{"chunk_id": "C4", "message_id": "M3", "block_id": "B1", "type": "text", "props": {...}, "delta": true}
|
|
{"chunk_id": "C5", "message_id": "M3", "block_id": "B1", "type": "text", "props": {...}, "delta": true}
|
|
```
|
|
|
|
**Fields Used:**
|
|
|
|
- `chunk_id`: C1~C5 (unique per chunk)
|
|
- `message_id`: M1, M2, M3 (per logical message)
|
|
- `block_id`: B1 (all belong to same LLM call)
|
|
- `delta`: true (for streaming messages)
|
|
|
|
**Frontend Behavior:**
|
|
|
|
- Render one block/section for `block_id: "B1"`
|
|
- Within this block, show 3 messages:
|
|
- Thinking message (chunks C1+C2 merged into M1)
|
|
- Tool call message (chunk C3 = M2)
|
|
- Text message (chunks C4+C5 merged into M3)
|
|
|
|
---
|
|
|
|
### Scenario 4: Agent Sequential Operations (Multiple Blocks)
|
|
|
|
**Agent orchestrates: LLM → MCP → LLM**
|
|
|
|
```typescript
|
|
// Agent code orchestrates three sequential operations:
|
|
// 1. Block B1: First LLM call
|
|
// 2. Block B2: MCP call
|
|
// 3. Block B3: Second LLM call
|
|
```
|
|
|
|
```json
|
|
// BLOCK 1: First LLM call
|
|
{"chunk_id": "C1", "message_id": "M1", "block_id": "B1", "type": "text", ...}
|
|
{"chunk_id": "C2", "message_id": "M1", "block_id": "B1", "type": "text", ...}
|
|
|
|
// BLOCK 2: MCP call
|
|
{"chunk_id": "C3", "message_id": "M2", "block_id": "B2", "type": "loading", ...}
|
|
{"chunk_id": "C4", "message_id": "M3", "block_id": "B2", "type": "text", ...}
|
|
|
|
// BLOCK 3: Second LLM call
|
|
{"chunk_id": "C5", "message_id": "M4", "block_id": "B3", "type": "text", ...}
|
|
{"chunk_id": "C6", "message_id": "M4", "block_id": "B3", "type": "text", ...}
|
|
```
|
|
|
|
**Frontend Behavior:**
|
|
|
|
- Render 3 distinct blocks/sections:
|
|
1. Block 1 (B1): LLM response with text
|
|
2. Block 2 (B2): MCP call with loading + result
|
|
3. Block 3 (B3): LLM response with text
|
|
|
|
---
|
|
|
|
### Scenario 5: Concurrent Operations (Blocks + Threads)
|
|
|
|
**Agent uses concurrent handler to make parallel calls**
|
|
|
|
```typescript
|
|
// Agent orchestrates parallel operations within one block (B1)
|
|
// The concurrent handler automatically assigns thread_id to each operation:
|
|
// - MCP call for weather (thread_id: "T1")
|
|
// - MCP call for news (thread_id: "T2")
|
|
// - LLM call for summary (thread_id: "T3")
|
|
//
|
|
// Messages from different threads may arrive in any order
|
|
```
|
|
|
|
```json
|
|
// Same block, different threads (may arrive in any order)
|
|
{"chunk_id": "C1", "message_id": "M1", "block_id": "B1", "thread_id": "T1", "type": "text", "props": {"content": "Weather: Sunny"}}
|
|
{"chunk_id": "C2", "message_id": "M2", "block_id": "B1", "thread_id": "T2", "type": "text", "props": {"content": "News: ..."}}
|
|
{"chunk_id": "C3", "message_id": "M1", "block_id": "B1", "thread_id": "T1", "type": "text", "props": {"content": ", 25°C"}}
|
|
{"chunk_id": "C4", "message_id": "M3", "block_id": "B1", "thread_id": "T3", "type": "text", "props": {"content": "Summary..."}}
|
|
```
|
|
|
|
**Fields Used:**
|
|
|
|
- `chunk_id`: C1, C2, C3, C4 (unique per chunk, chronological order)
|
|
- `message_id`: M1, M2, M3 (per operation/message)
|
|
- `block_id`: B1 (all belong to same parallel operation block)
|
|
- `thread_id`: T1, T2, T3 (distinguish concurrent operations)
|
|
|
|
**Frontend Behavior:**
|
|
|
|
- Render one block for `block_id: "B1"`
|
|
- Within this block, separate messages by `thread_id`:
|
|
- Thread T1 (Weather): M1 (chunks C1+C3 merged) → "Weather: Sunny, 25°C"
|
|
- Thread T2 (News): M2 (chunk C2)
|
|
- Thread T3 (Summary): M3 (chunk C4)
|
|
- Or interleave by `chunk_id` order (C1, C2, C3, C4) to show real-time arrival
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
| Field | Level | Purpose | Example |
|
|
| ----------- | ----------- | ------------------ | ---------- |
|
|
| `ChunkID` | System | Transport/debug | C1, C2, C3 |
|
|
| `MessageID` | LLM/Handler | Delta merging | M1, M2, M3 |
|
|
| `BlockID` | Agent | UI blocks/sections | B1, B2, B3 |
|
|
| `ThreadID` | Agent | Concurrency | T1, T2, T3 |
|
|
|
|
**Key Insight**: Each field serves a distinct purpose at a specific layer of the architecture. This hierarchical design supports simple single-message scenarios while enabling complex Agent orchestration with concurrent operations. Blocks provide natural UI boundaries for organizing related messages.
|