yao/agent/store/CHAT_STORAGE_DESIGN.md
Max 6fef453d46 Enhance CHAT_STORAGE_DESIGN.md with locale support and assistant details
- Added `locale` query parameter for assistant information retrieval, allowing for localization based on user preferences.
- Introduced `assistants` field in the response to provide localized details (name, avatar, description) for unique assistant IDs, improving frontend display capabilities.
- Updated documentation to clarify locale resolution priority and the significance of the new `assistants` field in chat responses.
2025-12-10 12:25:27 +08:00

63 KiB

Chat Storage Design

This document describes the design for storing chat conversations, messages, and execution steps in the YAO Agent system.

Table of Contents

Overview

The chat storage system is designed to:

  1. Store user-visible messages - All messages sent via ctx.Send(), including text, images, loading states, etc.
  2. Support resume/retry - Track execution steps to enable recovery from interruptions or failures
  3. Efficient writes - Batch message writes at request end

Design Goals

Goal Solution
Complete chat history Store final content of all ctx.Send() messages
Resume from interruption Track step status and input/output
Retry failed operations Store step input for re-execution
Minimize database writes Batch writes at request end

Non-Goals

  • Tracing/debugging - Handled by separate Trace module
  • Streaming replay - Not needed, history shows final content only
  • Request tracking/billing - Handled by OpenAPI Request module

Relationship with OpenAPI Request

The Agent storage focuses on chat content and execution state, while request tracking (billing, rate limiting, auditing) is handled globally by the OpenAPI layer:

Concern Module Table
Request tracking openapi/request openapi_request
Billing (tokens) openapi/request openapi_request
Rate limiting openapi/request -
Chat sessions agent/store agent_chat
Chat messages agent/store agent_message
Resume/Retry agent/store agent_resume

The request_id from OpenAPI middleware is passed to Agent and stored in messages/steps for correlation.

Architecture

┌─────────────────────────────────────────────────────────────┐
│                    Chat Storage                              │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌─────────────────┐                                        │
│  │      Chat       │  Metadata: title, assistant, user      │
│  └────────┬────────┘                                        │
│           │                                                  │
│           │ 1:N                                              │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │    Message      │  User-visible: type, props, role       │
│  └────────┬────────┘                                        │
│           │                                                  │
│           │ N:N (via request_id)                            │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │     Resume      │  Recovery: type, status, input/output  │
│  │  (only on fail) │  Only saved when interrupted/failed    │
│  └─────────────────┘                                        │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Data Models

1. Chat Table

Stores chat metadata and session information.

Table Name: agent_chat

Column Type Nullable Index Description
id ID No PK Auto-increment primary key
chat_id string(64) No Unique Unique chat identifier
title string(500) Yes - Chat title
assistant_id string(200) No Yes Associated assistant ID
last_connector string(200) Yes Yes Last used connector ID
last_mode string(50) Yes - Last used chat mode (chat/task)
status enum No Yes Status: active, archived
public boolean No - Whether shared across all teams
share enum No Yes Sharing scope: private, team
sort integer No - Sort order for display
last_message_at timestamp Yes Yes Timestamp of last message
metadata json Yes - Additional metadata
created_at timestamp No Yes Creation timestamp
updated_at timestamp No - Last update timestamp

Model Options:

{
  "option": {
    "soft_deletes": true,
    "permission": true,
    "timestamps": true
  }
}

Note: permission: true enables Yao's built-in permission management, which automatically adds the following fields:

Field Type Description
__yao_created_by string(200) User ID who created the record
__yao_updated_by string(200) User ID who last updated
__yao_team_id string(200) Team ID for team-level access
__yao_tenant_id string(200) Tenant ID for multi-tenancy

These fields are automatically managed by the framework and used for access control filtering.

Indexes:

Name Columns Type
idx_chat_assistant assistant_id index
idx_chat_last_conn last_connector index
idx_chat_status status index
idx_chat_share share index
idx_chat_last_msg last_message_at index

2. Message Table

Stores user-visible messages (both user input and assistant responses).

Table Name: agent_message

Column Type Nullable Index Description
id ID No PK Auto-increment primary key
message_id string(64) No - Message identifier (unique within request)
chat_id string(64) No Yes Parent chat ID
request_id string(64) Yes Yes Request ID for grouping
role enum No Yes Role: user, assistant
type string(50) No - Message type (text, image, loading, etc.)
props json No - Message properties (content, url, etc.)
block_id string(64) Yes Yes Block grouping ID
thread_id string(64) Yes Yes Thread grouping ID
assistant_id string(200) Yes Yes Assistant ID (join to get name/avatar)
connector string(200) Yes Yes Connector ID used for this message
mode string(50) Yes - Chat mode used for this message (chat/task)
sequence integer No - Message order within chat (in composite)
metadata json Yes - Additional metadata
created_at timestamp No Yes Creation timestamp
updated_at timestamp No - Last update timestamp

Indexes:

Name Columns Type
idx_msg_chat_seq chat_id, sequence index
idx_msg_request_message request_id, message_id unique
idx_msg_request request_id index
idx_msg_role role index
idx_msg_block block_id index
idx_msg_thread thread_id index
idx_msg_assistant assistant_id index

Message Ordering:

Messages are ordered by created_at first, then by sequence within the same timestamp. This ensures correct chronological order when there are multiple requests with overlapping sequence numbers:

ORDER BY created_at ASC, sequence ASC

Why this ordering?

  • sequence is assigned per-request, so different requests may have the same sequence numbers
  • created_at groups messages by request time, ensuring messages from earlier requests appear first
  • Within the same request (same created_at), sequence preserves the internal ordering

Message Types:

All message types are stored, including built-in types and custom types. See agent/output/BUILTIN_TYPES.md for built-in Props structures.

Type Description Props Example Stored?
user_input User input (frontend display) {"content": "Hello", "role": "user", "name": "John"} Yes
text Text/Markdown content {"content": "Hello **world**!"} Yes
thinking Reasoning process (o1, DeepSeek) {"content": "Let me analyze..."} Yes
loading Loading/processing indicator {"message": "Searching knowledge base..."} Yes
tool_call LLM tool/function call {"id": "call_abc123", "name": "get_weather", "arguments": "{\"location\":\"SF\"}"} Yes
retrieval KB/Web search results {"query": "...", "sources": [...], "total_results": 10} Yes
error Error message {"message": "Connection timeout", "code": "TIMEOUT", "details": "..."} Yes
image Image content {"url": "...", "alt": "...", "width": 200, "height": 200, "detail": "auto"} Yes
audio Audio content {"url": "...", "format": "mp3", "duration": 120.5, "transcript": "...", "controls": true} Yes
video Video content {"url": "...", "format": "mp4", "thumbnail": "...", "width": 640, "height": 360} Yes
action System action (CUI only) {"name": "open_panel", "payload": {"panel_id": "user_profile"}} Yes
event Lifecycle event (CUI only) {"event": "stream_start", "message": "...", "data": {...}} No
* (custom) Any custom type {"chartType": "bar", "data": [...], "options": {...}} Yes

