Merge pull request #1372 from trheyi/main

Refactor chat storage design by replacing history with message
This commit is contained in:
Max 2025-12-09 10:21:12 +08:00 committed by GitHub
commit f040361f44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1118 additions and 436 deletions

View file

@ -8,17 +8,18 @@ Defined in `types.go`:
```go ```go
const ( const (
TypeUserInput = "user_input" // User input message (frontend display only) TypeUserInput = "user_input" // User input message (frontend display only)
TypeText = "text" // Plain text or Markdown content TypeText = "text" // Plain text or Markdown content
TypeThinking = "thinking" // Reasoning/thinking process TypeThinking = "thinking" // Reasoning/thinking process
TypeLoading = "loading" // Loading/processing indicator TypeLoading = "loading" // Loading/processing indicator
TypeToolCall = "tool_call" // LLM tool/function call TypeToolCall = "tool_call" // LLM tool/function call
TypeError = "error" // Error message TypeRetrieval = "retrieval" // KB/Web search results (for feedback & analytics)
TypeImage = "image" // Image content TypeError = "error" // Error message
TypeAudio = "audio" // Audio content TypeImage = "image" // Image content
TypeVideo = "video" // Video content TypeAudio = "audio" // Audio content
TypeAction = "action" // System action (silent in standard clients) TypeVideo = "video" // Video content
TypeEvent = "event" // Lifecycle event (silent in standard clients) TypeAction = "action" // System action (silent in standard clients)
TypeEvent = "event" // Lifecycle event (silent in standard clients)
) )
``` ```
@ -281,7 +282,137 @@ msg := output.NewToolCallMessage(
--- ---
### 6. Error (`error`) ### 6. Retrieval (`retrieval`)
**Purpose:** Knowledge base and web search results (for feedback, analytics, and source attribution)
**Props Structure:**
```go
type RetrievalProps struct {
Query string `json:"query"` // Search query
Sources []RetrievalSource `json:"sources"` // Retrieved sources
TotalResults int `json:"total_results,omitempty"` // Total matching results
QueryTimeMs int64 `json:"query_time_ms,omitempty"` // Query execution time
Provider string `json:"provider,omitempty"` // Search provider (e.g., "tavily", "bing")
}
type RetrievalSource struct {
ID string `json:"id"` // Unique source ID within this retrieval
Type string `json:"type"` // Source type: "kb", "web", "file", "api", "mcp"
Title string `json:"title,omitempty"` // Source title
Content string `json:"content"` // Retrieved content/snippet
Score float64 `json:"score,omitempty"` // Relevance score
URL string `json:"url,omitempty"` // URL for web sources
CollectionID string `json:"collection_id,omitempty"` // KB collection ID
DocumentID string `json:"document_id,omitempty"` // KB document ID
ChunkID string `json:"chunk_id,omitempty"` // KB chunk ID
Metadata map[string]interface{} `json:"metadata,omitempty"` // Additional metadata
}
```
**Example (Knowledge Base):**
```json
{
"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
}
}
],
"total_results": 15,
"query_time_ms": 120
}
}
```
**Example (Web Search):**
```json
{
"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"
}
}
],
"provider": "tavily",
"total_results": 10,
"query_time_ms": 850
}
}
```
**Helper:**
```go
msg := output.NewRetrievalMessage(
"How to configure Yao models?",
[]output.RetrievalSource{
{
ID: "src_001",
Type: "kb",
CollectionID: "col_docs",
DocumentID: "doc_123",
ChunkID: "chunk_456",
Title: "Model Configuration Guide",
Content: "To configure a model in Yao...",
Score: 0.92,
},
},
)
```
**Source Types:**
| Type | Description | Key Fields |
| ------ | ----------------------- | ------------------------------------------ |
| `kb` | Knowledge base document | `collection_id`, `document_id`, `chunk_id` |
| `web` | Web search result | `url` |
| `file` | Uploaded file | `file_id`, `file_path` |
| `api` | External API result | `api_name`, `endpoint` |
| `mcp` | MCP tool result | `server`, `tool` |
**Use Cases:**
- **Source Attribution**: Display citations in the chat UI
- **User Feedback**: Allow users to rate individual sources (👍/👎)
- **Analytics**: Track which documents/sources are most useful
- **RAG Optimization**: Improve retrieval based on feedback data
**Adapter Behavior:**
- **CUI**: Renders as expandable source cards with feedback buttons
- **OpenAI**: Converts to markdown citations or footnotes
---
### 7. Error (`error`)
**Purpose:** Error message **Purpose:** Error message
@ -316,7 +447,7 @@ msg := output.NewErrorMessage("Connection timeout", "TIMEOUT")
--- ---
### 7. Action (`action`) ### 8. Action (`action`)
**Purpose:** System-level action/command (not displayed to user, only processed by client) **Purpose:** System-level action/command (not displayed to user, only processed by client)
@ -387,7 +518,7 @@ output.Send(ctx, output.NewTextMessage("I've opened the user details panel for y
--- ---
### 8. Event (`event`) ### 9. Event (`event`)
**Purpose:** Lifecycle event messages (stream_start, stream_end, connecting, etc.) **Purpose:** Lifecycle event messages (stream_start, stream_end, connecting, etc.)
@ -479,7 +610,7 @@ output.Send(ctx, output.NewEventMessage("stream_end", "Stream completed", endDat
--- ---
### 9. Image (`image`) ### 10. Image (`image`)
**Purpose:** Image content **Purpose:** Image content
@ -522,7 +653,7 @@ msg := output.NewImageMessage("https://example.com/avatar.jpg", "User avatar")
--- ---
### 10. Audio (`audio`) ### 11. Audio (`audio`)
**Purpose:** Audio content **Purpose:** Audio content
@ -567,7 +698,7 @@ msg := output.NewAudioMessage("https://example.com/audio.mp3", "mp3")
--- ---
### 11. Video (`video`) ### 12. Video (`video`)
**Purpose:** Video content **Purpose:** Video content
@ -644,6 +775,7 @@ OpenAI adapter converts built-in types to OpenAI format:
| `thinking` | `delta.reasoning_content` | `props.content` | Reasoning content (o1 models) | | `thinking` | `delta.reasoning_content` | `props.content` | Reasoning content (o1 models) |
| `loading` | `delta.reasoning_content` | `props.message` | Shows as thinking in OpenAI clients | | `loading` | `delta.reasoning_content` | `props.message` | Shows as thinking in OpenAI clients |
| `tool_call` | `delta.tool_calls` | `props.{id, name, arguments}` | | | `tool_call` | `delta.tool_calls` | `props.{id, name, arguments}` | |
| `retrieval` | `delta.content` | `props.sources` | Markdown citations/footnotes with source links |
| `error` | `error` | `props.{message, code}` | | | `error` | `error` | `props.{message, code}` | |
| `image` | `delta.content` | `props.{url, alt}` | Markdown: `![alt](url)` - displays inline | | `image` | `delta.content` | `props.{url, alt}` | Markdown: `![alt](url)` - displays inline |
| `audio` | `delta.content` | `props.url` | Markdown link (can't display inline) | | `audio` | `delta.content` | `props.url` | Markdown link (can't display inline) |

View file

@ -93,7 +93,6 @@ Stores chat metadata and session information.
| `assistant_id` | string(200) | No | Yes | Associated assistant ID | | `assistant_id` | string(200) | No | Yes | Associated assistant ID |
| `mode` | string(50) | No | - | Chat mode (default: "chat") | | `mode` | string(50) | No | - | Chat mode (default: "chat") |
| `status` | enum | No | Yes | Status: `active`, `archived` | | `status` | enum | No | Yes | Status: `active`, `archived` |
| `preset` | boolean | No | - | Whether this is a preset chat |
| `public` | boolean | No | - | Whether shared across all teams | | `public` | boolean | No | - | Whether shared across all teams |
| `share` | enum | No | Yes | Sharing scope: `private`, `team` | | `share` | enum | No | Yes | Sharing scope: `private`, `team` |
| `sort` | integer | No | - | Sort order for display | | `sort` | integer | No | - | Sort order for display |
@ -152,7 +151,7 @@ Stores user-visible messages (both user input and assistant responses).
| `block_id` | string(64) | Yes | Yes | Block grouping ID | | `block_id` | string(64) | Yes | Yes | Block grouping ID |
| `thread_id` | string(64) | Yes | Yes | Thread grouping ID | | `thread_id` | string(64) | Yes | Yes | Thread grouping ID |
| `assistant_id` | string(200) | Yes | Yes | Assistant ID (join to get name/avatar) | | `assistant_id` | string(200) | Yes | Yes | Assistant ID (join to get name/avatar) |
| `sequence` | integer | No | Yes | Message order within chat | | `sequence` | integer | No | - | Message order within chat (in composite) |
| `metadata` | json | Yes | - | Additional metadata | | `metadata` | json | Yes | - | Additional metadata |
| `created_at` | timestamp | No | Yes | Creation timestamp | | `created_at` | timestamp | No | Yes | Creation timestamp |
| `updated_at` | timestamp | No | - | Last update timestamp | | `updated_at` | timestamp | No | - | Last update timestamp |
@ -163,28 +162,34 @@ Stores user-visible messages (both user input and assistant responses).
| ------------------- | --------------------- | ----- | | ------------------- | --------------------- | ----- |
| `idx_msg_chat_seq` | `chat_id`, `sequence` | index | | `idx_msg_chat_seq` | `chat_id`, `sequence` | index |
| `idx_msg_request` | `request_id` | index | | `idx_msg_request` | `request_id` | index |
| `idx_msg_role` | `role` | index |
| `idx_msg_block` | `block_id` | index | | `idx_msg_block` | `block_id` | index |
| `idx_msg_thread` | `thread_id` | index |
| `idx_msg_assistant` | `assistant_id` | index | | `idx_msg_assistant` | `assistant_id` | index |
**Message Types (Built-in):** **Message Types:**
All built-in types defined in `agent/output/BUILTIN_TYPES.md` are stored. See that document for complete Props structures. 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? | | Type | Description | Props Example | Stored? |
| ------------ | -------------------------------- | ------------------------------------------------------------------------------------------- | ----------- | | ------------ | -------------------------------- | ------------------------------------------------------------------------------------------- | ------- |
| `user_input` | User input (frontend display) | `{"content": "Hello", "role": "user", "name": "John"}` | ✅ Yes | | `user_input` | User input (frontend display) | `{"content": "Hello", "role": "user", "name": "John"}` | ✅ Yes |
| `text` | Text/Markdown content | `{"content": "Hello **world**!"}` | ✅ Yes | | `text` | Text/Markdown content | `{"content": "Hello **world**!"}` | ✅ Yes |
| `thinking` | Reasoning process (o1, DeepSeek) | `{"content": "Let me analyze..."}` | ✅ Yes | | `thinking` | Reasoning process (o1, DeepSeek) | `{"content": "Let me analyze..."}` | ✅ Yes |
| `loading` | Loading/processing indicator | `{"message": "Searching knowledge base..."}` | ✅ 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 | | `tool_call` | LLM tool/function call | `{"id": "call_abc123", "name": "get_weather", "arguments": "{\"location\":\"SF\"}"}` | ✅ Yes |
| `error` | Error message | `{"message": "Connection timeout", "code": "TIMEOUT", "details": "..."}` | ✅ Yes | | `retrieval` | KB/Web search results | `{"query": "...", "sources": [...], "total_results": 10}` | ✅ Yes |
| `image` | Image content | `{"url": "...", "alt": "...", "width": 200, "height": 200, "detail": "auto"}` | ✅ Yes | | `error` | Error message | `{"message": "Connection timeout", "code": "TIMEOUT", "details": "..."}` | ✅ Yes |
| `audio` | Audio content | `{"url": "...", "format": "mp3", "duration": 120.5, "transcript": "...", "controls": true}` | ✅ Yes | | `image` | Image content | `{"url": "...", "alt": "...", "width": 200, "height": 200, "detail": "auto"}` | ✅ Yes |
| `video` | Video content | `{"url": "...", "format": "mp4", "thumbnail": "...", "width": 640, "height": 360}` | ✅ Yes | | `audio` | Audio content | `{"url": "...", "format": "mp3", "duration": 120.5, "transcript": "...", "controls": true}` | ✅ Yes |
| `action` | System action (CUI only) | `{"name": "open_panel", "payload": {"panel_id": "user_profile"}}` | ✅ Yes | | `video` | Video content | `{"url": "...", "format": "mp4", "thumbnail": "...", "width": 640, "height": 360}` | ✅ Yes |
| `event` | Lifecycle event (CUI only) | `{"event": "stream_start", "message": "...", "data": {...}}` | ⚠️ Optional | | `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 typically transient and may not need persistent storage. Consider storing only significant events or skipping entirely based on use case. **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 Call Storage:**
@ -272,32 +277,183 @@ User input with multimodal content (text + images + files) is stored as `user_in
} }
``` ```
### 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:**
```json
{
"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](../../kb/README.md) for details.
**Example: KB Search in Create Hook:**
```typescript
// 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:**
```json
{
"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 ### 3. Resume Table
Stores execution state for resume/retry functionality. **Only written when request is interrupted or failed.** Stores execution state for resume/retry functionality. **Only written when request is interrupted or failed.**
**Table Name:** `agent_resume` **Table Name:** `agent_resume`
| Column | Type | Nullable | Index | Description | | Column | Type | Nullable | Index | Description |
| ----------------- | ----------- | -------- | ------ | -------------------------------- | | ----------------- | ----------- | -------- | ------ | ---------------------------------------- |
| `id` | ID | No | PK | Auto-increment primary key | | `id` | ID | No | PK | Auto-increment primary key |
| `resume_id` | string(64) | No | Unique | Unique resume record identifier | | `resume_id` | string(64) | No | Unique | Unique resume record identifier |
| `chat_id` | string(64) | No | Yes | Parent chat ID | | `chat_id` | string(64) | No | Yes | Parent chat ID |
| `request_id` | string(64) | No | Yes | Request ID | | `request_id` | string(64) | No | Yes | Request ID |
| `assistant_id` | string(200) | No | Yes | Assistant executing this step | | `assistant_id` | string(200) | No | Yes | Assistant executing this step |
| `stack_id` | string(64) | No | Yes | Stack node ID for this execution | | `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_parent_id` | string(64) | Yes | Yes | Parent stack ID (for A2A calls) |
| `stack_depth` | integer | No | - | Call depth (0=root, 1+=nested) | | `stack_depth` | integer | No | - | Call depth (0=root, 1+=nested) |
| `type` | enum | No | Yes | Step type | | `type` | enum | No | Yes | Step type |
| `status` | enum | No | Yes | Status: `interrupted`, `failed` | | `status` | enum | No | Yes | Status: `interrupted`, `failed` |
| `input` | json | Yes | - | Step input data | | `input` | json | Yes | - | Step input data |
| `output` | json | Yes | - | Step output data (partial) | | `output` | json | Yes | - | Step output data (partial) |
| `space_snapshot` | json | Yes | - | Space data snapshot for recovery | | `space_snapshot` | json | Yes | - | Space data snapshot for recovery |
| `error` | text | Yes | - | Error message if failed | | `error` | text | Yes | - | Error message if failed |
| `sequence` | integer | No | Yes | Step order within request | | `sequence` | integer | No | - | Step order within request (in composite) |
| `metadata` | json | Yes | - | Additional metadata | | `metadata` | json | Yes | - | Additional metadata |
| `created_at` | timestamp | No | Yes | Creation timestamp | | `created_at` | timestamp | No | Yes | Creation timestamp |
| `updated_at` | timestamp | No | - | Last update timestamp | | `updated_at` | timestamp | No | - | Last update timestamp |
**Space Snapshot:** **Space Snapshot:**
@ -344,6 +500,7 @@ If interrupted during delegate, the `space_snapshot` allows restoring `ctx.Space
| ---------------------- | ------------------------ | ----- | | ---------------------- | ------------------------ | ----- |
| `idx_resume_chat` | `chat_id` | index | | `idx_resume_chat` | `chat_id` | index |
| `idx_resume_request` | `request_id`, `sequence` | index | | `idx_resume_request` | `request_id`, `sequence` | index |
| `idx_resume_type` | `type` | index |
| `idx_resume_status` | `status` | index | | `idx_resume_status` | `status` | index |
| `idx_resume_stack` | `stack_id` | index | | `idx_resume_stack` | `stack_id` | index |
| `idx_resume_parent` | `stack_parent_id` | index | | `idx_resume_parent` | `stack_parent_id` | index |
@ -351,20 +508,16 @@ If interrupted during delegate, the `space_snapshot` allows restoring `ctx.Space
## Write Strategy ## Write Strategy
### Two-Write Strategy ### Single-Write Strategy
All data is buffered in memory during execution and written to database only **twice**: All data is buffered in memory during execution and written to database **only once** when `Stream()` exits:
1. **Write 1 (Entry)**: When `Stream()` starts - save user input message
2. **Write 2 (Exit)**: When `Stream()` exits - batch save messages (and steps only on error/interrupt)
**Note**: Request tracking (status, tokens, duration) is handled by [OpenAPI Request Middleware](../../openapi/request/REQUEST_DESIGN.md). **Note**: Request tracking (status, tokens, duration) is handled by [OpenAPI Request Middleware](../../openapi/request/REQUEST_DESIGN.md).
``` ```
Stream() Entry Stream() Entry
├── 【Write 1】Save user input ├── Buffer user input message (role=user)
│ - User message (role=user)
├── Execution (all in memory) ├── Execution (all in memory)
│ - ctx.Send() → messageBuffer │ - ctx.Send() → messageBuffer
@ -372,10 +525,10 @@ Stream() Entry
│ - ctx.Replace() → update messageBuffer │ - ctx.Replace() → update messageBuffer
│ - Each step → stepBuffer │ - Each step → stepBuffer
└── 【Write 2】Save final state (via defer) └── 【Single Write】Save final state (via defer)
├── Always: ├── Always:
│ - Batch write all assistant messages │ - Batch write all messages (user input + assistant responses)
│ - Update token usage in openapi_request (via request_id) │ - Update token usage in openapi_request (via request_id)
└── Only on error/interrupt: └── Only on error/interrupt:
@ -384,59 +537,59 @@ Stream() Entry
### Write Points ### Write Points
| Event | Message Table | Step Table | Token Usage | | Event | Message Table | Step Table | Token Usage |
| ---------------- | -------------------- | ----------------------------------- | ----------- | | ---------------- | -------------------------------------- | ----------------------------------- | ----------- |
| Stream entry | Write 1 (user input) | - | - | | Stream entry | Buffer user input | - | - |
| During execution | Buffer in memory | Buffer in memory | - | | During execution | Buffer in memory | Buffer in memory | - |
| **Completed** | **Batch write all** | **❌ Skip (no need to resume)** | ✅ Update | | **Completed** | **Batch write all (user + assistant)** | **❌ Skip (no need to resume)** | ✅ Update |
| On interrupt | Batch write buffered | ✅ Batch write (status=interrupted) | ✅ Update | | On interrupt | Batch write all buffered | ✅ Batch write (status=interrupted) | ✅ Update |
| On error | Batch write buffered | ✅ Batch write (status=failed) | ✅ Update | | On error | Batch write all buffered | ✅ Batch write (status=failed) | ✅ Update |
**Why skip Steps on success?** **Why skip Steps on success?**
- Steps are only needed for resume/retry operations - Steps are only needed for resume/retry operations
- If completed successfully, there's nothing to resume - If completed successfully, there's nothing to resume
- Reduces database writes and keeps Step table clean - Reduces database writes and keeps Resume table clean
### Why Two Writes? ### Why Single Write?
| Scenario | What Happens | Data Safe? | | Scenario | What Happens | Data Safe? |
| ------------------ | ----------------------------------- | ---------- | | ------------------ | --------------------------------- | ---------- |
| Normal completion | `defer` triggers → Write 2 executes | ✅ | | Normal completion | `defer` triggers → Write executes | ✅ |
| User clicks stop | `defer` triggers → Write 2 executes | ✅ | | User clicks stop | `defer` triggers → Write executes | ✅ |
| LLM timeout | `defer` triggers → Write 2 executes | ✅ | | LLM timeout | `defer` triggers → Write executes | ✅ |
| Tool failure | `defer` triggers → Write 2 executes | ✅ | | Tool failure | `defer` triggers → Write executes | ✅ |
| Network disconnect | `defer` triggers → Write 2 executes | ✅ | | Network disconnect | `defer` triggers → Write executes | ✅ |
| Process crash | Service is down, user must retry | N/A | | Process crash | Service is down, user must retry | N/A |
**Note**: Process crash is a catastrophic failure handled at infrastructure level, not application level. **Note**: Process crash is a catastrophic failure handled at infrastructure level, not application level.
### Write Count Comparison ### Write Count Comparison
For a typical request: user input → hook_create → llm → tool → llm → hook_next → 5 messages For a typical request: user input → hook_create → llm → tool → hook_next → 5 messages
| Strategy | Database Writes | Notes | | Strategy | Database Writes | Notes |
| ---------------------- | --------------- | ------------------ | | ------------------------- | --------------- | --------------------- |
| Write per operation | 1 + 5 + 5 = 11 | One write per step | | Write per operation | 1 + 5 + 5 = 11 | One write per step |
| **Two-write strategy** | **2** | Entry + Exit only | | **Single-write strategy** | **1** | Exit only (via defer) |
### Implementation ### Implementation
````go ````go
func (ast *Assistant) Stream(ctx, inputMessages, options) { func (ast *Assistant) Stream(ctx, inputMessages, options) {
// ========== Write 1: Entry ==========
userMsg := createUserMessage(ctx, inputMessages)
chatStore.SaveMessages(ctx.ChatID, []*Message{userMsg})
// ========== Memory Buffers ========== // ========== Memory Buffers ==========
messageBuffer := NewMessageBuffer() messageBuffer := NewMessageBuffer()
stepBuffer := NewStepBuffer() stepBuffer := NewStepBuffer()
// Buffer user input message (not written yet)
userMsg := createUserMessage(ctx, inputMessages)
messageBuffer.Add(userMsg)
// Track current step for error handling // Track current step for error handling
var currentStep *Step var currentStep *Step
defer func() { defer func() {
// ========== Write 2: Exit (always executes) ========== // ========== Single Write: Exit (always executes) ==========
// Determine final status for incomplete steps // Determine final status for incomplete steps
finalStatus := "completed" finalStatus := "completed"
if ctx.IsInterrupted() { if ctx.IsInterrupted() {
@ -451,9 +604,13 @@ func (ast *Assistant) Stream(ctx, inputMessages, options) {
currentStep.Status = finalStatus currentStep.Status = finalStatus
} }
// Batch write all buffered data // Batch write all buffered messages (user input + assistant responses)
chatStore.SaveMessages(ctx.ChatID, messageBuffer.GetAll()) chatStore.SaveMessages(ctx.ChatID, messageBuffer.GetAll())
chatStore.SaveSteps(stepBuffer.GetAll())
// Only save steps on error/interrupt (not on success)
if finalStatus != "completed" {
chatStore.SaveResume(stepBuffer.GetAll())
}
// Update token usage in OpenAPI request record // Update token usage in OpenAPI request record
if ctx.RequestID != "" && completionResponse != nil { if ctx.RequestID != "" && completionResponse != nil {
@ -580,7 +737,6 @@ type Chat struct {
AssistantID string `json:"assistant_id"` AssistantID string `json:"assistant_id"`
Mode string `json:"mode"` Mode string `json:"mode"`
Status string `json:"status"` Status string `json:"status"`
Preset bool `json:"preset"`
Public bool `json:"public"` Public bool `json:"public"`
Share string `json:"share"` // "private" or "team" Share string `json:"share"` // "private" or "team"
Sort int `json:"sort"` Sort int `json:"sort"`
@ -639,6 +795,20 @@ type ChatFilter struct {
AssistantID string `json:"assistant_id,omitempty"` AssistantID string `json:"assistant_id,omitempty"`
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
Keywords string `json:"keywords,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"` Page int `json:"page,omitempty"`
PageSize int `json:"pagesize,omitempty"` PageSize int `json:"pagesize,omitempty"`
} }
@ -648,17 +818,28 @@ type MessageFilter struct {
RequestID string `json:"request_id,omitempty"` RequestID string `json:"request_id,omitempty"`
Role string `json:"role,omitempty"` Role string `json:"role,omitempty"`
BlockID string `json:"block_id,omitempty"` BlockID string `json:"block_id,omitempty"`
ThreadID string `json:"thread_id,omitempty"`
Type string `json:"type,omitempty"`
Limit int `json:"limit,omitempty"` Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"` Offset int `json:"offset,omitempty"`
} }
// ChatList paginated response // ChatList paginated response with time-based grouping
type ChatList struct { type ChatList struct {
Data []*Chat `json:"data"` Data []*Chat `json:"data"`
Page int `json:"page"` Groups []*ChatGroup `json:"groups,omitempty"` // Time-based groups for UI display
PageSize int `json:"pagesize"` Page int `json:"page"`
PageCount int `json:"pagecount"` PageSize int `json:"pagesize"`
Total int `json:"total"` 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
} }
``` ```
@ -671,13 +852,12 @@ A typical conversation with various message types stored in `agent_message`:
``` ```
User: "What's the weather in SF? Also show me a chart." User: "What's the weather in SF? Also show me a chart."
Timeline: Timeline (user input → hook_create → llm → tool → hook_next):
1. User sends multimodal input 1. User sends input
2. Hook shows loading state 2. Create hook shows loading state
3. LLM thinks and calls tool 3. LLM thinks and calls tool
4. Tool returns result 4. Tool executes and returns result
5. LLM generates text response 5. Next hook generates text response and image chart
6. Hook sends image chart
``` ```
**Stored Messages:** **Stored Messages:**
@ -745,7 +925,7 @@ Timeline:
"sequence": 4 "sequence": 4
}, },
// 5. Tool result (role=assistant, type=text, with tool metadata) // 5. Tool result from Next hook (role=assistant, type=text, with tool metadata)
{ {
"message_id": "msg_005", "message_id": "msg_005",
"chat_id": "chat_123", "chat_id": "chat_123",
@ -753,39 +933,23 @@ Timeline:
"role": "assistant", "role": "assistant",
"type": "text", "type": "text",
"props": { "props": {
"content": "Weather data retrieved: 18°C, sunny, humidity 65%" "content": "The weather in San Francisco is currently **18°C** and sunny with 65% humidity. Perfect weather for outdoor activities!"
}, },
"block_id": "B2", "block_id": "B3",
"metadata": { "metadata": {
"tool_call_id": "call_weather_001", "tool_call_id": "call_weather_001",
"tool_name": "get_weather", "tool_name": "get_weather"
"is_tool_result": true
}, },
"assistant_id": "weather_assistant", "assistant_id": "weather_assistant",
"sequence": 5 "sequence": 5
}, },
// 6. LLM text response (role=assistant, type=text) // 6. Chart image from Next hook (role=assistant, type=image)
{ {
"message_id": "msg_006", "message_id": "msg_006",
"chat_id": "chat_123", "chat_id": "chat_123",
"request_id": "req_abc", "request_id": "req_abc",
"role": "assistant", "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": "B2",
"assistant_id": "weather_assistant",
"sequence": 6
},
// 7. Chart image from Next hook (role=assistant, type=image)
{
"message_id": "msg_007",
"chat_id": "chat_123",
"request_id": "req_abc",
"role": "assistant",
"type": "image", "type": "image",
"props": { "props": {
"url": "https://charts.example.com/weather_sf.png", "url": "https://charts.example.com/weather_sf.png",
@ -795,7 +959,7 @@ Timeline:
}, },
"block_id": "B3", "block_id": "B3",
"assistant_id": "weather_assistant", "assistant_id": "weather_assistant",
"sequence": 7 "sequence": 6
} }
] ]
``` ```
@ -904,13 +1068,54 @@ Multimedia content storage:
### 5. Load Chat History ### 5. Load Chat History
```go ```go
// Get chat list // Example 1: Flat list (default)
chats, _ := chatStore.ListChats(ChatFilter{ chats, _ := chatStore.ListChats(ChatFilter{
UserID: "user123", UserID: "user123",
Status: "active", Status: "active",
OrderBy: "last_message_at",
Order: "desc",
Page: 1, Page: 1,
PageSize: 20, PageSize: 20,
}) })
// Response: chats.Data = [...], chats.Groups = nil
// Example 2: 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 3: 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 4: 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
})
// Get messages for a chat // Get messages for a chat
messages, _ := chatStore.GetMessages("chat_123", MessageFilter{ messages, _ := chatStore.GetMessages("chat_123", MessageFilter{
@ -1054,6 +1259,99 @@ return {
// 2. The delegated agent's Create hook can read: ctx.space.GetDel("choose_prompt") // 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
## Related Documents ## Related Documents
- [OpenAPI Request Design](../../openapi/request/REQUEST_DESIGN.md) - Global request tracking, billing, rate limiting - [OpenAPI Request Design](../../openapi/request/REQUEST_DESIGN.md) - Global request tracking, billing, rate limiting

File diff suppressed because it is too large Load diff

View file

@ -22,7 +22,8 @@ import (
var systemModels = map[string]string{ var systemModels = map[string]string{
"__yao.agent.assistant": "yao/models/agent/assistant.mod.yao", "__yao.agent.assistant": "yao/models/agent/assistant.mod.yao",
"__yao.agent.chat": "yao/models/agent/chat.mod.yao", "__yao.agent.chat": "yao/models/agent/chat.mod.yao",
"__yao.agent.history": "yao/models/agent/history.mod.yao", "__yao.agent.message": "yao/models/agent/message.mod.yao",
"__yao.agent.resume": "yao/models/agent/resume.mod.yao",
"__yao.attachment": "yao/models/attachment.mod.yao", "__yao.attachment": "yao/models/attachment.mod.yao",
"__yao.audit": "yao/models/audit.mod.yao", "__yao.audit": "yao/models/audit.mod.yao",
"__yao.config": "yao/models/config.mod.yao", "__yao.config": "yao/models/config.mod.yao",

View file

@ -197,7 +197,8 @@ var testServer *http.Server = nil
var testSystemModels = map[string]string{ var testSystemModels = map[string]string{
"__yao.agent.assistant": "yao/models/agent/assistant.mod.yao", "__yao.agent.assistant": "yao/models/agent/assistant.mod.yao",
"__yao.agent.chat": "yao/models/agent/chat.mod.yao", "__yao.agent.chat": "yao/models/agent/chat.mod.yao",
"__yao.agent.history": "yao/models/agent/history.mod.yao", "__yao.agent.message": "yao/models/agent/message.mod.yao",
"__yao.agent.resume": "yao/models/agent/resume.mod.yao",
"__yao.attachment": "yao/models/attachment.mod.yao", "__yao.attachment": "yao/models/attachment.mod.yao",
"__yao.audit": "yao/models/audit.mod.yao", "__yao.audit": "yao/models/audit.mod.yao",
"__yao.config": "yao/models/config.mod.yao", "__yao.config": "yao/models/config.mod.yao",

View file

@ -1,53 +1,103 @@
{ {
"name": "Chat", "name": "Chat",
"label": "Chat", "label": "Chat",
"description": "Chat table for storing chat metadata and information", "description": "Chat session table for storing chat metadata and session information",
"tags": ["agent", "system"], "tags": ["agent", "system"],
"builtin": true, "builtin": true,
"readonly": true, "readonly": true,
"sort": 9999, "sort": 9999,
"table": { "name": "agent_chat", "comment": "Agent chat table" }, "table": { "name": "agent_chat", "comment": "Agent chat session table" },
"columns": [ "columns": [
{ {
"name": "id", "name": "id",
"type": "ID", "type": "ID",
"label": "Chat ID", "label": "ID",
"comment": "Unique chat identifier" "comment": "Auto-increment primary key"
}, },
{ {
"name": "chat_id", "name": "chat_id",
"type": "string", "type": "string",
"label": "Chat ID", "label": "Chat ID",
"comment": "Chat identifier", "comment": "Unique chat identifier",
"length": 200, "length": 64,
"nullable": false, "nullable": false,
"unique": true, "unique": true
"index": true
}, },
{ {
"name": "title", "name": "title",
"type": "string", "type": "string",
"label": "Title", "label": "Title",
"comment": "Chat title", "comment": "Chat title",
"length": 200, "length": 500,
"nullable": true "nullable": true
}, },
{ {
"name": "assistant_id", "name": "assistant_id",
"type": "string", "type": "string",
"label": "Assistant ID", "label": "Assistant ID",
"comment": "Assistant identifier", "comment": "Associated assistant ID",
"length": 200, "length": 200,
"nullable": false,
"index": true
},
{
"name": "mode",
"type": "string",
"label": "Mode",
"comment": "Chat mode (default: chat)",
"length": 50,
"nullable": false,
"default": "chat"
},
{
"name": "status",
"type": "enum",
"label": "Status",
"comment": "Chat status",
"option": ["active", "archived"],
"default": "active",
"nullable": false,
"index": true
},
{
"name": "public",
"type": "boolean",
"label": "Public",
"comment": "Whether shared across all teams",
"default": false,
"nullable": false
},
{
"name": "share",
"type": "enum",
"label": "Share",
"comment": "Sharing scope",
"option": ["private", "team"],
"default": "private",
"nullable": false,
"index": true
},
{
"name": "sort",
"type": "integer",
"label": "Sort",
"comment": "Sort order for display",
"default": 9999
},
{
"name": "last_message_at",
"type": "datetime",
"label": "Last Message At",
"comment": "Timestamp of last message",
"nullable": true, "nullable": true,
"index": true "index": true
}, },
{ {
"name": "silent", "name": "metadata",
"type": "boolean", "type": "json",
"label": "Silent Mode", "label": "Metadata",
"comment": "Whether this is a silent chat", "comment": "Additional metadata",
"default": false, "nullable": true
"index": true
} }
], ],
"relations": { "relations": {
@ -57,20 +107,19 @@
"key": "assistant_id", "key": "assistant_id",
"foreign": "assistant_id" "foreign": "assistant_id"
}, },
"history": { "messages": {
"type": "hasMany", "type": "hasMany",
"model": "__yao.agent.history", "model": "__yao.agent.message",
"key": "chat_id", "key": "chat_id",
"foreign": "cid" "foreign": "chat_id"
},
"resumes": {
"type": "hasMany",
"model": "__yao.agent.resume",
"key": "chat_id",
"foreign": "chat_id"
} }
}, },
"indexes": [ "option": { "timestamps": true, "soft_deletes": true, "permission": true }
{
"name": "idx_agent_chat_silent",
"columns": ["silent", "created_at"],
"type": "index",
"comment": "Index for silent mode filtering"
}
],
"option": { "timestamps": true, "soft_deletes": false, "permission": true }
} }

View file

@ -1,126 +0,0 @@
{
"name": "History",
"label": "Chat History",
"description": "Chat history table for storing detailed chat message information",
"tags": ["agent", "system"],
"builtin": true,
"readonly": true,
"sort": 9999,
"table": { "name": "agent_history", "comment": "Agent chat history table" },
"columns": [
{
"name": "id",
"type": "ID",
"label": "Record ID",
"comment": "Unique record identifier"
},
{
"name": "chat_id",
"type": "string",
"label": "Chat ID",
"comment": "Chat identifier",
"length": 200,
"nullable": true,
"index": true
},
{
"name": "name",
"type": "string",
"label": "Role Name",
"comment": "Role display name",
"length": 200,
"nullable": true
},
{
"name": "content",
"type": "text",
"label": "Message Content",
"comment": "Text content of the message",
"nullable": true
},
{
"name": "context",
"type": "json",
"label": "Context",
"comment": "Message context data",
"nullable": true
},
{
"name": "assistant_id",
"type": "string",
"label": "Assistant ID",
"comment": "Assistant identifier",
"length": 200,
"nullable": true,
"index": true
},
{
"name": "assistant_name",
"type": "string",
"label": "Assistant Name",
"comment": "Assistant display name",
"length": 200,
"nullable": true
},
{
"name": "assistant_avatar",
"type": "string",
"label": "Assistant Avatar",
"comment": "Assistant avatar URL",
"length": 200,
"nullable": true
},
{
"name": "mentions",
"type": "json",
"label": "Mentions",
"comment": "Mention information in the message",
"nullable": true
},
{
"name": "silent",
"type": "boolean",
"label": "Silent Mode",
"comment": "Whether this is a silent message",
"default": false,
"index": true
},
{
"name": "expired_at",
"type": "timestamp",
"label": "Expired At",
"comment": "Record expiration time",
"nullable": true,
"index": true
}
],
"relations": {
"chat": {
"type": "hasOne",
"model": "__yao.agent.chat",
"key": "chat_id",
"foreign": "chat_id"
},
"assistant": {
"type": "hasOne",
"model": "__yao.agent.assistant",
"key": "assistant_id",
"foreign": "assistant_id"
},
"user": {
"type": "hasOne",
"model": "__yao.user",
"key": "user_id",
"foreign": "user_id"
}
},
"indexes": [
{
"name": "idx_agent_history_expired",
"columns": ["expired_at", "silent"],
"type": "index",
"comment": "Index for expiration and cleanup"
}
],
"option": { "timestamps": true, "soft_deletes": false, "permission": true }
}

View file

@ -0,0 +1,134 @@
{
"name": "Message",
"label": "Message",
"description": "Chat message table for storing user-visible messages",
"tags": ["agent", "system"],
"builtin": true,
"readonly": true,
"sort": 9999,
"table": { "name": "agent_message", "comment": "Agent chat message table" },
"columns": [
{
"name": "id",
"type": "ID",
"label": "ID",
"comment": "Auto-increment primary key"
},
{
"name": "message_id",
"type": "string",
"label": "Message ID",
"comment": "Unique message identifier",
"length": 64,
"nullable": false,
"unique": true
},
{
"name": "chat_id",
"type": "string",
"label": "Chat ID",
"comment": "Parent chat ID",
"length": 64,
"nullable": false,
"index": true
},
{
"name": "request_id",
"type": "string",
"label": "Request ID",
"comment": "Request ID for grouping",
"length": 64,
"nullable": true,
"index": true
},
{
"name": "role",
"type": "enum",
"label": "Role",
"comment": "Message role",
"option": ["user", "assistant"],
"nullable": false,
"index": true
},
{
"name": "type",
"type": "string",
"label": "Type",
"comment": "Message type (text, image, loading, tool_call, retrieval, etc.)",
"length": 50,
"nullable": false
},
{
"name": "props",
"type": "json",
"label": "Props",
"comment": "Message properties (content, url, etc.)",
"nullable": false
},
{
"name": "block_id",
"type": "string",
"label": "Block ID",
"comment": "Block grouping ID",
"length": 64,
"nullable": true,
"index": true
},
{
"name": "thread_id",
"type": "string",
"label": "Thread ID",
"comment": "Thread grouping ID for concurrent operations",
"length": 64,
"nullable": true,
"index": true
},
{
"name": "assistant_id",
"type": "string",
"label": "Assistant ID",
"comment": "Assistant ID (join to get name/avatar)",
"length": 200,
"nullable": true,
"index": true
},
{
"name": "sequence",
"type": "integer",
"label": "Sequence",
"comment": "Message order within chat",
"nullable": false
},
{
"name": "metadata",
"type": "json",
"label": "Metadata",
"comment": "Additional metadata (tool_call_id, tool_name, etc.)",
"nullable": true
}
],
"relations": {
"chat": {
"type": "hasOne",
"model": "__yao.agent.chat",
"key": "chat_id",
"foreign": "chat_id"
},
"assistant": {
"type": "hasOne",
"model": "__yao.agent.assistant",
"key": "assistant_id",
"foreign": "assistant_id"
}
},
"indexes": [
{
"name": "idx_msg_chat_seq",
"columns": ["chat_id", "sequence"],
"type": "index",
"comment": "Index for message ordering within chat"
}
],
"option": { "timestamps": true, "soft_deletes": false }
}

View file

@ -0,0 +1,170 @@
{
"name": "Resume",
"label": "Resume",
"description": "Resume table for storing execution state for resume/retry functionality",
"tags": ["agent", "system"],
"builtin": true,
"readonly": true,
"sort": 9999,
"table": { "name": "agent_resume", "comment": "Agent resume/recovery table" },
"columns": [
{
"name": "id",
"type": "ID",
"label": "ID",
"comment": "Auto-increment primary key"
},
{
"name": "resume_id",
"type": "string",
"label": "Resume ID",
"comment": "Unique resume record identifier",
"length": 64,
"nullable": false,
"unique": true
},
{
"name": "chat_id",
"type": "string",
"label": "Chat ID",
"comment": "Parent chat ID",
"length": 64,
"nullable": false,
"index": true
},
{
"name": "request_id",
"type": "string",
"label": "Request ID",
"comment": "Request ID",
"length": 64,
"nullable": false,
"index": true
},
{
"name": "assistant_id",
"type": "string",
"label": "Assistant ID",
"comment": "Assistant executing this step",
"length": 200,
"nullable": false,
"index": true
},
{
"name": "stack_id",
"type": "string",
"label": "Stack ID",
"comment": "Stack node ID for this execution",
"length": 64,
"nullable": false,
"index": true
},
{
"name": "stack_parent_id",
"type": "string",
"label": "Stack Parent ID",
"comment": "Parent stack ID (for A2A calls)",
"length": 64,
"nullable": true,
"index": true
},
{
"name": "stack_depth",
"type": "integer",
"label": "Stack Depth",
"comment": "Call depth (0=root, 1+=nested)",
"nullable": false,
"default": 0
},
{
"name": "type",
"type": "enum",
"label": "Type",
"comment": "Step type",
"option": [
"input",
"hook_create",
"llm",
"tool",
"hook_next",
"delegate"
],
"nullable": false,
"index": true
},
{
"name": "status",
"type": "enum",
"label": "Status",
"comment": "Resume status (only interrupted or failed are stored)",
"option": ["interrupted", "failed"],
"nullable": false,
"index": true
},
{
"name": "input",
"type": "json",
"label": "Input",
"comment": "Step input data",
"nullable": true
},
{
"name": "output",
"type": "json",
"label": "Output",
"comment": "Step output data (partial)",
"nullable": true
},
{
"name": "space_snapshot",
"type": "json",
"label": "Space Snapshot",
"comment": "Space data snapshot for recovery",
"nullable": true
},
{
"name": "error",
"type": "text",
"label": "Error",
"comment": "Error message if failed",
"nullable": true
},
{
"name": "sequence",
"type": "integer",
"label": "Sequence",
"comment": "Step order within request",
"nullable": false
},
{
"name": "metadata",
"type": "json",
"label": "Metadata",
"comment": "Additional metadata",
"nullable": true
}
],
"relations": {
"chat": {
"type": "hasOne",
"model": "__yao.agent.chat",
"key": "chat_id",
"foreign": "chat_id"
},
"assistant": {
"type": "hasOne",
"model": "__yao.agent.assistant",
"key": "assistant_id",
"foreign": "assistant_id"
}
},
"indexes": [
{
"name": "idx_resume_request_seq",
"columns": ["request_id", "sequence"],
"type": "index",
"comment": "Index for resume ordering within request"
}
],
"option": { "timestamps": true, "soft_deletes": false }
}