- 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.
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:
- Store user-visible messages - All messages sent via
ctx.Send(), including text, images, loading states, etc. - Support resume/retry - Track execution steps to enable recovery from interruptions or failures
- 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?
sequenceis assigned per-request, so different requests may have the same sequence numberscreated_atgroups messages by request time, ensuring messages from earlier requests appear first- Within the same request (same
created_at),sequencepreserves 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:
- User Feedback - Users can rate (👍/👎) individual sources
- Quality Analytics - Track which documents/sources are most useful
- Source Attribution - Display citations in the UI
- 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