Note on event type: Lifecycle events (stream_start, stream_end, etc.) are transient control signals and are NOT stored. They are only used for real-time streaming coordination.

Note on custom types: Any type not in the built-in list is stored as-is with its original type and props structure.

Tool Call Storage:

Tool calls from LLM responses are stored as tool_call type messages. The raw tool call data is preserved in props:

{
  "message_id": "msg_001",
  "chat_id": "chat_123",
  "role": "assistant",
  "type": "tool_call",
  "props": {
    "id": "call_abc123",
    "name": "get_weather",
    "arguments": "{\"location\": \"San Francisco\", \"unit\": \"celsius\"}"
  },
  "block_id": "B1",
  "sequence": 5
}

Tool Result Storage:

Tool execution results can be stored as text type with metadata indicating it's a tool result:

{
  "message_id": "msg_002",
  "chat_id": "chat_123",
  "role": "assistant",
  "type": "text",
  "props": {
    "content": "The weather in San Francisco is 18°C and sunny."
  },
  "metadata": {
    "tool_call_id": "call_abc123",
    "tool_name": "get_weather",
    "is_tool_result": true
  },
  "block_id": "B1",
  "sequence": 6
}

Custom Types:

Any type not in the built-in list is considered a custom type and stored with its original structure:

{
  "type": "chart",
  "props": {
    "chartType": "bar",
    "data": [...],
    "options": {...}
  }
}

Multimodal User Input:

User input with multimodal content (text + images + files) is stored as user_input type:

{
  "message_id": "msg_000",
  "chat_id": "chat_123",
  "role": "user",
  "type": "user_input",
  "props": {
    "content": [
      { "type": "text", "text": "What's in this image?" },
      {
        "type": "image_url",
        "image_url": {
          "url": "https://example.com/photo.jpg",
          "detail": "high"
        }
      }
    ],
    "role": "user",
    "name": "John"
  },
  "sequence": 1
}

Knowledge Base & Web Search Results

Retrieval results from knowledge bases and web searches need to be stored for:

  1. User Feedback - Users can rate (👍/👎) individual sources
  2. Quality Analytics - Track which documents/sources are most useful
  3. Source Attribution - Display citations in the UI
  4. RAG Optimization - Improve retrieval based on feedback

Storage Approach: Store retrieval results as a special message type retrieval with structured props.

Retrieval Message Structure:

{
  "message_id": "msg_retrieval_001",
  "chat_id": "chat_123",
  "request_id": "req_abc",
  "role": "assistant",
  "type": "retrieval",
  "props": {
    "query": "How to configure Yao models?",
    "sources": [
      {
        "id": "src_001",
        "type": "kb",
        "collection_id": "col_docs",
        "document_id": "doc_123",
        "chunk_id": "chunk_456",
        "title": "Model Configuration Guide",
        "content": "To configure a model in Yao, create a .mod.yao file...",
        "score": 0.92,
        "metadata": {
          "file_path": "/docs/model.md",
          "page": 3
        }
      },
      {
        "id": "src_002",
        "type": "kb",
        "collection_id": "col_docs",
        "document_id": "doc_124",
        "chunk_id": "chunk_789",
        "title": "Advanced Model Options",
        "content": "Models support various options including soft_deletes...",
        "score": 0.87,
        "metadata": {
          "file_path": "/docs/advanced.md",
          "page": 12
        }
      },
      {
        "id": "src_003",
        "type": "web",
        "url": "https://yaoapps.com/docs/models",
        "title": "Yao Models Documentation",
        "content": "Official documentation for Yao model system...",
        "score": 0.85,
        "metadata": {
          "domain": "yaoapps.com",
          "fetched_at": "2024-01-15T10:30:00Z"
        }
      }
    ],
    "total_results": 15,
    "query_time_ms": 120
  },
  "block_id": "B1",
  "assistant_id": "docs_assistant",
  "sequence": 2
}

Source Types:

Type Description Key Fields
kb Knowledge base document collection_id, document_id, chunk_id
web Web search result url, domain
file Uploaded file file_id, file_path
api External API result api_name, endpoint
mcp MCP tool result server, tool

Source Feedback:

User feedback on retrieval sources is handled by the Knowledge Base module. See KB Feedback for details.

Example: KB Search in Create Hook:

// In Create hook, search knowledge base and store results
const results = await ctx.kb.search("col_docs", query, { limit: 5 });

// Send retrieval message (stored automatically)
ctx.Send({
  type: "retrieval",
  props: {
    query: query,
    sources: results.documents.map((doc, idx) => ({
      id: `src_${idx}`,
      type: "kb",
      collection_id: "col_docs",
      document_id: doc.document.metadata.document_id,
      chunk_id: doc.document.id,
      title: doc.document.metadata.title || "Untitled",
      content: doc.document.content,
      score: doc.score,
      metadata: doc.document.metadata,
    })),
    total_results: results.total,
    query_time_ms: results.query_time_ms,
  },
});

// Also send loading message for user feedback
ctx.Send({
  type: "loading",
  props: { message: `Found ${results.total} relevant documents...` },
});

Example: Web Search Results:

{
  "type": "retrieval",
  "props": {
    "query": "latest AI news 2024",
    "sources": [
      {
        "id": "src_001",
        "type": "web",
        "url": "https://example.com/ai-news",
        "title": "AI Breakthroughs in 2024",
        "content": "Summary of the article...",
        "score": 0.95,
        "metadata": {
          "domain": "example.com",
          "published_at": "2024-01-10",
          "fetched_at": "2024-01-15T10:30:00Z",
          "snippet": "The year 2024 has seen remarkable..."
        }
      }
    ],
    "provider": "tavily",
    "total_results": 10,
    "query_time_ms": 850
  }
}

3. Resume Table

Stores execution state for resume/retry functionality. Only written when request is interrupted or failed.

Table Name: agent_resume

