yao/agent/output/message/STREAMING.md
Max ef49827faf Refactor message handling for improved streaming and event management
- 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.
2025-11-26 18:30:43 +08:00

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.