Deep Dive: Chat Shell Direct Streaming Architecture
Executive Summary
The Chat Shell's direct streaming architecture is a hybrid HTTP-SSE + WebSocket bridge design that achieves three key goals:
- Direct LLM streaming without Docker container overhead
- Multi-LLM support (Anthropic, OpenAI, Google) via LangGraph
- Seamless frontend integration via WebSocket while maintaining HTTP benefits
Unlike traditional WebSocket-only approaches, this architecture leverages HTTP SSE for the core streaming transport and uses a Bridge Pattern to integrate with WebSocket clients.
1. Architecture Overview
1.1 Three Deployment Modes
┌─────────────────────────────────────────────────────────────────────────────┐
│ CHAT SHELL DEPLOYMENT MODES │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ HTTP Mode │ │ Package Mode │ │ CLI Mode │ │
│ │ (Port │ │ (Imported │ │ (Interactive │ │
│ │ 8001) │ │ Module) │ │ Terminal) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ FastAPI + LangGraph Core │ │
│ │ ┌─────────┐ ┌──────────┐ ┌────────────┐ ┌─────────────┐ │ │
│ │ │ ChatAgent│ │Streaming │ │ Tool │ │ Message │ │ │
│ │ │(LangGraph│ │ Core │ │ Registry │ │ Compression │ │ │
│ │ │ Agent) │ │ │ │ │ │ │ │ │
│ │ └─────────┘ └──────────┘ └────────────┘ └─────────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
1.2 HTTP Mode: The Primary Production Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ HTTP MODE: FULL PRODUCTION SETUP │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ FRONTEND BACKEND CHAT SHELL │
│ (Next.js) (FastAPI) (FastAPI) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Socket │◄──────────────│ Socket.IO│ │ │ │
│ │ Client │ WebSocket │ Server │ │ │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ │ ┌─────────────────────┼─────────────────────┐ │ │
│ │ │ ▼ │ │ │
│ │ │ ┌──────────┐ │ │ │
│ │ │ │ WS Bridge│ │ │ │
│ │ │ │ (Redis │ │ │ │
│ │ │ │ Pub/Sub)│ │ │ │
│ │ │ └──────────┘ │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ ┌──────────┐ │ │ │
│ │ │ │ Redis │ │ │ │
│ │ │ │ Pub/Sub │ │ │ │
│ │ └──────────────┴──────────┴─────────────────┘ │ │
│ │ │ │ │
│ │ ┌─────────────────────┼─────────────────────┐ │ │
│ │ │ ▼ │ │ │
│ │ │ ┌──────────┐ │ │ │
│ │ │ │ Backend │ │ │ │
│ │ │ │ SSE │ │ │ │
│ │ │ │ Handler │ │ │ │
│ │ │ └──────────┘ │ │ │
│ │ │ │ │ │ │
│ │ │ ┌────────────────┴────────────────┐ │ │ │
│ │ │ │ │ │ │ │
│ │ │ ▼ ▼ │ │ │
│ │ │ ┌──────┐ ┌──────┐│ │ │
│ │ │ │Direct│ │ HTTP ││ │ │
│ │ │ │Call │ │Adapter│ │ │
│ │ │ │(In- │ │(Out- ││ │ │
│ │ │ │Proc) │ │ Proc)││ │ │
│ │ │ └──┬───┘ └──┬───┘│ │ │
│ │ │ │ │ │ │ │
│ │ └────┴──────────────────────────────┴────┘ │ │
│ │ │ │ │
│ │ ┌────┴────┐ │ │
│ │ ▼ ▼ │ │
│ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │Streaming │ │Streaming │ │ │
│ │ │ Core │ │ Core │ │ │
│ │ │(Package) │ │ (HTTP) │ │ │
│ │ └──────────┘ └──────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────┴──────────────┤
│ │
└─────────────────────────────────────────────────────────────────────────────┘
2. Streaming Mechanism: HTTP SSE
2.1 Why HTTP SSE over WebSocket?
┌─────────────────────────────────────────────────────────────────────────────┐
│ HTTP SSE vs WebSocket: CHAT SHELL DESIGN RATIONALE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ HTTP SSE ADVANTAGES │ │
│ ├──────────────────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ ✓ SIMPLE CONNECTION MGT ✓ PROXY-FRIENDLY ✓ AUTO RECONNECT │ │
│ │ │ │
│ │ ✓ HTTP INFRASTRUCTURE ✓ FIREWALL-FRIENDLY ✓ LAST-EVENT-ID │ │
│ │ │ │
│ │ ✓ ONE-WAY STREAMING ✓ LOAD BALANCER READY ✓ SIMPLE CANCEL │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ WEBSOCKET ADVANTAGES │ │
│ ├──────────────────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ ✓ BIDIRECTIONAL COMMS ✓ LOWER LATENCY ✓ BINARY DATA │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ ╔══════════════════════════════════════════════════════════════════════╗ │
│ ║ CHAT SHELL CHOICE: HTTP SSE FOR LLM STREAMING ║ │
│ ║ ║ │
│ ║ LLM streaming is inherently ONE-WAY (server → client). ║ │
│ ║ HTTP SSE provides this perfectly without WebSocket overhead. ║ │
│ ║ ║ │
│ ║ WebSocket is used at the FRONTEND layer for: ║ │
│ ║ - Bidirectional user interactions ║ │
│ ║ - Multi-client synchronization ║ │
│ ║ - Cross-worker event broadcasting ║ │
│ ╚══════════════════════════════════════════════════════════════════════╝ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
2.2 SSE Event Types in Chat Shell
# From chat_shell/api/v1/schemas.py
class ResponseEventType(Enum):
RESPONSE_START = "response.start" # Stream begins
CONTENT_DELTA = "content.delta" # Text chunk
REASONING_DELTA = "reasoning.delta" # DeepSeek R1 reasoning
TOOL_START = "tool.start" # Tool execution starts
TOOL_DONE = "tool.done" # Tool execution completes
RESPONSE_DONE = "response.done" # Stream completes
RESPONSE_CANCELLED = "response.cancelled" # User cancelled
ERROR = "response.error" # Error occurred
# SSE Format:
# event: content.delta
# data: {"type": "text", "text": "chunk", "result": {...}}
#
2.3 Streaming Flow: Token-Level Detail
┌─────────────────────────────────────────────────────────────────────────────┐
│ TOKEN-LEVEL STREAMING FLOW (HTTP SSE) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ │
│ │ LLM Provider │ │
│ │ (OpenAI/Claude │ │
│ │ /Google/etc) │ │
│ └────────┬─────────┘ │
│ │ Stream Tokens │
│ ▼ │
│ ┌──────────────────┐ │
│ │ LangGraph │ ← create_react_agent() │
│ │ Agent Builder │ ← astream_events() │
│ └────────┬─────────┘ │
│ │ on_chat_model_stream │
│ ▼ │
│ ┌──────────────────┐ │
│ │ StreamingCore │ ← process_token() │
│ │ (core.py) │ ← TTFT tracking │
│ └────────┬─────────┘ │
│ │ │
│ ├──► SSEEmitter.emit_chunk() ──► SSE format │
│ │ "data: {type: 'chunk', content: '...'}" │
│ │ │
│ └──► RedisPublishingEmitter.emit_chunk() ──► Redis Pub/Sub │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ EMISSION DUAL PATHS │ │
│ │ │ │
│ │ PATH 1: Direct SSE (Standalone Mode) │ │
│ │ ───────────────────────────────────── │ │
│ │ StreamingResponse ──► HTTP Client (Frontend/Browser) │ │
│ │ │ │
│ │ PATH 2: Bridge Mode (Backend Integration) │ │
│ │ ───────────────────────────────────── │ │
│ │ RedisPublishingEmitter ──► Redis ──► WS Bridge ──► WebSocket │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
3. The Bridge Pattern: WebSocket Integration
3.1 Bridge Architecture Diagram
┌─────────────────────────────────────────────────────────────────────────────┐
│ BRIDGE PATTERN: REDIS PUB/SUB BRIDGE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ CHAT SHELL (HTTP Service) │ │
│ │ ┌────────────┐ ┌───────────────┐ ┌──────────────────────┐ │ │
│ │ │Streaming │───►│ RedisPublishing│───►│ Redis Pub/Sub │ │ │
│ │ │ Core │ │ Emitter │ │ Channel: stream:{id}│ │ │
│ │ └────────────┘ └───────────────┘ └──────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ Publish Events │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ REDIS LAYER │ │
│ │ │ │
│ │ ┌────────────────────┐ ┌────────────────────┐ │ │
│ │ │ Subtask Content │ │ Streaming Events │ │ │
│ │ │ (string - accum) │ │ (pub/sub - real) │ │ │
│ │ └────────────────────┘ └────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ Subscribe │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ BACKEND (WebSocket Bridge) │ │
│ │ │ │
│ │ ┌─────────────────────┐ ┌─────────────────────────┐ │ │
│ │ │ WebSocketBridge │───────►│ Socket.IO Emit │ │ │
│ │ │ (ws_bridge.py) │ │ chat:start/chunk/done │ │ │
│ │ └─────────────────────┘ └─────────────────────────┘ │ │
│ │ ▲ │ │
│ │ │ Subscribe │ │
│ │ ┌────────┴────────┐ │ │
│ │ │ Redis Pub/Sub │ │ │
│ │ │ Listener │ │ │
│ │ └─────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ WebSocket │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ FRONTEND (Browser) │ │
│ │ │ │
│ │ ┌─────────────────────┐ ┌─────────────────────────┐ │ │
│ │ │ Socket.IO Client │───────►│ useUnifiedMessages │ │ │
│ │ │ (SocketContext) │ │ (State Machine) │ │ │
│ │ └─────────────────────┘ └─────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
3.2 Emitter Abstraction Hierarchy
# chat_shell/services/streaming/emitters.py
┌─────────────────────────────────────────────────────────┐
│ StreamEmitter (ABC) │
│ Abstract Base Class │
├─────────────────────────────────────────────────────────┤
│ + emit_start(task_id, subtask_id) │
│ + emit_chunk(content, offset, subtask_id) │
│ + emit_done(task_id, subtask_id, result) │
│ + emit_error(subtask_id, error) │
│ + emit_cancelled(subtask_id) │
└──────────────┬──────────────────────────────────────────┘
│
┌────────┴────────┐
│ │
▼ ▼
┌─────────────┐ ┌──────────────────────┐
│ SSEEmitter │ │ RedisPublishingEmitter│
│ (Direct SSE)│ │ (Bridge Mode) │
├─────────────┤ ├──────────────────────┤
│ _events: [] │ │ _storage: session_mgr │
├─────────────┤ ├──────────────────────┤
│ format_sse()│ │ publish_streaming_ │
│ get_event() │ │ chunk() │
└─────────────┘ └──────────────────────┘
3.3 Bridge Data Flow Sequence
┌─────────────────────────────────────────────────────────────────────────────┐
│ BRIDGE MODE: COMPLETE SEQUENCE DIAGRAM │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ FRONTEND BACKEND_WS BACKEND_HTTP REDIS CHAT_SHELL LLM │
│ │ │ │ │ │ │ │
│ │ ┌─────────┴──────────┐ │ │ │ │ │
│ │ │ User Sends │ │ │ │ │ │
│ │ │ Message │ │ │ │ │ │
│ ├──►├────────────────────►│ │ │ │ │ │
│ │ │ socket:send │ │ │ │ │ │
│ │ └─────────────────────┘ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ ┌────────────┴────────┐ │ │ │ │
│ │ │ │ Create Subtask │ │ │ │ │
│ │ │ │ Start WS Bridge │ │ │ │ │
│ │ │ │ Subscribe to Redis │ │ │ │ │
│ │ │ └──────────┬──────────┘ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ ┌──────────┴──────────┐ │ │ │ │
│ │ │ │ HTTP POST /v1/resp │ │ │ │ │
│ │ │ ├─────────────────────►│ │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ ┌─────────┴─────────┐ │ │ │ │ │
│ │ │ Emit chat:start │ │ │ │ │ │
│ │◄─├───────────────────┤ │ │ │ │ │
│ │ │ (AI message init) │ │ │ │ │ │
│ │ └───────────────────┘ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │┌─────────┴────────┐│
│ │ │ │ │ ││ Start LLM Call ││
│ │ │ │ │ │└────────┬─────────┘│
│ │ │ │ │ │ │ │
│ │ │ │ │ │ ▼ │
│ │ │ │ │ │ ┌──────────┐ │
│ │ │ │ │ │ │ Stream │ │
│ │ │ │ │ │ │ Tokens │ │
│ │ │ │ │ │ └────┬─────┘ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ Emit chunk │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ ┌─────▼────────┴────┐ │
│ │ │ │ │ │ Redis Pub/Sub │ │
│ │ │ │ │ │ stream:{id} │ │
│ │ │ │ │ └─────┬────────────┘ │
│ │ │ │ │ │ │
│ │ │ Receive from Redis ◄──├────────────┘ │
│ │ │ │ │
│ │ │ ┌────────────────────┐ │ │
│ │ │ │ Forward to WS Room │ │ │
│ │ │ └─────────┬──────────┘ │ │
│ │ │ │ │ │
│ │ ┌─────────┴──────────┐│ │ │
│ │ │ socket:chat:chunk ││ │ │
│ │◄─├────────────────────┤│ │ │
│ │ │ {chunk: "..."} ││ │ │
│ │ └────────────────────┘│ │ │
│ │ │ │ │ │
│ │ │ │ [Repeat for each chunk...] │
│ │ │ │ │ │
│ │ │ │ │ │ │
│ │ │ │ │ │ LLM Complete │
│ │ │ │ │ │ │ │
│ │ │ │ │ ┌─────▼─────────┴────┐ │
│ │ │ │ │ │ Redis Pub/Sub │ │
│ │ │ │ │ │ {type: "done"} │ │
│ │ │ │ │ └─────┬──────────────┘ │
│ │ │ Receive ◄──├────────────┘ │ │
│ │ │ │ │ │
│ │ ┌─────────┴──────────┐ │ │ │
│ │ │ socket:chat:done │ │ │ │
│ │◄─├────────────────────┤ │ │ │
│ │ │ {result: {...}} │ │ │ │
│ │ └────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ Stop Bridge│ │ │
│ │ │ │ │ │
│ │ │ │ │ │
└─────┴────────────┴────────────┴───────────────────────┴──────────────────┴──┘
4. Backend Integration: Two Streaming Modes
4.1 Mode 1: Legacy Direct WebSocket (Deprecated)
┌─────────────────────────────────────────────────────────────────────────────┐
│ LEGACY MODE: DIRECT WS EMIT (Deprecated) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Chat Shell (Package Mode) Backend WebSocketStreamingHandler │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ StreamingCore │ │ WebSocketStreaming │ │
│ │ ┌──────────────────┐ │ │ Handler │ │
│ │ │WebSocketEmitter │─├─────────►│ ┌──────────────────┐ │ │
│ │ │emit_chunk() │ │Direct WS │ │ socket.emit() │ │ │
│ │ └──────────────────┘ │Connection│ │ to Frontend │ │ │
│ └──────────────────────┘ │ └──────────────────┘ │ │
│ └──────────────────────┘ │
│ │
│ PROBLEM: │
│ - Tight coupling between Chat Shell and Backend │
│ - Chat Shell must be imported as Python package │
│ - Cannot run as independent service │
│ - Hard to scale/maintain │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
4.2 Mode 2: Bridge Mode (Current Production)
┌─────────────────────────────────────────────────────────────────────────────┐
│ BRIDGE MODE: HTTP + REDIS + WS (Production) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Chat Shell (HTTP Service) Backend │
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────────┐ │
│ │ StreamingCore │ │ BackendStreamHandler │ │
│ │ ┌─────────────────────┐ │ HTTP │ (sse_handler.py) │ │
│ │ │RedisPublishing │─├───────────►│ │ │
│ │ │Emitter │ │ SSE │ ┌─────────────────────┐ │ │
│ │ │publish_streaming_ │ │ │ │ Parse SSE Events │ │ │
│ │ │chunk() │ │ │ │ Accumulate Response │ │ │
│ │ └─────────────────────┘ │ │ │ Forward to WS │ │ │
│ └─────────────────────────┘ │ └─────────────────────┘ │ │
│ │ │ │ │ │
│ │ │ ▼ │ │
│ │ │ ┌─────────────────────┐ │ │
│ │ │ │ Redis Save (cache) │ │ │
│ │ │ │ DB Save (persist) │ │ │
│ │ │ └─────────────────────┘ │ │
│ │ └──────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ Redis Pub/Sub Channel │◄─────────────────────────────────────────┐ │
│ │ stream:{subtask_id} │ │ │
│ └─────────────────────────┘ │ │
│ ▲ │ │
│ │ │ │
│ ┌────────┴─────────────┐ │ │
│ │ WebSocketBridge │◄────────────────────────────────────────────┘ │
│ │ (ws_bridge.py) │ Subscribes to Redis │
│ │ │ Forwards to WebSocket │
│ └───────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ Socket.IO Emit │ │
│ │ chat:chunk, chat:done │ │
│ └─────────────────────────┘ │
│ │
│ BENEFITS: │
│ ✓ Chat Shell runs as independent service (port 8001) │
│ ✓ Can scale independently │
│ ✓ Language-agnostic (Backend could be different language) │
│ ✓ Resilient (Redis buffers if Backend temporarily down) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5. Frontend Consumption
5.1 Frontend Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ FRONTEND STREAMING CONSUMPTION │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ SOCKET.IO LAYER │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ SocketContext│ │ChatStream │ │ ws_emitter │ │ │
│ │ │ (Connection) │ │ Context │ │ (Emitter) │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └─────────────┘ │ │
│ │ │ │ │ │
│ │ └────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
│ │ │ EVENT HANDLERS │ │ │
│ │ │ chat:start ──► Add AI message (streaming status) │ │ │
│ │ │ chat:chunk ──► Append content to streaming message │ │ │
│ │ │ chat:done ──► Mark complete, save result │ │ │
│ │ │ chat:cancelled ──► Mark cancelled │ │ │
│ │ │ chat:error ──► Show error │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
│ │ │ STATE MANAGEMENT │ │ │
│ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ TaskStateMachine (SINGLE SOURCE OF TRUTH) │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ ┌─────────────┐ ┌────────────────────────────┐ │ │ │ │
│ │ │ │ │ Messages │◄───────│ useUnifiedMessages Hook │ │ │ │ │
│ │ │ │ │ (Map) │ │ (React State) │ │ │ │ │
│ │ │ │ └─────────────┘ └────────────────────────────┘ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ ┌─────────────┐ ┌────────────────────────────┐ │ │ │ │
│ │ │ │ │ Status │◄───────│ useTaskStateMachine Hook │ │ │ │ │
│ │ │ │ │(idle/stream)│ │ (Subscription) │ │ │ │ │
│ │ │ │ └─────────────┘ └────────────────────────────┘ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ ┌─────────────┐ │ │ │ │
│ │ │ │ │ Recovery │◄───────│ Automatic on reconnect │ │ │ │ │
│ │ │ │ │ Logic │ │ Offset-based resume │ │ │ │ │
│ │ │ │ └─────────────┘ │ │ │ │
│ │ │ └─────────────────────────────────────────────────────────┘ │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
│ │ │ UI COMPONENTS │ │ │
│ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │ │
│ │ │ │ChatMessage │ │ Streaming │ │ ThinkingDisplay │ │ │ │
│ │ │ │ List │ │ Bubble │ │ (Tool Calls) │ │ │ │
│ │ │ └─────────────┘ └─────────────┘ └─────────────────────┘ │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5.2 Message State Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ MESSAGE STATE FLOW (Frontend) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. USER SENDS MESSAGE │ │
│ │ │ │
│ │ User Input ──► useUnifiedMessages ──► TaskStateMachine │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ Add pending message Set status: 'pending' │ │
│ │ (optimistic UI) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 2. BACKEND CONFIRMS │ │
│ │ │ │
│ │ HTTP Response ◄── Backend creates subtask, returns subtaskId │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Update message with: │ │
│ │ - subtaskId (from backend) │ │
│ │ - status: 'pending' → 'sent' │ │
│ │ - messageId (for ordering) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 3. AI RESPONSE STARTS (WebSocket) │ │
│ │ │ │
│ │ WebSocket: chat:start │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Add AI message: │ │
│ │ - id: auto-generated │ │
│ │ - type: 'ai' │ │
│ │ - status: 'streaming' │ │
│ │ - content: '' (empty, accumulating) │ │
│ │ - subtaskId: from event │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 4. CHUNKS ARRIVE (WebSocket) │ │
│ │ │ │
│ │ WebSocket: chat:chunk ◄── Redis Bridge ◄── Chat Shell │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Update AI message: │ │
│ │ - content += chunk │ │
│ │ - status: 'streaming' │ │
│ │ - re-render with new content │ │
│ │ │ │
│ │ [REPEATS FOR EACH CHUNK] │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 5. STREAM COMPLETES │ │
│ │ │ │
│ │ WebSocket: chat:done │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Update AI message: │ │
│ │ - status: 'streaming' → 'completed' │ │
│ │ - result: {value, thinking, sources, ...} │ │
│ │ - Save to localStorage (for recovery) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
6. Recovery Mechanism
6.1 Offset-Based Stream Recovery
┌─────────────────────────────────────────────────────────────────────────────┐
│ STREAM RECOVERY ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ PROBLEM: User disconnects during streaming, reconnects later │
│ SOLUTION: Offset-based recovery with Redis caching │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ RECOVERY COMPONENTS │ │
│ │ │ │
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
│ │ │ Frontend │ │ Redis │ │ Backend │ │ │
│ │ │ State Machine │ │ Cache │ │ Storage │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ │
│ │ │ │offset │ │ │ │content │ │ │ │subtask │ │ │ │
│ │ │ │tracking │ │ │ │cache │ │ │ │status │ │ │ │
│ │ │ │(per msg) │ │ │ │(TTL: 5min)│ │ │ │+ result │ │ │ │
│ │ │ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ │
│ │ │ │local │ │ │ │streaming │ │ │ │DB │ │ │ │
│ │ │ │storage │ │ │ │:{id} │ │ │ │persistent │ │ │ │
│ │ │ │(fallback) │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ │ │
│ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ RECOVERY FLOW │ │
│ │ │ │
│ │ [DISCONNECT] │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ User loses connection (network/WiFi/sleep) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ [STILL STREAMING] │ │
│ │ │ │ │
│ │ ├──► Chat Shell continues streaming to Redis │ │
│ │ ├──► Backend accumulates in Redis (TTL) │ │
│ │ └──► Frontend stores last offset │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ [RECONNECT] │ │
│ │ │ │ │
│ │ ├──► Frontend detects reconnection │ │
│ │ ├──► Calls TaskStateMachine.recover() │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ [RECOVERY LOGIC] │ │
│ │ │ │ │
│ │ ├──► Check Redis cache for streaming content │ │
│ │ │ ├─► If found: Restore from offset │ │
│ │ │ └─► If not found: Fetch from DB (subtask.result) │ │
│ │ │ │ │
│ │ ├──► Resume WebSocket connection │ │
│ │ │ │ │
│ │ └──► Subscribe to new events │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ [DISPLAY RECOVERED CONTENT] │ │
│ │ │ │ │
│ │ ├──► Show recovered content as "completed" or "incomplete" │ │
│ │ │ │ │
│ │ └──► Continue receiving new chunks │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
6.2 Recovery Sequence Diagram
┌─────────────────────────────────────────────────────────────────────────────┐
│ RECOVERY SEQUENCE: DISCONNECT + RECONNECT │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Time ──────────────────────────────────────────────────────────────────► │
│ │
│ FRONTEND WS_CONN REDIS BACKEND CHAT_SHELL LLM │
│ │ │ │ │ │ │ │
│ │ ┌─────────┴────────┐ │ │ │ │ │
│ │ │ ACTIVE STREAMING │ │ │ │ │ │
│ │ └─────────┬────────┘ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ ┌────────┴────────┐ │ │ │
│ │ │ │ │ Periodic Save │ │ │ │
│ │ │ │ │ to Redis │ │ │ │
│ │ │ │ │ (every 1s) │ │ │ │
│ │ │ │ └────────┬────────┘ │ │ │
│ │ │ │ │ │ │ │
│ │ ══════════╪════════════╪═══════════╪════════════╪════════════╪══ │
│ │ [DISCONNECT] │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ Connection Lost │ │ │ │ │
│ │ (WiFi/Network/Sleep) │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ ╳ │ │ │ │ │
│ │ (WebSocket │ │ │ │ │
│ │ disconnected) │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ ══════════╪════════════╪═══════════╪════════════╪════════════╪══ │
│ │ [BACKEND CONTINUES] │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ ◄────────┼────────────┼────────────┤ │
│ │ │ │ │ LLM continues, │ │ │
│ │ │ │ │ saves to Redis │ │ │
│ │ │ │ └────────┼────────────┼────────────┘ │
│ │ │ │ │ │ │ │
│ │ ══════════╪════════════╪═══════════╪════════════╪════════════╪══ │
│ │ [RECONNECT] │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ Socket.IO │ │ │ │ │ │
│ │ reconnects│ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ joinTask │ │ │ │ │ │
│ │ (with │ │ │ │ │ │
│ │ subtasks)│ │ │ │ │ │
│ ├───────────►│ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ ┌────────┴────────┐ │ │ │
│ │ │ │ │ Check Redis for │ │ │ │
│ │ │ │ │ streaming content│ │ │ │
│ │ │ │ └────────┬────────┘ │ │ │
│ │ │ │ │ │ │ │
│ │ Recovery │ │ │ │ │ │
│ │ Response │ │ │ │ │ │
│ │◄───────────┼────────────┼───────────┤ │ │ │
│ │ (with │ │ (cached │ │ │ │
│ │ partial │ │ content)│ │ │ │
│ │ content) │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ ┌─────────┴────────┐ │ │ │ │ │
│ │ │ Display Recovered│ │ │ │ │ │
│ │ │ Content in UI │ │ │ │ │ │
│ │ └─────────┬────────┘ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ ┌─────────┴────────┐ │ │
│ │ │ │ │ │ Subscribe to WS │ │ │
│ │ │ │ │ │ for new chunks │ │ │
│ │ │ │ │ └─────────┬────────┘ │ │
│ │ │ │ │ │ │ │
│ │ ◄─────────┼────────────┼───────────┼────────────┤ │ │
│ │ ┌─────────┴────────┐ │ │ │ │ │
│ │ │ Continue Receiving│ │ │ │ │ │
│ │ │ New Chunks via WS │ │ │ │ │ │
│ │ └───────────────────┘ │ │ │ │ │
│ │ │ │ │ │ │ │
└─────┴────────────┴────────────┴───────────┴────────────┴────────────┴───────┘
7. LangGraph Workflow Design
7.1 Agent Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ LANGGRAPH AGENT WORKFLOW │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ AGENT BUILDER (graph_builder.py) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │
│ │ │ LLM │───►│create_react │───►│ ReAct Agent │ │ │
│ │ │ (Streaming) │ │ _agent() │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ │ ┌───────────────┐ │ │ │
│ │ │ │ LLM Node │ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │ (llm.bind_ │ │ │ │
│ │ │ ToolRegistry│───►│ Dynamic │───►│ │ tools) │ │ │ │
│ │ │ │ │ Tool Selection │ └───────┬───────┘ │ │ │
│ │ └─────────────┘ └─────────────┘ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ ┌───────────────┐ │ │ │
│ │ │PromptModifier│───►│ prompt() │───►│ │ Tool Node │ │ │ │
│ │ │ Tools │ │ (injects │ │ │ (execute) │ │ │ │
│ │ └─────────────┘ │ skills) │ │ └───────┬───────┘ │ │ │
│ │ └─────────────┘ │ │ │ │ │
│ │ │ │ Loop │ │ │
│ │ │ ▼ │ │ │
│ │ │ ┌───────────────┐ │ │ │
│ │ │ │ Router Node │ │ │ │
│ │ │ │ (continue/ │ │ │ │
│ │ │ │ end) │ │ │ │
│ │ │ └───────────────┘ │ │ │
│ │ └───────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ EXECUTION FLOW │ │
│ │ │ │
│ │ 1. START │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 2. LLM Node │ │
│ │ ├─► Inject system prompt with skill modifications │ │
│ │ ├─► Model generates response │ │
│ │ └─► Stream tokens via astream_events() │ │
│ │ ├─► on_chat_model_start: Track TTFT start │ │
│ │ ├─► on_chat_model_stream: Yield tokens │ │
│ │ └─► on_chat_model_end: Track completion │ │
│ │ │ │ │
│ │ ├──► If no tool calls ──► END │ │
│ │ │ │ │
│ │ └──► If tool calls ──► TOOL Node │ │
│ │ ├─► on_tool_start: Emit tool event │ │
│ │ ├─► Execute tool │ │
│ │ └─► on_tool_end: Emit tool result │ │
│ │ │ │ │
│ │ ◄──┘ (Loop back to LLM Node) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 3. END │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
7.2 Multi-LLM Support
┌─────────────────────────────────────────────────────────────────────────────┐
│ MULTI-LLM SUPPORT ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ MODEL FACTORY (models/factory.py) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌────────────┐ │ │
│ │ │ OpenAI │ │ Anthropic │ │ Google │ │ DeepSeek │ │ │
│ │ │ (GPT-4) │ │ (Claude 3) │ │ (Gemini) │ │ (R1) │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └─────┬──────┘ │ │
│ │ │ │ │ │ │ │
│ │ └────────────────┴────────────────┴───────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌───────────────────┐ │ │
│ │ │ LangChainModelFactory │ │
│ │ │ .create_from_config() │ │
│ │ └───────────┬───────┘ │ │
│ │ │ │ │
│ │ ┌───────────┴───────────┐ │ │
│ │ │ BaseChatModel │ │ │
│ │ │ (Streaming Enabled) │ │ │
│ │ └───────────┬───────────┘ │ │
│ │ │ │ │
│ └────────────────────────────────┼─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ UNIFIED STREAMING INTERFACE │ │
│ │ │ │
│ │ All models implement the same LangChain streaming interface: │ │
│ │ - astream_events() │ │
│ │ - on_chat_model_stream │ │
│ │ - Token-level streaming │ │
│ │ - Tool execution callbacks │ │
│ │ │ │
│ │ Special handling for reasoning models (DeepSeek R1): │ │
│ │ - reasoning_content extracted from additional_kwargs │ │
│ │ - Wrapped with __REASONING__ markers │ │
│ │ - Emitted as reasoning.delta events │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
8. Trade-offs: Direct HTTP vs WebSocket-Only
8.1 Comparison Matrix
┌─────────────────────────────────────────────────────────────────────────────┐
│ ARCHITECTURE COMPARISON │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┬─────────────────────┬────────────────────────┐ │
│ │ Aspect │ HTTP SSE + Bridge │ WebSocket Only │ │
│ ├──────────────────────┼─────────────────────┼────────────────────────┤ │
│ │ │ │ │ │
│ │ CONNECTION │ │ │ │
│ │ Complexity │ MEDIUM │ LOW │ │
│ │ (HTTP + WS Bridge) │ (2 connections) │ (1 connection) │ │
│ │ │ │ │ │
│ ├──────────────────────┼─────────────────────┼────────────────────────┤ │
│ │ │ │ │ │
│ │ SCALABILITY │ │ │ │
│ │ Horizontal Scale │ EXCELLENT │ GOOD │ │
│ │ │ (Independent │ (Socket.IO with │ │
│ │ │ Chat Shell svc) │ sticky sessions) │ │
│ │ │ │ │ │
│ ├──────────────────────┼─────────────────────┼────────────────────────┤ │
│ │ │ │ │ │
│ │ RESILIENCE │ │ │ │
│ │ Disconnection │ EXCELLENT │ GOOD │ │
│ │ Recovery │ (Redis buffers │ (Built-in reconn │ │
│ │ │ + offset tracking) │ but state lost) │ │
│ │ │ │ │ │
│ ├──────────────────────┼─────────────────────┼────────────────────────┤ │
│ │ │ │ │ │
│ │ INFRASTRUCTURE │ │ │ │
│ │ Requirements │ HIGHER │ LOWER │ │
│ │ │ (HTTP + Redis + WS) │ (WebSocket only) │ │
│ │ │ │ │ │
│ ├──────────────────────┼─────────────────────┼────────────────────────┤ │
│ │ │ │ │ │
│ │ LATENCY │ │ │ │
│ │ Overhead │ LOW-MEDIUM │ LOW │ │
│ │ │ (HTTP + Redis hop) │ (Direct) │ │
│ │ │ │ │ │
│ ├──────────────────────┼─────────────────────┼────────────────────────┤ │
│ │ │ │ │ │
│ │ MULTI-CLIENT │ │ │ │
│ │ Sync (Group Chat) │ EXCELLENT │ EXCELLENT │ │
│ │ │ (Both use WS) │ (Native) │ │
│ │ │ │ │ │
│ ├──────────────────────┼─────────────────────┼────────────────────────┤ │
│ │ │ │ │ │
│ │ BIDIRECTIONAL │ │ │ │
│ │ Comms (Cancel) │ MEDIUM │ EXCELLENT │ │
│ │ │ (HTTP cancel API │ (Native WebSocket │ │
│ │ │ + Redis signal) │ signaling) │ │
│ │ │ │ │ │
│ ├──────────────────────┼─────────────────────┼────────────────────────┤ │
│ │ │ │ │ │
│ │ MAINTAINABILITY │ │ │ │
│ │ Code Complexity │ MEDIUM │ LOW │ │
│ │ │ (Bridge code) │ (Simpler) │ │
│ │ │ │ │ │
│ ├──────────────────────┼─────────────────────┼────────────────────────┤ │
│ │ │ │ │ │
│ │ MULTI-LANGUAGE │ │ │ │
│ │ Support │ EXCELLENT │ HARD │ │
│ │ │ (HTTP + Redis │ (Shared Socket.IO │ │
│ │ │ are universal) │ libs required) │ │
│ │ │ │ │ │
│ └──────────────────────┴─────────────────────┴────────────────────────┘ │
│ │
│ KEY INSIGHT: │
│ Chat Shell's hybrid approach trades some complexity for significant │
│ gains in scalability, resilience, and operational flexibility. │
│ The WebSocket is reserved for what it does best: │
│ - Bidirectional user interactions │
│ - Multi-client synchronization │
│ - Frontend state management │
│ │
│ HTTP SSE handles what it's best at: │
│ - One-way LLM streaming │
│ - Simple connection management │
│ - Proxy/load balancer compatibility │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
8.2 When to Use Each Pattern
┌─────────────────────────────────────────────────────────────────────────────┐
│ ARCHITECTURE DECISION GUIDE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ CHOOSE WEBSOCKET-ONLY IF: │ │
│ │ │ │
│ │ • Simple application with single server │ │
│ │ • Low infrastructure complexity requirement │ │
│ │ • Primarily bidirectional real-time communication │ │
│ │ • No need for horizontal scaling │ │
│ │ • Single programming language stack │ │
│ │ │ │
│ │ Examples: Chat rooms, gaming, collaborative editing │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ CHOOSE HTTP SSE + BRIDGE IF: │ │
│ │ │ │
│ │ • One-way streaming from external service │ │
│ │ • Need to scale LLM service independently │ │
│ │ • High availability / recovery requirements │ │
│ │ • Multi-language microservices architecture │ │
│ │ • Complex proxy/load balancer infrastructure │ │
│ │ │ │
│ │ Examples: AI chatbots, streaming APIs, media streaming │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ CHAT SHELL ARCHITECTURE: │ │
│ │ │ │
│ │ Chat Shell's hybrid is OPTIMAL for: │ │
│ │ │ │
│ │ ✓ AI/LLM streaming (one-way by nature) │ │
│ │ ✓ Service independence (Backend vs Chat Shell) │ │
│ │ ✓ Multi-LLM routing (different providers) │ │
│ │ ✓ Operational flexibility (scale each component) │ │
│ │ ✓ Disconnection recovery (offset tracking) │ │
│ │ ✓ Group chat synchronization (WebSocket strength) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
9. Key Code Files Reference
9.1 Chat Shell Module
| File | Purpose |
|---|---|
chat_shell/api/v1/response.py | Main HTTP SSE endpoint, streaming response handler |
chat_shell/services/streaming/core.py | StreamingCore, StreamingState, token processing |
chat_shell/services/streaming/emitters.py | SSEEmitter, RedisPublishingEmitter, NullEmitter |
chat_shell/services/chat_service.py | ChatService implementing ChatInterface |
chat_shell/agent.py | ChatAgent, AgentConfig, tool execution |
chat_shell/agents/graph_builder.py | LangGraphAgentBuilder, ReAct workflow |
chat_shell/main.py | FastAPI app, lifespan management, middleware |
9.2 Backend Module
| File | Purpose |
|---|---|
backend/app/services/chat/streaming/ws_bridge.py | WebSocketBridge, Redis Pub/Sub subscriber |
backend/app/services/chat/streaming/sse_handler.py | BackendStreamHandler, SSE event processing |
backend/app/services/chat/streaming/__init__.py | Module exports, re-exports from services.streaming |
backend/app/services/chat/adapters/http.py | HTTPAdapter for remote Chat Shell communication |
backend/app/api/endpoints/adapter/chat.py | Chat API endpoints, correction endpoints |
9.3 Frontend Module
| File | Purpose |
|---|---|
frontend/src/features/tasks/hooks/useUnifiedMessages.ts | Main message hook, single source of truth |
frontend/src/features/tasks/hooks/useTaskStateMachine.ts | Task state machine subscription hook |
frontend/src/lib/stream-proxy.ts | Next.js streaming proxy utility |
frontend/src/app/api/chat/stream/route.ts | Streaming API route proxy |
10. Summary: Chat Shell's Unique Architecture
10.1 The "Why"
The Chat Shell's direct streaming architecture was designed to solve a specific problem: How to stream LLM responses efficiently while maintaining operational flexibility?
Traditional approaches either:
- Run LLM code inside the backend (tight coupling, hard to scale)
- Use WebSocket for everything (complex connection management for one-way streams)
Chat Shell's solution:
- Decouple the LLM streaming service (Chat Shell) from the orchestration layer (Backend)
- Use HTTP SSE for the actual LLM streaming (simple, proxy-friendly)
- Bridge to WebSocket for frontend integration (unified client experience)
- Use Redis as the intermediate buffer (resilience, multi-worker support)
10.2 The "What"
The architecture consists of:
Chat Shell Service (FastAPI + LangGraph)
- Handles LLM streaming via HTTP SSE
- Multi-LLM support (OpenAI, Anthropic, Google, DeepSeek)
- Tool execution with ReAct pattern
- Redis Publishing Emitter for bridge mode
Backend Bridge Layer
- WebSocketBridge: Subscribes to Redis, forwards to WebSocket
- BackendStreamHandler: Processes HTTP SSE, manages persistence
- SessionManager: Redis cache for recovery
Frontend State Machine
- TaskStateMachine: Single source of truth for messages
- useUnifiedMessages: React hook for message display
- Automatic recovery on reconnection
10.3 The Trade-offs
Advantages:
- Independent scaling of Chat Shell and Backend
- Language-agnostic (could use different languages for each)
- Excellent disconnection recovery via offset tracking
- Simple HTTP infrastructure (load balancers, proxies)
- Resilient with Redis buffering
Costs:
- Higher infrastructure complexity (HTTP + Redis + WebSocket)
- Slightly more latency (Redis hop)
- More code to maintain (bridge layer)
- Two connection types to manage
10.4 The Verdict
Chat Shell's architecture is optimal for AI/LLM streaming systems where:
- One-way streaming dominates (LLM responses)
- Service independence is required
- Operational flexibility matters (scale independently)
- Recovery from disconnections is critical
- Multiple LLM providers need to be supported
The bridge pattern elegantly solves the impedance mismatch between HTTP SSE (perfect for LLM streaming) and WebSocket (perfect for frontend state synchronization).
Document generated for Wegent Chat Shell deep dive analysisArchitecture version: 2025.1