Column Type Nullable Index Description
id ID No PK Auto-increment primary key
resume_id string(64) No Unique Unique resume record identifier
chat_id string(64) No Yes Parent chat ID
request_id string(64) No Yes Request ID
assistant_id string(200) No Yes Assistant executing this step
stack_id string(64) No Yes Stack node ID for this execution
stack_parent_id string(64) Yes Yes Parent stack ID (for A2A calls)
stack_depth integer No - Call depth (0=root, 1+=nested)
type enum No Yes Step type
status enum No Yes Status: interrupted, failed
input json Yes - Step input data
output json Yes - Step output data (partial)
space_snapshot json Yes - Space data snapshot for recovery
error text Yes - Error message if failed
sequence integer No - Step order within request (in composite)
metadata json Yes - Additional metadata
created_at timestamp No Yes Creation timestamp
updated_at timestamp No - Last update timestamp

Space Snapshot:

The space_snapshot field stores the shared data space (ctx.Space) at each step for recovery purposes.

// Example: In Next hook, set data to Space before delegate
ctx.space.Set("choose_prompt", "query");
return {
  delegate: { agent_id: "expense", messages: payload.messages },
};

If interrupted during delegate, the space_snapshot allows restoring ctx.Space state:

{
  "choose_prompt": "query",
  "user_preferences": { "currency": "USD" }
}

Resume Step Types:

Type Description Input Output
input User input received {messages: [...]} -
hook_create Create hook execution {messages: [...]} {messages: [...], ...}
llm LLM completion call {messages: [...]} {content: "...", tool_calls: [...]}
tool Tool/MCP execution {server, tool, args} {result: ...}
hook_next Next hook execution {completion, tools} {data: ...}
delegate A2A delegation {agent_id, messages} {response: ...}

Resume Status (only two values - table only stores failed/interrupted):

Status Description Action
failed Failed with error Retry
interrupted User interrupted Continue

Indexes:

Name Columns Type
idx_resume_chat chat_id index
idx_resume_request request_id, sequence index
idx_resume_type type index
idx_resume_status status index
idx_resume_stack stack_id index
idx_resume_parent stack_parent_id index
idx_resume_assistant assistant_id index

Write Strategy

Single-Write Strategy

All data is buffered in memory during execution and written to database only once when Stream() exits:

Note: Request tracking (status, tokens, duration) is handled by OpenAPI Request Middleware.

Stream() Entry
    │
    ├── Buffer user input message (role=user)
    │
    ├── Execution (all in memory)
    │   - ctx.Send()    → messageBuffer
    │   - ctx.Append()  → update messageBuffer
    │   - ctx.Replace() → update messageBuffer
    │   - Each step     → stepBuffer
    │
    └── 【Single Write】Save final state (via defer)
        │
        ├── Always:
        │   - Batch write all messages (user input + assistant responses)
        │   - Update token usage in openapi_request (via request_id)
        │
        └── Only on error/interrupt:
            - Batch write all steps (for resume/retry)

Write Points

Event Message Table Step Table Token Usage
Stream entry Buffer user input - -
During execution Buffer in memory Buffer in memory -
Completed Batch write all (user + assistant) Skip (no need to resume) Update
On interrupt Batch write all buffered Batch write (status=interrupted) Update
On error Batch write all buffered Batch write (status=failed) Update

Why skip Steps on success?

  • Steps are only needed for resume/retry operations
  • If completed successfully, there's nothing to resume
  • Reduces database writes and keeps Resume table clean

Why Single Write?

Scenario What Happens Data Safe?
Normal completion defer triggers → Write executes
User clicks stop defer triggers → Write executes
LLM timeout defer triggers → Write executes
Tool failure defer triggers → Write executes
Network disconnect defer triggers → Write executes
Process crash Service is down, user must retry N/A

Note: Process crash is a catastrophic failure handled at infrastructure level, not application level.

Write Count Comparison

For a typical request: user input → hook_create → llm → tool → hook_next → 5 messages

Strategy Database Writes Notes
Write per operation 1 + 5 + 5 = 11 One write per step
Single-write strategy 1 Exit only (via defer)

Implementation

func (ast *Assistant) Stream(ctx, inputMessages, options) {
    // ========== Memory Buffers ==========
    messageBuffer := NewMessageBuffer()
    stepBuffer := NewStepBuffer()

    // Buffer user input message (not written yet)
    userMsg := createUserMessage(ctx, inputMessages)
    messageBuffer.Add(userMsg)

    // Track current step for error handling
    var currentStep *Step

    defer func() {
        // ========== Single Write: Exit (always executes) ==========
        // Determine final status for incomplete steps
        finalStatus := "completed"
        if ctx.IsInterrupted() {
            finalStatus = "interrupted"
        }
        if r := recover(); r != nil {
            finalStatus = "failed"
        }

        // Update status of any incomplete step
        if currentStep != nil && currentStep.Status == "running" {
            currentStep.Status = finalStatus
        }

        // Batch write all buffered messages (user input + assistant responses)
        chatStore.SaveMessages(ctx.ChatID, messageBuffer.GetAll())

        // Only save steps on error/interrupt (not on success)
        if finalStatus != "completed" {
            chatStore.SaveResume(stepBuffer.GetAll())
        }

        // Update token usage in OpenAPI request record
        if ctx.RequestID != "" && completionResponse != nil {
            request.UpdateTokenUsage(
                ctx.RequestID,
                completionResponse.Usage.PromptTokens,
                completionResponse.Usage.CompletionTokens,
            )
        }
    }()

    // ========== Execution (all in memory) ==========
    // Note: request_id = ctx.RequestID (from OpenAPI middleware)

    // hook_create
    currentStep = stepBuffer.Add(createStep(ctx, "hook_create", "running", input, nil))
    createResponse := ast.HookScript.Create(...)
    currentStep.Output = createResponse
    currentStep.Status = "completed"

    // llm
    currentStep = stepBuffer.Add(createStep(ctx, "llm", "running", messages, nil))
    completionResponse := ast.executeLLMStream(...)
    currentStep.Output = completionResponse
    currentStep.Status = "completed"

    // tool (if any)
    for _, toolCall := range completionResponse.ToolCalls {
        currentStep = stepBuffer.Add(createStep(ctx, "tool", "running", toolCall, nil))
        result := executeToolCall(toolCall)
        currentStep.Output = result
        currentStep.Status = "completed"
    }

    // hook_next
    currentStep = stepBuffer.Add(createStep(ctx, "hook_next", "running", payload, nil))
    nextResponse := ast.HookScript.Next(...)
    currentStep.Output = nextResponse
    currentStep.Status = "completed"
    currentStep = nil // All done

    // Messages are automatically buffered via ctx.Send()
}

// createResumeRecord creates a resume record with context information
// Only called when request fails or is interrupted
func createResumeRecord(ctx *Context, stepType, status string, input, output interface{}, err error) *Resume {
    // Capture Space snapshot for recovery
    var spaceSnapshot map[string]interface{}
    if ctx.Space != nil {
        spaceSnapshot = ctx.Space.Snapshot() // Get all key-value pairs
    }

    errorMsg := ""
    if err != nil {
        errorMsg = err.Error()
    }

    return &Resume{
        ResumeID:      generateID(),
        ChatID:        ctx.ChatID,        // ChatID
        RequestID:     ctx.RequestID,     // From OpenAPI middleware
        AssistantID:   ctx.AssistantID,
        StackID:       ctx.Stack.ID,
        StackParentID: ctx.Stack.ParentID,
        StackDepth:    ctx.Stack.Depth,
        Type:          stepType,
        Status:        status,            // "failed" or "interrupted"
        Input:         input,
        Output:        output,
        SpaceSnapshot: spaceSnapshot,     // Shared space data for recovery
        Error:         errorMsg,
        Sequence:      nextSequence(),
    }
}

## API Interface

### ChatStore Interface

```go
// ChatStore defines the chat storage interface
// Provides operations for chat, message, and resume management
type ChatStore interface {
    // ==========================================================================
    // Chat Management
    // ==========================================================================

    // CreateChat creates a new chat session
    CreateChat(chat *Chat) error

    // GetChat retrieves a single chat by ID
    GetChat(chatID string) (*Chat, error)

    // UpdateChat updates chat fields
    UpdateChat(chatID string, updates map[string]interface{}) error

    // DeleteChat deletes a chat and its associated messages
    DeleteChat(chatID string) error

    // ListChats retrieves a paginated list of chats with optional grouping
    ListChats(filter ChatFilter) (*ChatList, error)

    // ==========================================================================
    // Message Management
    // ==========================================================================

    // SaveMessages batch saves messages for a chat
    // This is the primary write method - messages are buffered during execution
    // and batch-written at the end of a request
    SaveMessages(chatID string, messages []*Message) error

    // GetMessages retrieves messages for a chat with filtering
    GetMessages(chatID string, filter MessageFilter) ([]*Message, error)

    // UpdateMessage updates a single message
    UpdateMessage(messageID string, updates map[string]interface{}) error

    // DeleteMessages deletes specific messages from a chat
    DeleteMessages(chatID string, messageIDs []string) error

    // ==========================================================================
    // Resume Management (only called on failure/interrupt)
    // ==========================================================================

    // SaveResume batch saves resume records
    // Only called when request is interrupted or failed
    SaveResume(records []*Resume) error

    // GetResume retrieves all resume records for a chat
    GetResume(chatID string) ([]*Resume, error)

    // GetLastResume retrieves the last (most recent) resume record for a chat
    GetLastResume(chatID string) (*Resume, error)

    // GetResumeByStackID retrieves resume records for a specific stack
    GetResumeByStackID(stackID string) ([]*Resume, error)

    // GetStackPath returns the stack path from root to the given stack
    // Returns: [root_stack_id, ..., current_stack_id]
    GetStackPath(stackID string) ([]string, error)

    // DeleteResume deletes all resume records for a chat
    // Called after successful resume to clean up
    DeleteResume(chatID string) error
}

// AssistantStore defines the assistant storage interface
// Separated from ChatStore for clearer responsibility
type AssistantStore interface {
    // SaveAssistant saves assistant information
    SaveAssistant(assistant *AssistantModel) (string, error)

    // UpdateAssistant updates assistant fields
    UpdateAssistant(assistantID string, updates map[string]interface{}) error

    // DeleteAssistant deletes an assistant
    DeleteAssistant(assistantID string) error

    // GetAssistants retrieves a paginated list of assistants with filtering
    GetAssistants(filter AssistantFilter, locale ...string) (*AssistantList, error)

    // GetAssistantTags retrieves all unique tags from assistants with filtering
    GetAssistantTags(filter AssistantFilter, locale ...string) ([]Tag, error)

    // GetAssistant retrieves a single assistant by ID
    GetAssistant(assistantID string, fields []string, locale ...string) (*AssistantModel, error)

    // DeleteAssistants deletes assistants based on filter conditions
    DeleteAssistants(filter AssistantFilter) (int64, error)
}

// Store combines ChatStore and AssistantStore interfaces
// This is the main interface for the storage layer
type Store interface {
    ChatStore
    AssistantStore
}

// SpaceStore defines the interface for Space snapshot operations
// Note: Space itself uses plan.Space interface, this is for persistence
type SpaceStore interface {
    // Snapshot returns all key-value pairs in the space
    Snapshot() map[string]interface{}

    // Restore sets multiple key-value pairs from a snapshot
    Restore(data map[string]interface{}) error
}
```

### Data Structures

```go
// Chat represents a chat session
type Chat struct {
    ChatID        string                 `json:"chat_id"`
    Title         string                 `json:"title,omitempty"`
    AssistantID   string                 `json:"assistant_id"`
    LastConnector string                 `json:"last_connector,omitempty"` // Last used connector ID (updated on each message)
    LastMode      string                 `json:"last_mode,omitempty"`      // Last used chat mode (updated on each message)
    Status        string                 `json:"status"`          // "active" or "archived"
    Public        bool                   `json:"public"`          // Whether shared across all teams
    Share         string                 `json:"share"`           // "private" or "team"
    Sort          int                    `json:"sort"`            // Sort order for display
    LastMessageAt *time.Time             `json:"last_message_at,omitempty"`
    Metadata      map[string]interface{} `json:"metadata,omitempty"`
    CreatedAt     time.Time              `json:"created_at"`
    UpdatedAt     time.Time              `json:"updated_at"`
}

// Message represents a chat message
type Message struct {
    MessageID   string                 `json:"message_id"`
    ChatID      string                 `json:"chat_id"`
    RequestID   string                 `json:"request_id,omitempty"`
    Role        string                 `json:"role"` // "user" or "assistant"
    Type        string                 `json:"type"` // "text", "image", "loading", "tool_call", "retrieval", etc.
    Props       map[string]interface{} `json:"props"`
    BlockID     string                 `json:"block_id,omitempty"`
    ThreadID    string                 `json:"thread_id,omitempty"`
    AssistantID string                 `json:"assistant_id,omitempty"`
    Connector   string                 `json:"connector,omitempty"` // Connector ID used for this message
    Mode        string                 `json:"mode,omitempty"`      // Chat mode used for this message (chat or task)
    Sequence    int                    `json:"sequence"`
    Metadata    map[string]interface{} `json:"metadata,omitempty"`
    CreatedAt   time.Time              `json:"created_at"`
    UpdatedAt   time.Time              `json:"updated_at"`
}

// Resume represents an execution state for recovery
// Only stored when request is interrupted or failed
type Resume struct {
    ResumeID      string                 `json:"resume_id"`
    ChatID        string                 `json:"chat_id"`
    RequestID     string                 `json:"request_id"`
    AssistantID   string                 `json:"assistant_id"`
    StackID       string                 `json:"stack_id"`
    StackParentID string                 `json:"stack_parent_id,omitempty"`
    StackDepth    int                    `json:"stack_depth"`
    Type          string                 `json:"type"`   // "input", "hook_create", "llm", "tool", "hook_next", "delegate"
    Status        string                 `json:"status"` // "failed" or "interrupted"
    Input         map[string]interface{} `json:"input,omitempty"`
    Output        map[string]interface{} `json:"output,omitempty"`
    SpaceSnapshot map[string]interface{} `json:"space_snapshot,omitempty"` // Shared space data for recovery
    Error         string                 `json:"error,omitempty"`
    Sequence      int                    `json:"sequence"`
    Metadata      map[string]interface{} `json:"metadata,omitempty"`
    CreatedAt     time.Time              `json:"created_at"`
    UpdatedAt     time.Time              `json:"updated_at"`
}

// ResumeStatus constants
const (
    ResumeStatusFailed      = "failed"
    ResumeStatusInterrupted = "interrupted"
)

// ResumeType constants
const (
    ResumeTypeInput      = "input"
    ResumeTypeHookCreate = "hook_create"
    ResumeTypeLLM        = "llm"
    ResumeTypeTool       = "tool"
    ResumeTypeHookNext   = "hook_next"
    ResumeTypeDelegate   = "delegate"
)
```

### Filter Structures

```go
// ChatFilter for listing chats
type ChatFilter struct {
    // Permission filters (direct filtering on Yao permission fields)
    UserID string `json:"user_id,omitempty"` // Filter by __yao_created_by
    TeamID string `json:"team_id,omitempty"` // Filter by __yao_team_id

    // Business filters
    AssistantID string `json:"assistant_id,omitempty"`
    Status      string `json:"status,omitempty"`
    Keywords    string `json:"keywords,omitempty"`

    // Time range filter
    StartTime *time.Time `json:"start_time,omitempty"` // Filter chats after this time
    EndTime   *time.Time `json:"end_time,omitempty"`   // Filter chats before this time
    TimeField string     `json:"time_field,omitempty"` // Field for time filter: "created_at" or "last_message_at" (default)

    // Sorting
    OrderBy string `json:"order_by,omitempty"` // Field to sort by (default: "last_message_at")
    Order   string `json:"order,omitempty"`    // Sort order: "desc" (default) or "asc"

    // Response format
    GroupBy string `json:"group_by,omitempty"` // "time" for time-based groups, empty for flat list

    // Pagination
    Page     int `json:"page,omitempty"`
    PageSize int `json:"pagesize,omitempty"`

    // Advanced permission filter (not serialized)
    // Use for complex conditions like: (created_by = user OR team_id = team)
    QueryFilter func(query.Query) `json:"-"`
}

// MessageFilter for listing messages
type MessageFilter struct {
    RequestID string `json:"request_id,omitempty"`
    Role      string `json:"role,omitempty"`
    BlockID   string `json:"block_id,omitempty"`
    ThreadID  string `json:"thread_id,omitempty"`
    Type      string `json:"type,omitempty"`
    Limit     int    `json:"limit,omitempty"`
    Offset    int    `json:"offset,omitempty"`
}

// ChatList paginated response with time-based grouping
type ChatList struct {
    Data      []*Chat      `json:"data"`
    Groups    []*ChatGroup `json:"groups,omitempty"` // Time-based groups for UI display
    Page      int          `json:"page"`
    PageSize  int          `json:"pagesize"`
    PageCount int          `json:"pagecount"`
    Total     int          `json:"total"`
}

// ChatGroup represents a time-based group of chats
type ChatGroup struct {
    Label string   `json:"label"` // "Today", "Yesterday", "This Week", "This Month", "Earlier"
    Key   string   `json:"key"`   // "today", "yesterday", "this_week", "this_month", "earlier"
    Chats []*Chat  `json:"chats"` // Chats in this group
    Count int      `json:"count"` // Number of chats in group
}
```

## Usage Examples

### 1. Complete Message Storage Example

A typical conversation with various message types stored in `agent_message`:

```
User: "What's the weather in SF? Also show me a chart."

Timeline (user input → hook_create → llm → tool → hook_next):
1. User sends input
2. Create hook shows loading state
3. LLM thinks and calls tool
4. Tool executes and returns result
5. Next hook generates text response and image chart
```

**Stored Messages:**

```json
[
  // 1. User input (role=user, type=user_input)
  {
    "message_id": "msg_001",
    "chat_id": "chat_123",
    "request_id": "req_abc",
    "role": "user",
    "type": "user_input",
    "props": {
      "content": "What's the weather in SF? Also show me a chart.",
      "role": "user"
    },
    "sequence": 1
  },

  // 2. Loading state from Create hook (role=assistant, type=loading)
  {
    "message_id": "msg_002",
    "chat_id": "chat_123",
    "request_id": "req_abc",
    "role": "assistant",
    "type": "loading",
    "props": {
      "message": "Searching knowledge base..."
    },
    "block_id": "B1",
    "assistant_id": "weather_assistant",
    "sequence": 2
  },

  // 3. LLM thinking process (role=assistant, type=thinking)
  {
    "message_id": "msg_003",
    "chat_id": "chat_123",
    "request_id": "req_abc",
    "role": "assistant",
    "type": "thinking",
    "props": {
      "content": "User wants weather info for San Francisco. I should use the get_weather tool..."
    },
    "block_id": "B2",
    "assistant_id": "weather_assistant",
    "sequence": 3
  },

  // 4. LLM tool call (role=assistant, type=tool_call)
  {
    "message_id": "msg_004",
    "chat_id": "chat_123",
    "request_id": "req_abc",
    "role": "assistant",
    "type": "tool_call",
    "props": {
      "id": "call_weather_001",
      "name": "get_weather",
      "arguments": "{\"location\": \"San Francisco\", \"unit\": \"celsius\"}"
    },
    "block_id": "B2",
    "assistant_id": "weather_assistant",
    "sequence": 4
  },

  // 5. Tool result from Next hook (role=assistant, type=text, with tool metadata)
  {
    "message_id": "msg_005",
    "chat_id": "chat_123",
    "request_id": "req_abc",
    "role": "assistant",
    "type": "text",
    "props": {
      "content": "The weather in San Francisco is currently **18°C** and sunny with 65% humidity. Perfect weather for outdoor activities!"
    },
    "block_id": "B3",
    "metadata": {
      "tool_call_id": "call_weather_001",
      "tool_name": "get_weather"
    },
    "assistant_id": "weather_assistant",
    "sequence": 5
  },

  // 6. Chart image from Next hook (role=assistant, type=image)
  {
    "message_id": "msg_006",
    "chat_id": "chat_123",
    "request_id": "req_abc",
    "role": "assistant",
    "type": "image",
    "props": {
      "url": "https://charts.example.com/weather_sf.png",
      "alt": "San Francisco 7-day weather forecast",
      "width": 800,
      "height": 400
    },
    "block_id": "B3",
    "assistant_id": "weather_assistant",
    "sequence": 6
  }
]
```

**Streaming IDs (from `STREAMING.md`):**

During streaming, messages include additional fields for real-time delivery:

| Field        | Purpose                        | Stored? |
| ------------ | ------------------------------ | ------- |
| `chunk_id`   | Deduplication, ordering, debug |  No   |
| `message_id` | Delta merge target             |  Yes  |
| `block_id`   | UI block/section grouping      |  Yes  |
| `thread_id`  | Concurrent stream distinction  |  Yes  |
| `delta`      | Whether this is a delta chunk  |  No   |
| `delta_path` | Path for delta merge           |  No   |

**Note:** `chunk_id`, `delta`, and `delta_path` are transient streaming control fields and are NOT stored. Only the final merged content is persisted.

### 2. Error Message Storage

When errors occur, they are stored as `error` type:

```json
{
  "message_id": "msg_err_001",
  "chat_id": "chat_123",
  "request_id": "req_abc",
  "role": "assistant",
  "type": "error",
  "props": {
    "message": "Failed to connect to weather service",
    "code": "SERVICE_UNAVAILABLE",
    "details": "Connection timeout after 30 seconds"
  },
  "block_id": "B2",
  "assistant_id": "weather_assistant",
  "sequence": 5
}
```

### 3. Action Message Storage (CUI clients)

System actions are stored but only processed by CUI clients:

```json
{
  "message_id": "msg_action_001",
  "chat_id": "chat_123",
  "request_id": "req_abc",
  "role": "assistant",
  "type": "action",
  "props": {
    "name": "open_panel",
    "payload": {
      "panel_id": "weather_details",
      "location": "San Francisco"
    }
  },
  "block_id": "B2",
  "assistant_id": "weather_assistant",
  "sequence": 6
}
```

### 4. Audio/Video Message Storage

Multimedia content storage:

```json
// Audio message
{
  "message_id": "msg_audio_001",
  "chat_id": "chat_123",
  "role": "assistant",
  "type": "audio",
  "props": {
    "url": "https://storage.example.com/audio/response.mp3",
    "format": "mp3",
    "duration": 45.5,
    "transcript": "Here's the weather forecast for today...",
    "controls": true
  },
  "sequence": 7
}

// Video message
{
  "message_id": "msg_video_001",
  "chat_id": "chat_123",
  "role": "assistant",
  "type": "video",
  "props": {
    "url": "https://storage.example.com/video/weather_report.mp4",
    "format": "mp4",
    "thumbnail": "https://storage.example.com/video/weather_report_thumb.jpg",
    "duration": 120.0,
    "width": 1280,
    "height": 720,
    "controls": true
  },
  "sequence": 8
}
```

### 5. Load Chat History

```go
// Example 1: Filter by user (simple permission check)
chats, _ := chatStore.ListChats(ChatFilter{
    UserID:   "user123",  // Filters by __yao_created_by
    Status:   "active",
    OrderBy:  "last_message_at",
    Order:    "desc",
    Page:     1,
    PageSize: 20,
})
// Response: chats.Data = [...], chats.Groups = nil

// Example 2: Filter by team
chats, _ := chatStore.ListChats(ChatFilter{
    TeamID:   "team456",  // Filters by __yao_team_id
    Status:   "active",
    Page:     1,
    PageSize: 20,
})

// Example 3: Filter by user AND team (both must match)
chats, _ := chatStore.ListChats(ChatFilter{
    UserID:   "user123",
    TeamID:   "team456",
    Page:     1,
    PageSize: 20,
})

// Example 4: Complex permission filter (user OR team) using QueryFilter
chats, _ := chatStore.ListChats(ChatFilter{
    Page:     1,
    PageSize: 20,
    QueryFilter: func(qb query.Query) {
        qb.Where(func(sub query.Query) {
            sub.Where("__yao_created_by", "user123").
                OrWhere("__yao_team_id", "team456")
        })
    },
})

// Example 5: Grouped by time
chats, _ := chatStore.ListChats(ChatFilter{
    UserID:   "user123",
    GroupBy:  "time", // Enable time-based grouping
    OrderBy:  "last_message_at",
    Order:    "desc",
    Page:     1,
    PageSize: 20,
})
// Response includes time-based groups:
// chats.Groups = [
//   { Key: "today", Label: "Today", Chats: [...], Count: 3 },
//   { Key: "yesterday", Label: "Yesterday", Chats: [...], Count: 5 },
//   { Key: "this_week", Label: "This Week", Chats: [...], Count: 8 },
//   { Key: "this_month", Label: "This Month", Chats: [...], Count: 4 },
//   { Key: "earlier", Label: "Earlier", Chats: [...], Count: 0 },
// ]

// Example 6: Filter by time range
startTime := time.Now().AddDate(0, 0, -7) // Last 7 days
chats, _ := chatStore.ListChats(ChatFilter{
    UserID:    "user123",
    StartTime: &startTime,
    TimeField: "last_message_at", // Filter by last message time
    OrderBy:   "last_message_at",
    Order:     "desc",
})

// Example 7: Filter specific date range
start := time.Date(2024, 12, 1, 0, 0, 0, 0, time.Local)
end := time.Date(2024, 12, 31, 23, 59, 59, 0, time.Local)
chats, _ := chatStore.ListChats(ChatFilter{
    UserID:    "user123",
    StartTime: &start,
    EndTime:   &end,
    TimeField: "created_at", // Filter by creation time
})

// Example 8: Combine permission with business filters
chats, _ := chatStore.ListChats(ChatFilter{
    UserID:      "user123",
    TeamID:      "team456",
    AssistantID: "weather_assistant",
    Status:      "active",
    Keywords:    "weather",
    Page:        1,
    PageSize:    20,
})

// Get messages for a chat
messages, _ := chatStore.GetMessages("chat_123", MessageFilter{
    Limit: 100,
})

// Return to frontend
return map[string]interface{}{
    "chat":     chat,
    "messages": messages,
}
```

### 6. Resume from Interruption

```go
func (ast *Assistant) Resume(ctx *Context) error {
    // 1. Find last resume record
    record, _ := chatStore.GetLastResume(ctx.ChatID)
    if record == nil {
        return nil // Nothing to resume
    }

    // 2. Restore Space data from snapshot
    if record.SpaceSnapshot != nil && ctx.Space != nil {
        for key, value := range record.SpaceSnapshot {
            ctx.Space.Set(key, value)
        }
    }

    // 3. Check if this is an A2A nested call
    if record.StackDepth > 0 {
        // Need to rebuild the call stack
        return ast.ResumeNestedCall(ctx, record)
    }

    // 4. Resume based on step type
    var err error
    switch record.Type {
    case "llm":
        // Re-execute LLM call with saved input
        messages := record.Input["messages"].([]Message)
        err = ast.executeLLMStream(ctx, messages, ...)

    case "tool":
        // Retry tool call
        err = ast.retryToolCall(ctx, record)

    case "hook_next":
        // Re-execute hook
        err = ast.executeHookNext(ctx, record.Input)

    case "delegate":
        // Resume delegated agent call
        agentID := record.Input["agent_id"].(string)
        messages := record.Input["messages"].([]Message)
        err = ast.delegateToAgent(ctx, agentID, messages)
    }

    // 5. Clean up resume records on success
    if err == nil {
        chatStore.DeleteResume(ctx.ChatID)
    }

    return err
}
```

### 7. Resume A2A Nested Calls

For agent-to-agent (A2A) recursive calls, the stack information is essential for proper recovery.

```go
func (ast *Assistant) ResumeNestedCall(ctx *Context, step *Step) error {
    // 1. Rebuild the call stack from root to interrupted point
    stackPath, _ := chatStore.GetStackPath(step.StackID)
    // stackPath: [root_stack_id, parent_stack_id, ..., current_stack_id]

    // 2. Get all steps for each stack level
    for _, stackID := range stackPath {
        steps, _ := chatStore.GetStepsByStackID(stackID)
        // Restore context for each level
    }

    // 3. Resume from the interrupted assistant
    targetAssistant := assistant.Select(step.AssistantID)
    return targetAssistant.Stream(ctx, step.Input["messages"], ...)
}
```

### 8. Handle Interruption

Interruption is handled automatically by the `defer` block in the two-write strategy. When `ctx.IsInterrupted()` returns true, the status is set to `interrupted` and all buffered data is saved.

```go
// Inside the defer block (see Write Strategy - Implementation)
if ctx.IsInterrupted() {
    status = "interrupted"
}
// Then batch write all buffered messages and steps
```

## A2A (Agent-to-Agent) Call Example

When Assistant A delegates to Assistant B, the step records look like:

```
Request: User asks "analyze this data and visualize it"

Step Records:
┌─────┬─────────────┬─────────────┬──────────┬───────┬───────┬─────────────┬─────────────────────────────┐
│ seq │ assistant   │ stack_id    │ parent   │ depth │ type  │ status      │ space_snapshot              │
├─────┼─────────────┼─────────────┼──────────┼───────┼───────┼─────────────┼─────────────────────────────┤
│  1  │ analyzer    │ stk_001     │ null     │ 0     │ input │ completed   │ {}                          │
│  2  │ analyzer    │ stk_001     │ null     │ 0     │ llm   │ completed   │ {}                          │
│  3  │ analyzer    │ stk_001     │ null     │ 0     │ delegate │ running  │ {"choose_prompt": "query"}  │ ← Space data set before delegate
│  4  │ visualizer  │ stk_002     │ stk_001  │ 1     │ input │ completed   │ {"choose_prompt": "query"}  │
│  5  │ visualizer  │ stk_002     │ stk_001  │ 1     │ llm   │ interrupted │ {"choose_prompt": "query"}  │ ← interrupted here
└─────┴─────────────┴─────────────┴──────────┴───────┴───────┴─────────────┴─────────────────────────────┘

Resume Flow:
1. Find step with status="interrupted" → step 5
2. Restore Space from space_snapshot: {"choose_prompt": "query"}
3. Check stack_depth=1 → nested call
4. Get stack path: [stk_001, stk_002]
5. Resume visualizer assistant with step 5's input
6. When visualizer completes, update step 3 (delegate) to completed
```

**Space Snapshot Use Case (from expense assistant):**

```typescript
// In Next hook, before delegating to another agent
ctx.space.Set("choose_prompt", "query");
return {
  delegate: { agent_id: "expense", messages: payload.messages },
};

// If interrupted during delegate, Resume will:
// 1. Restore space_snapshot → ctx.space now has "choose_prompt": "query"
// 2. The delegated agent's Create hook can read: ctx.space.GetDel("choose_prompt")
```

## Concurrent Operations Storage

When an Agent makes parallel calls (e.g., multiple MCP tools, multiple sub-agents), messages use `block_id` and `thread_id` for grouping:

```
Main Agent concurrently calls 3 tasks:
├── Thread T1: Weather query (MCP)
├── Thread T2: News search (MCP)
├── Thread T3: Stock query (MCP)
└── Wait for all to complete, then summarize
```

**Stored Messages:**

```json
[
  // All concurrent messages share the same block_id, different thread_id
  // Messages may arrive in any order due to concurrency

  // Thread T1: Weather result
  {
    "message_id": "msg_t1_001",
    "chat_id": "chat_123",
    "request_id": "req_abc",
    "role": "assistant",
    "type": "text",
    "props": { "content": "Weather in SF: 18°C, sunny" },
    "block_id": "B1",
    "thread_id": "T1",
    "assistant_id": "main_assistant",
    "sequence": 2
  },

  // Thread T2: News result
  {
    "message_id": "msg_t2_001",
    "chat_id": "chat_123",
    "request_id": "req_abc",
    "role": "assistant",
    "type": "text",
    "props": { "content": "Top news: AI breakthrough announced..." },
    "block_id": "B1",
    "thread_id": "T2",
    "assistant_id": "main_assistant",
    "sequence": 3
  },

  // Thread T3: Stock result
  {
    "message_id": "msg_t3_001",
    "chat_id": "chat_123",
    "request_id": "req_abc",
    "role": "assistant",
    "type": "text",
    "props": { "content": "AAPL: $185.50 (+1.2%)" },
    "block_id": "B1",
    "thread_id": "T3",
    "assistant_id": "main_assistant",
    "sequence": 4
  },

  // After all threads complete, main agent summarizes (new block)
  {
    "message_id": "msg_summary",
    "chat_id": "chat_123",
    "request_id": "req_abc",
    "role": "assistant",
    "type": "text",
    "props": {
      "content": "Here's your daily briefing: The weather is great at 18°C..."
    },
    "block_id": "B2",
    "thread_id": null,
    "assistant_id": "main_assistant",
    "sequence": 5
  }
]
```

**Key Points:**

| Field       | Concurrent Usage                                   |
| ----------- | -------------------------------------------------- |
| `block_id`  | Same for all parallel operations (B1)              |
| `thread_id` | Different for each concurrent task (T1, T2, T3)    |
| `sequence`  | Reflects actual arrival order (may be interleaved) |

**Frontend Rendering:**

- Group messages by `block_id` for visual blocks
- Within a block, optionally group by `thread_id` to show parallel results
- Use `sequence` for chronological display

## HTTP API

The chat storage provides RESTful HTTP APIs for managing chat sessions and messages.

**Base Path:** `/v1/chat`

### Chat Sessions

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/sessions` | List chat sessions with pagination and filtering |
| `GET` | `/sessions/:chat_id` | Get a single chat session |
| `PUT` | `/sessions/:chat_id` | Update chat session (title, status, metadata) |
| `DELETE` | `/sessions/:chat_id` | Delete chat session |
| `GET` | `/sessions/:chat_id/messages` | Get messages for a chat session |

### List Chat Sessions

**Request:**

```
GET /v1/chat/sessions?page=1&pagesize=20&assistant_id=xxx&status=active&keywords=search&group_by=time
```

**Query Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `page` | int | 1 | Page number |
| `pagesize` | int | 20 | Items per page (max 100) |
| `assistant_id` | string | - | Filter by assistant ID |
| `status` | string | - | Filter by status: `active`, `archived` |
| `keywords` | string | - | Search in title |
| `start_time` | RFC3339 | - | Filter chats after this time |
| `end_time` | RFC3339 | - | Filter chats before this time |
| `time_field` | string | `last_message_at` | Field for time filter: `created_at` or `last_message_at` |
| `order_by` | string | `last_message_at` | Sort field |
| `order` | string | `desc` | Sort order: `asc` or `desc` |
| `group_by` | string | - | Set to `time` for time-based grouping |

**Response:**

```json
{
  "data": [
    {
      "chat_id": "chat_123",
      "title": "Weather Query",
      "assistant_id": "weather_assistant",
      "status": "active",
      "last_message_at": "2024-01-15T10:30:00Z",
      "created_at": "2024-01-15T10:00:00Z"
    }
  ],
  "groups": [
    {
      "key": "today",
      "label": "Today",
      "chats": [...],
      "count": 3
    },
    {
      "key": "yesterday",
      "label": "Yesterday",
      "chats": [...],
      "count": 5
    }
  ],
  "page": 1,
  "pagesize": 20,
  "pagecount": 5,
  "total": 100
}
```

### Get Chat Session

**Request:**

```
GET /v1/chat/sessions/chat_123
```

**Response:**

```json
{
  "chat_id": "chat_123",
  "title": "Weather Query",
  "assistant_id": "weather_assistant",
  "last_connector": "deepseek.v3",
  "last_mode": "chat",
  "status": "active",
  "public": false,
  "share": "private",
  "last_message_at": "2024-01-15T10:30:00Z",
  "metadata": {},
  "created_at": "2024-01-15T10:00:00Z",
  "updated_at": "2024-01-15T10:30:00Z"
}
```

### Update Chat Session

**Request:**

```
PUT /v1/chat/sessions/chat_123
Content-Type: application/json

{
  "title": "New Title",
  "status": "archived",
  "metadata": {"custom_field": "value"}
}
```

**Response:**

```json
{
  "message": "Chat updated successfully",
  "chat_id": "chat_123"
}
```

### Delete Chat Session

**Request:**

```
DELETE /v1/chat/sessions/chat_123
```

**Response:**

```json
{
  "message": "Chat deleted successfully",
  "chat_id": "chat_123"
}
```

### Get Chat Messages

**Request:**

```
GET /v1/chat/sessions/chat_123/messages?limit=100&offset=0&role=assistant&type=text
```

**Query Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `request_id` | string | - | Filter by request ID |
| `role` | string | - | Filter by role: `user`, `assistant` |
| `block_id` | string | - | Filter by block ID |
| `thread_id` | string | - | Filter by thread ID |
| `type` | string | - | Filter by message type |
| `limit` | int | 100 | Max messages to return (max 1000) |
| `offset` | int | 0 | Offset for pagination |
| `locale` | string | - | Locale for assistant info (e.g., `zh-cn`, `en-us`). Falls back to `Accept-Language` header |

**Locale Resolution Priority:**
1. Query parameter `locale`
2. HTTP header `Accept-Language`

**Response:**

```json
{
  "chat_id": "chat_123",
  "messages": [
    {
      "message_id": "msg_001",
      "chat_id": "chat_123",
      "request_id": "req_abc",
      "role": "user",
      "type": "user_input",
      "props": {
        "content": "What's the weather?",
        "role": "user"
      },
      "sequence": 1,
      "created_at": "2024-01-15T10:00:00Z"
    },
    {
      "message_id": "msg_002",
      "chat_id": "chat_123",
      "request_id": "req_abc",
      "role": "assistant",
      "type": "text",
      "props": {
        "content": "The weather in San Francisco is 18°C and sunny."
      },
      "block_id": "B1",
      "assistant_id": "weather_assistant",
      "sequence": 2,
      "created_at": "2024-01-15T10:00:05Z"
    }
  ],
  "count": 2,
  "assistants": {
    "weather_assistant": {
      "assistant_id": "weather_assistant",
      "name": "Weather Assistant",
      "avatar": "https://example.com/weather-avatar.png",
      "description": "Get weather information for any location"
    }
  }
}
```

**Note:** The `assistants` field contains localized assistant information (name, avatar, description) for all unique `assistant_id` values found in the messages. This allows the frontend to display assistant details without additional API calls. The locale is determined by the `locale` query parameter or `Accept-Language` header.

### Permission Filtering

All endpoints respect Yao's permission system:

| Constraint | Behavior |
|------------|----------|
| `OwnerOnly` | User can only access their own chats (`__yao_created_by` matches) |
| `TeamOnly` | User can access own chats OR team-shared chats (`share = "team"`) |
| No constraints | Full access (for admin users) |

**Permission Fields Used:**

- `__yao_created_by`: User who created the chat
- `__yao_team_id`: Team ID for team-level access
- `public`: Whether chat is public to all
- `share`: Sharing scope (`private` or `team`)

## Related Documents

- [OpenAPI Request Design](../../openapi/request/REQUEST_DESIGN.md) - Global request tracking, billing, rate limiting
- [Trace Module](../../trace/README.md) - Detailed execution tracing for debugging
- [Agent Context](../context/README.md) - Context and message handling