Update DESIGN.md, TECHNICAL.md, and TODO.md for P4 Delivery Implementation

- Revised DESIGN.md to clarify the architecture of the P4 delivery process, emphasizing the separation of content generation and channel decision-making.
- Expanded TECHNICAL.md with detailed notes on the DeliveryRequest structure, DeliveryContent generation, and the role of the Delivery Center in managing delivery channels.
- Updated TODO.md to reflect the completion of the Delivery Agent setup and the integration of delivery preferences, ensuring comprehensive tracking of the P4 implementation progress.
- Enhanced documentation to outline the new delivery channels and their configurations, including email and webhook options, for improved clarity and usability.
This commit is contained in:
Max 2026-01-18 11:55:33 +08:00
parent 603ed69a9e
commit 9e14d8b7af
3 changed files with 844 additions and 82 deletions

View file

@ -262,7 +262,7 @@ Human/Event: P1 → P2 → P3 → P4 → P5
| P1 | Goal Gen | Report + history | Goals | Always |
| P2 | Task Plan | Goals + tools | Tasks | Always |
| P3 | Run + Valid | Tasks + Experts | TaskResults | Always |
| P4 | Delivery | All results | Email/File | Always |
| P4 | Delivery | All results | Email/Webhook | Always |
| P5 | Learning | Summary | KB entries | Always |
### 4.2 P0: Inspiration (Clock only)
@ -316,7 +316,7 @@ type Goals struct {
}
type DeliveryTarget struct {
Type DeliveryType // email | webhook | report | notification
Type DeliveryType // Preferred delivery type (P4 will use Delivery Center)
Recipients []string // email addresses, webhook URLs, user IDs
Format string // markdown | html | json | text
Template string // template name
@ -489,13 +489,169 @@ Universal assertion library supporting 8 types:
### 4.6 P4: Deliver
Send output:
P4 generates delivery content and pushes to Delivery Center. **Agent only generates content, Delivery Center decides channels.**
**Architecture:**
```
┌─────────────────────────────────────────────────────────────┐
│ P4 Delivery Agent │
│ Role: Generate content only (Summary, Body, Attachments) │
│ NOT responsible for: Channel selection │
└─────────────────────┬───────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ DeliveryRequest │
│ - Content: Summary, Body, Attachments │
│ - Context: robot_id, exec_id, trigger_type, team_id │
│ (No Channels - Delivery Center decides) │
└─────────────────────┬───────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Delivery Center │
│ Role: │
│ 1. Read Robot/User delivery preferences │
│ 2. Decide which channels to use │
│ 3. Execute delivery (email, webhook) │
│ 4. Future: auto-notify based on user subscriptions │
│ │
│ (Current: internal, future: yao/delivery) │
└─────────────────────────────────────────────────────────────┘
```
**Key Design:**
- **Separation of concerns**: Agent generates content, Delivery Center handles channels
- **User preferences**: Channels decided by Robot/User configuration, not Agent
- **Automatic delivery**: If webhook configured, every execution pushes automatically
- **Future-ready**: Delivery Center can be extracted to `yao/delivery` package
**Delivery Request Structure:**
```go
// DeliveryRequest - pushed to Delivery Center
// No Channels field - Delivery Center decides based on preferences
type DeliveryRequest struct {
Content *DeliveryContent `json:"content"` // Agent-generated content
Context *DeliveryContext `json:"context"` // Tracking info
}
// DeliveryContent - content generated by Delivery Agent
type DeliveryContent struct {
Summary string `json:"summary"` // Brief 1-2 sentence summary
Body string `json:"body"` // Full markdown report
Attachments []DeliveryAttachment `json:"attachments,omitempty"` // Output artifacts
}
// DeliveryAttachment - task output attachment with metadata
type DeliveryAttachment struct {
Title string `json:"title"` // Human-readable title
Description string `json:"description,omitempty"` // What this artifact is
TaskID string `json:"task_id,omitempty"` // Which task produced this
File string `json:"file"` // Wrapper: __<uploader>://<fileID>
}
// DeliveryContext - tracking and audit info
type DeliveryContext struct {
RobotID string `json:"robot_id"`
ExecutionID string `json:"execution_id"`
TriggerType TriggerType `json:"trigger_type"`
TeamID string `json:"team_id"`
}
```
**File Wrapper Format:**
Attachments use the standard `yao/attachment` wrapper format:
- Format: `__<uploader>://<fileID>`
- Example: `__yao.attachment://ccd472d11feb96e03a3fc468f494045c`
- Parse: `attachment.Parse(value)``(uploader, fileID, isWrapper)`
- Read: `attachment.Base64(ctx, value)` → base64 content
**Delivery Channels (Delivery Center decides):**
| Channel | Description | When |
|---------|-------------|------|
| `email` | Send via yao/messenger | If configured in preferences |
| `webhook` | POST to URL (Slack, 飞书, etc.) | If configured, every execution |
| `notify` | In-app push notification | Based on user subscriptions (future) |
**Delivery Agent:**
The Delivery Agent **only generates content**, does NOT decide channels:
```go
// Delivery Agent Input
type DeliveryAgentInput struct {
Robot *Robot `json:"robot"`
TriggerType TriggerType `json:"trigger"`
Inspiration *InspirationReport `json:"inspiration"` // P0
Goals *Goals `json:"goals"` // P1
Tasks []Task `json:"tasks"` // P2
Results []TaskResult `json:"results"` // P3
}
// Delivery Agent Output - only content, no channels
type DeliveryAgentOutput struct {
Content *DeliveryContent `json:"content"`
}
```
**Example Agent Output:**
```json
{
"content": {
"summary": "Sales report completed: 15 new leads processed",
"body": "## Weekly Sales Report\n\n### Summary\n...",
"attachments": [
{"title": "Sales Report.pdf", "file": "__yao.attachment://abc123"},
{"title": "Lead Analysis.xlsx", "file": "__yao.attachment://def456"}
]
}
}
```
**Delivery Result:**
```go
// DeliveryResult - returned by Delivery Center
type DeliveryResult struct {
RequestID string `json:"request_id"` // Delivery request ID
Content *DeliveryContent `json:"content"` // What was delivered
Success bool `json:"success"` // All channels succeeded
Results []ChannelResult `json:"results"` // Per-channel results
Error string `json:"error,omitempty"`
}
// ChannelResult - result for a single channel
type ChannelResult struct {
Type DeliveryType `json:"type"`
Success bool `json:"success"`
Recipients []string `json:"recipients,omitempty"` // For email
Details interface{} `json:"details,omitempty"`
Error string `json:"error,omitempty"`
SentAt *time.Time `json:"sent_at,omitempty"`
}
```
**Config (Delivery Preferences):**
Robot config defines delivery **preferences** (Delivery Center reads and executes):
```yaml
delivery:
type: email # email | file | webhook | notify
opts:
to: ["manager@company.com"]
preferences:
email:
enabled: true
to: ["manager@company.com"]
cc: ["team@company.com"]
webhook:
enabled: true
url: "https://slack.com/webhook/reports"
# Every execution pushes to webhook automatically
# Note: notify handled by Delivery Center based on user subscriptions (future)
```
### 4.7 P5: Learn
@ -564,10 +720,9 @@ const (
type DeliveryType string
const (
DeliveryEmail DeliveryType = "email"
DeliveryFile DeliveryType = "file"
DeliveryWebhook DeliveryType = "webhook"
DeliveryNotify DeliveryType = "notify"
DeliveryEmail DeliveryType = "email" // Email via yao/messenger
DeliveryWebhook DeliveryType = "webhook" // POST to URL
DeliveryNotify DeliveryType = "notify" // In-app notification (future)
)
// ExecStatus - execution status enum

View file

@ -854,10 +854,9 @@ const (
type DeliveryType string
const (
DeliveryEmail DeliveryType = "email"
DeliveryFile DeliveryType = "file"
DeliveryWebhook DeliveryType = "webhook"
DeliveryNotify DeliveryType = "notify"
DeliveryEmail DeliveryType = "email" // Email via yao/messenger
DeliveryWebhook DeliveryType = "webhook" // POST to URL
DeliveryNotify DeliveryType = "notify" // In-app notification (future)
)
// DedupResult - deduplication result
@ -1309,9 +1308,11 @@ type Goals struct {
Delivery *DeliveryTarget `json:"delivery,omitempty"` // where to send results (for P4)
}
// DeliveryTarget - where to deliver results (defined in P1, used in P4)
// DeliveryTarget - where to deliver results (defined in P1, used by P4)
// Note: This is a hint from P1 Goals. Actual delivery is handled by Delivery Center
// based on Robot/User preferences, not strictly by this target.
type DeliveryTarget struct {
Type DeliveryType `json:"type"` // email | webhook | report | notification
Type DeliveryType `json:"type"` // Preferred delivery type
Recipients []string `json:"recipients,omitempty"` // email addresses, webhook URLs, user IDs
Format string `json:"format,omitempty"` // markdown | html | json | text
Template string `json:"template,omitempty"` // template name or inline template
@ -1399,13 +1400,74 @@ type ValidationResult struct {
ReplyContent string `json:"reply_content,omitempty"` // content for next turn (if NeedReply)
}
// DeliveryResult - P4 delivery output
// DeliveryRequest - pushed to Delivery Center
// Agent only generates content, Delivery Center decides channels based on preferences
type DeliveryRequest struct {
Content *DeliveryContent `json:"content"` // Agent-generated content
Context *DeliveryContext `json:"context"` // Tracking info
// No Channels field - Delivery Center decides based on Robot/User preferences
}
// DeliveryContent - content generated by Delivery Agent
type DeliveryContent struct {
Summary string `json:"summary"` // Brief summary (1-2 sentences)
Body string `json:"body"` // Full markdown report
Attachments []DeliveryAttachment `json:"attachments,omitempty"` // Output artifacts
}
// DeliveryAttachment - task output attachment with metadata
// File uses wrapper format: __<uploader>://<fileID>
// Example: __yao.attachment://ccd472d11feb96e03a3fc468f494045c
// Parse with attachment.Parse(value) → (uploader, fileID, isWrapper)
type DeliveryAttachment struct {
Title string `json:"title"` // Human-readable title, e.g., "Market Analysis Report"
Description string `json:"description,omitempty"` // Description of what this artifact is
TaskID string `json:"task_id,omitempty"` // Which task produced this artifact
File string `json:"file"` // Wrapper format: __<uploader>://<fileID>
}
// DeliveryContext - tracking and audit info
type DeliveryContext struct {
RobotID string `json:"robot_id"`
ExecutionID string `json:"execution_id"`
TriggerType TriggerType `json:"trigger_type"`
TeamID string `json:"team_id"`
}
// DeliveryPreferences - Robot/User delivery preferences (read by Delivery Center)
type DeliveryPreferences struct {
Email *EmailPreference `json:"email,omitempty"`
Webhook *WebhookPreference `json:"webhook,omitempty"`
// notify is handled automatically based on user subscriptions
}
type EmailPreference struct {
Enabled bool `json:"enabled"`
To []string `json:"to"`
CC []string `json:"cc,omitempty"`
}
type WebhookPreference struct {
Enabled bool `json:"enabled"`
URL string `json:"url"`
// If enabled, every execution pushes automatically
}
// DeliveryResult - P4 delivery output (returned by Delivery Center)
type DeliveryResult struct {
Type DeliveryType `json:"type"`
RequestID string `json:"request_id"` // Delivery request ID
Content *DeliveryContent `json:"content,omitempty"` // What was delivered
Success bool `json:"success"` // All channels succeeded
Results []ChannelResult `json:"results,omitempty"` // Per-channel results
Error string `json:"error,omitempty"` // Overall error if any
}
// ChannelResult - result for a single delivery channel
type ChannelResult struct {
Type DeliveryType `json:"type"` // email | webhook | notify
Success bool `json:"success"`
Recipients []string `json:"recipients,omitempty"` // who received
Content string `json:"content,omitempty"` // formatted content delivered
Details interface{} `json:"details,omitempty"` // channel-specific response
Recipients []string `json:"recipients,omitempty"` // Who received (for email)
Details interface{} `json:"details,omitempty"` // Channel-specific response
Error string `json:"error,omitempty"`
SentAt *time.Time `json:"sent_at,omitempty"`
}
@ -1893,3 +1955,407 @@ Supported assertion types:
- `type` - type checking (with optional path)
- `script` - custom script validation
- `agent` - AI agent validation
---
## 6. P4 Delivery Implementation
### 6.1 Overview
P4 Delivery summarizes P3 execution results and delivers to configured channels.
```
┌─────────────────────────────────────────────────────────────┐
│ delivery.go (P4 Entry) │
│ - DeliveryExecution: main entry point │
│ - Calls Delivery Agent with full execution context │
│ - Routes DeliveryContent to configured channels │
└─────────────────────┬───────────────────────────────────────┘
┌────────────┴────────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Delivery Agent │ │ Channel Handlers │
│ - Summarize │ │ - Email │
│ - Format body │ │ - Webhook │
│ - List files │ │ - Notify │
└─────────────────┘ │ - File │
└─────────────────┘
```
### 6.2 Delivery Request Structure
P4 generates a `DeliveryRequest` with **only content** and pushes to Delivery Center.
**Delivery Center decides channels** based on Robot/User preferences.
```go
// DeliveryRequest - pushed to Delivery Center
// No Channels - Delivery Center decides based on preferences
type DeliveryRequest struct {
Content *DeliveryContent `json:"content"` // Agent-generated content
Context *DeliveryContext `json:"context"` // Tracking info
}
// DeliveryContent - content generated by Delivery Agent
type DeliveryContent struct {
Summary string `json:"summary"` // Brief 1-2 sentence summary
Body string `json:"body"` // Full markdown report
Attachments []DeliveryAttachment `json:"attachments,omitempty"` // Output artifacts from P3
}
// DeliveryAttachment - file attachment with metadata
type DeliveryAttachment struct {
Title string `json:"title"` // Human-readable title
Description string `json:"description,omitempty"` // What this artifact is
TaskID string `json:"task_id,omitempty"` // Which task produced this
File string `json:"file"` // Wrapper: __<uploader>://<fileID>
}
// DeliveryContext - tracking and audit info
type DeliveryContext struct {
RobotID string `json:"robot_id"`
ExecutionID string `json:"execution_id"`
TriggerType TriggerType `json:"trigger_type"`
TeamID string `json:"team_id"`
}
```
**Example DeliveryRequest:**
```json
{
"content": {
"summary": "Sales report completed: 15 new leads",
"body": "## Weekly Sales Report\n...",
"attachments": [{"title": "Report.pdf", "file": "__yao.attachment://abc123"}]
},
"context": {
"robot_id": "mem_abc123",
"execution_id": "exec_xyz789",
"trigger_type": "clock",
"team_id": "team_123"
}
}
```
**Channel Decision by Delivery Center:**
Delivery Center reads Robot/User preferences and decides channels:
```go
// DeliveryPreferences - from Robot config
type DeliveryPreferences struct {
Email *EmailPreference `json:"email,omitempty"`
Webhook *WebhookPreference `json:"webhook,omitempty"`
}
type EmailPreference struct {
Enabled bool `json:"enabled"`
To []string `json:"to"`
CC []string `json:"cc,omitempty"`
}
type WebhookPreference struct {
Enabled bool `json:"enabled"`
URL string `json:"url"`
// If enabled, every execution pushes automatically
}
```
### 6.3 File Wrapper Format
Attachments use the standard `yao/attachment` wrapper format:
```go
// Format: __<uploader>://<fileID>
// Example: __yao.attachment://ccd472d11feb96e03a3fc468f494045c
import "github.com/yaoapp/yao/attachment"
// Parse wrapper to get uploader and fileID
uploader, fileID, isWrapper := attachment.Parse(wrapper)
// uploader: "__yao.attachment"
// fileID: "ccd472d11feb96e03a3fc468f494045c"
// isWrapper: true
// Get file info
manager := attachment.Managers[uploader]
fileInfo, err := manager.Info(ctx, fileID)
// Read file content as base64
base64Content := attachment.Base64(ctx, wrapper)
// Read with data URI format
dataURI := attachment.Base64(ctx, wrapper, true)
// "data:image/png;base64,..."
```
### 6.4 Delivery Agent
The Delivery Agent **only generates content**, does NOT decide channels.
Channel decisions are made by Delivery Center based on Robot/User preferences.
**Input:**
```go
type DeliveryAgentInput struct {
Robot *Robot `json:"robot"` // Robot identity and config
TriggerType TriggerType `json:"trigger"` // clock | human | event
Inspiration *InspirationReport `json:"inspiration"` // P0 (clock only)
Goals *Goals `json:"goals"` // P1
Tasks []Task `json:"tasks"` // P2
Results []TaskResult `json:"results"` // P3
}
```
**Output:**
```go
// DeliveryAgentOutput - only content, no channels
type DeliveryAgentOutput struct {
Content *DeliveryContent `json:"content"` // Generated content
}
```
**Agent Responsibilities:**
The agent focuses on content generation:
- **Summary**: Brief 1-2 sentence summary of execution results
- **Body**: Full markdown report with details
- **Attachments**: Select which P3-generated files to include
**Example Output:**
```json
{
"content": {
"summary": "Sales report completed: 15 new leads processed, 3 high-priority",
"body": "## Weekly Sales Report\n\n### Summary\n- Total leads: 15\n- High priority: 3\n...",
"attachments": [
{"title": "Sales Report.pdf", "task_id": "task_1", "file": "__yao.attachment://abc123"},
{"title": "Lead Analysis.xlsx", "task_id": "task_2", "file": "__yao.attachment://def456"}
]
}
}
```
### 6.5 Delivery Center
The Delivery Center receives `DeliveryRequest`, **decides channels based on preferences**, and executes delivery.
**Current implementation:** Internal to P4 (in `executor/delivery.go`)
**Future:** Can be extracted to standalone `yao/delivery` package
```go
// DeliveryCenter - handles channel decision and delivery execution
type DeliveryCenter struct {
handlers map[DeliveryType]ChannelHandler
}
// ChannelHandler - interface for channel implementations
type ChannelHandler interface {
Deliver(ctx context.Context, content *DeliveryContent, opts map[string]interface{}) (*ChannelResult, error)
}
// Deliver - main entry point
func (dc *DeliveryCenter) Deliver(ctx context.Context, req *DeliveryRequest) *DeliveryResult {
requestID := generateID()
// 1. Get Robot/User delivery preferences
prefs := dc.getDeliveryPreferences(ctx, req.Context.RobotID)
// 2. Decide channels based on preferences
channels := dc.decideChannels(prefs)
// 3. Execute delivery to each channel
var results []ChannelResult
allSuccess := true
for _, ch := range channels {
handler, ok := dc.handlers[ch.Type]
if !ok {
results = append(results, ChannelResult{
Type: ch.Type,
Success: false,
Error: fmt.Sprintf("unsupported channel: %s", ch.Type),
})
allSuccess = false
continue
}
result, err := handler.Deliver(ctx, req.Content, ch.Options)
if err != nil {
result = &ChannelResult{Type: ch.Type, Success: false, Error: err.Error()}
allSuccess = false
}
results = append(results, *result)
}
// 4. Future: check user subscriptions and send notifications
// dc.sendNotifications(ctx, req)
return &DeliveryResult{
RequestID: requestID,
Content: req.Content,
Success: allSuccess,
Results: results,
}
}
// decideChannels - decide which channels to use based on preferences
func (dc *DeliveryCenter) decideChannels(prefs *DeliveryPreferences) []channelWithOpts {
var channels []channelWithOpts
if prefs.Email != nil && prefs.Email.Enabled {
channels = append(channels, channelWithOpts{
Type: DeliveryEmail,
Options: map[string]interface{}{"to": prefs.Email.To, "cc": prefs.Email.CC},
})
}
if prefs.Webhook != nil && prefs.Webhook.Enabled {
channels = append(channels, channelWithOpts{
Type: DeliveryWebhook,
Options: map[string]interface{}{"url": prefs.Webhook.URL},
})
}
return channels
}
```
### 6.6 Channel Handlers
Each delivery channel has a dedicated handler implementing `ChannelHandler`:
```go
// EmailHandler - uses yao/messenger
type EmailHandler struct {
messenger *messenger.Manager
}
func (h *EmailHandler) Deliver(ctx context.Context, content *DeliveryContent, opts map[string]interface{}) (*ChannelResult, error) {
// Convert attachments to messenger format
var attachments []messenger.Attachment
for _, att := range content.Attachments {
uploader, fileID, _ := attachment.Parse(att.File)
manager := attachment.Managers[uploader]
data, _ := manager.Read(ctx, fileID)
info, _ := manager.Info(ctx, fileID)
attachments = append(attachments, messenger.Attachment{
Filename: att.Title,
ContentType: info.ContentType,
Content: data,
})
}
to := opts["to"].([]string)
err := h.messenger.Send(ctx, &messenger.Message{
To: to,
Subject: content.Summary, // Use summary as subject
Body: content.Body,
Attachments: attachments,
})
now := time.Now()
return &ChannelResult{
Type: DeliveryEmail,
Success: err == nil,
Recipients: to,
SentAt: &now,
}, err
}
// WebhookHandler - POST JSON to URL
type WebhookHandler struct{}
func (h *WebhookHandler) Deliver(ctx context.Context, content *DeliveryContent, opts map[string]interface{}) (*ChannelResult, error) {
payload, _ := json.Marshal(content)
req, _ := http.NewRequestWithContext(ctx, "POST", opts["url"].(string), bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return &ChannelResult{Type: DeliveryWebhook, Success: false}, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return &ChannelResult{Type: DeliveryWebhook, Success: false}, fmt.Errorf("webhook failed: %d", resp.StatusCode)
}
now := time.Now()
return &ChannelResult{
Type: DeliveryWebhook,
Success: true,
Details: map[string]interface{}{"status_code": resp.StatusCode},
SentAt: &now,
}, nil
}
```
**Note on Notifications:**
`notify` is NOT configured per-Robot. Future Delivery Center will:
1. Check user subscription preferences after receiving DeliveryRequest
2. Automatically send in-app notifications to subscribed users
3. This is transparent to P4 and Delivery Agent
### 6.7 Execution Persistence
Robot execution history is stored in `__yao.agent_execution` table for UI display:
```go
// Model: yao/models/agent/execution.mod.yao
// Table: __yao.agent_execution
type ExecutionRecord struct {
ID string `json:"id"` // Execution ID
MemberID string `json:"member_id"` // Robot member ID
TeamID string `json:"team_id"` // Team ID
JobID string `json:"job_id"` // Linked job.Job ID
TriggerType TriggerType `json:"trigger_type"` // clock | human | event
Status ExecStatus `json:"status"` // pending | running | completed | failed
Phase Phase `json:"phase"` // Current phase
Input *TriggerInput `json:"input"` // Original trigger input
Inspiration *InspirationReport `json:"inspiration"` // P0 result
Goals *Goals `json:"goals"` // P1 result
Tasks []Task `json:"tasks"` // P2 result
Results []TaskResult `json:"results"` // P3 results
Delivery *DeliveryResult `json:"delivery"` // P4 result
Learning []LearningEntry `json:"learning"` // P5 entries
Error string `json:"error"` // Error message if failed
StartTime time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
```
**Store Interface:**
```go
// store/execution.go
type ExecutionStore interface {
// Save creates or updates an execution record
Save(ctx context.Context, record *ExecutionRecord) error
// Get retrieves an execution by ID
Get(ctx context.Context, execID string) (*ExecutionRecord, error)
// List retrieves executions with filters
List(ctx context.Context, opts ListOptions) ([]*ExecutionRecord, int, error)
// UpdatePhase updates the current phase
UpdatePhase(ctx context.Context, execID string, phase Phase, data interface{}) error
}
type ListOptions struct {
MemberID string // Filter by robot
TeamID string // Filter by team
Status ExecStatus // Filter by status
TriggerType TriggerType // Filter by trigger
Page int
PageSize int
}
```

View file

@ -62,7 +62,7 @@
- [x] `RobotStatus` - robot status (idle, working, paused, error, maintenance)
- [x] `InterventionAction` - human actions (task.add, goal.adjust, etc.)
- [x] `Priority` - priority levels (high, normal, low)
- [x] `DeliveryType` - delivery types (email, file, webhook, notify)
- [x] `DeliveryType` - delivery types (email, webhook, notify)
- [x] `DedupResult` - dedup results (skip, merge, proceed)
- [x] `EventSource` - event sources (webhook, database)
- [x] `LearningType` - learning types (execution, feedback, insight)
@ -555,7 +555,7 @@ yao-dev-app/assistants/
- [x] `robot/delivery/package.yao` - config
- [x] `robot/delivery/prompts.yml` - system prompt:
- Input: Task results, delivery target (email, report, notification)
- Input: Full execution context (P0-P3 results)
- Output: Formatted delivery content
- Style: Clear, professional
@ -869,92 +869,231 @@ Created new `yao/assert` package for universal assertion/validation:
**Depends on:** Phase 9 (P3 Run)
### 10.1 Delivery Agent Setup
### 10.1 Execution Persistence (Prerequisite)
> **Background:** Each Robot execution (P0-P5) needs persistent storage for UI history queries.
- [ ] `yao/models/agent/execution.mod.yao` - Execution record model (`agent_execution` table)
- [ ] id, execution_id (unique)
- [ ] member_id, team_id, job_id
- [ ] trigger_type (enum: clock, human, event)
- [ ] status (enum: pending, running, completed, failed, cancelled)
- [ ] phase (enum: inspiration, goals, tasks, run, delivery, learning)
- [ ] start_time, end_time, error
- [ ] input (JSON) - trigger input
- [ ] inspiration (JSON) - P0 output
- [ ] goals (JSON) - P1 output
- [ ] tasks (JSON) - P2 output
- [ ] results (JSON) - P3 output
- [ ] delivery (JSON) - P4 output
- [ ] learning (JSON) - P5 output
- [ ] Relations: member (hasOne __yao.member)
- [ ] `agent/robot/store/execution.go` - Execution record storage
- [ ] `SaveExecution()` - save/update execution record
- [ ] `GetExecution()` - get execution by ID
- [ ] `ListExecutions()` - query execution history by member_id
- [ ] Integrate into Executor - update record after each phase
### 10.2 Messenger Attachment Support ✅
> **Conclusion:** `yao/messenger` already supports attachments
```go
// messenger/types/types.go
type Attachment struct {
Filename string `json:"filename"`
ContentType string `json:"content_type"`
Content []byte `json:"content"`
Inline bool `json:"inline,omitempty"`
CID string `json:"cid,omitempty"`
}
```
Supported channels:
- [x] Email - Full attachment support
- [x] SMS - No attachment (text only)
- [x] WhatsApp - TBD
### 10.3 Type Updates (Prerequisite)
- [ ] Update `types/enums.go` - Remove `DeliveryFile` from `DeliveryType`
- [ ] Update `types/robot.go` - Delivery types for new architecture
- [ ] `DeliveryResult` - update to new structure (RequestID, Content, Results[])
- [ ] Add `DeliveryContent` struct
- [ ] Add `DeliveryAttachment` struct
- [ ] Add `DeliveryRequest` struct
- [ ] Add `DeliveryContext` struct
- [ ] Add `DeliveryPreferences` struct
- [ ] Add `ChannelResult` struct
- [ ] Update `types/enums_test.go` - Remove `DeliveryFile` test
- [ ] Update `types/robot_test.go` - Update delivery result tests
### 10.4 Delivery Agent Setup
- [ ] `robot/delivery/package.yao` - Delivery Agent config
- [ ] `robot/delivery/prompts.yml` - delivery prompts
- [ ] Input: Full execution context (P0-P3 results)
- [ ] Output: DeliveryContent (Summary, Body, Attachments) - **only content, no channels**
- [ ] Agent focuses on content generation, NOT channel selection
### 10.2 Implementation
### 10.5 Delivery Content Structure
- [ ] `executor/delivery.go` - `RunDelivery(ctx, exec, data)` - real implementation
- [ ] `executor/delivery.go` - build delivery content from results
- [ ] `executor/delivery.go` - support email delivery
- [ ] `executor/delivery.go` - support file delivery
- [ ] `executor/delivery.go` - support webhook delivery
- [ ] `executor/delivery.go` - support notify delivery
```go
// DeliveryRequest - pushed to Delivery Center
// No Channels - Delivery Center decides based on preferences
type DeliveryRequest struct {
Content *DeliveryContent `json:"content"` // Agent-generated content
Context *DeliveryContext `json:"context"` // Tracking info
}
### 10.3 Tests
// DeliveryContent - Content generated by Delivery Agent (only content)
type DeliveryContent struct {
Summary string `json:"summary"` // Brief 1-2 sentence summary
Body string `json:"body"` // Full markdown report
Attachments []DeliveryAttachment `json:"attachments,omitempty"` // Output artifacts from P3
}
// DeliveryAttachment - Task output attachment with metadata
type DeliveryAttachment struct {
Title string `json:"title"` // Human-readable title
Description string `json:"description,omitempty"` // What this artifact is
TaskID string `json:"task_id,omitempty"` // Which task produced this
File string `json:"file"` // Wrapper: __<uploader>://<fileID>
}
// DeliveryContext - tracking info
type DeliveryContext struct {
RobotID string `json:"robot_id"`
ExecutionID string `json:"execution_id"`
TriggerType string `json:"trigger_type"`
TeamID string `json:"team_id"`
}
```
**Key Design:**
- **Agent only generates content** (Summary, Body, Attachments)
- **Delivery Center decides channels** based on Robot/User preferences
- If webhook configured, every execution pushes automatically
**File Wrapper:**
- Format: `__<uploader>://<fileID>`
- Parse: `attachment.Parse(value)``(uploader, fileID, isWrapper)`
- Read: `attachment.Base64(ctx, value)` → base64 content
**Delivery Channels (Delivery Center decides):**
| Channel | Description |
|---------|-------------|
| `email` | Send via yao/messenger (if configured in preferences) |
| `webhook` | POST JSON to URL (if configured, every execution) |
| `notify` | In-app notification based on user subscriptions (future) |
### 10.6 Implementation
**P4 Entry (executor/delivery.go):**
- [ ] `RunDelivery(ctx, exec, data)` - P4 entry point
- [ ] Call Delivery Agent to generate content (only content, no channels)
- [ ] Build DeliveryRequest (Content + Context)
- [ ] Push to Delivery Center
- [ ] Store DeliveryResult in exec.Delivery
**Delivery Center (executor/delivery.go, future: yao/delivery):**
- [ ] `DeliveryCenter.Deliver(ctx, request)` - main entry
- [ ] Read Robot/User delivery preferences
- [ ] Decide which channels to use based on preferences
- [ ] Call appropriate handler for each channel
- [ ] Aggregate ChannelResults into DeliveryResult
- [ ] `ChannelHandler` interface
- [ ] `Deliver(ctx, content, opts) (*ChannelResult, error)`
**Channel Handlers:**
- [ ] `EmailHandler` - uses yao/messenger
- [ ] Convert DeliveryAttachment to messenger.Attachment
- [ ] Use Summary as email subject
- [ ] Support to, cc from preferences
- [ ] `WebhookHandler` - POST JSON
- [ ] POST DeliveryContent as JSON payload
- [ ] If enabled, every execution pushes automatically
### 10.7 Tests
- [ ] `executor/delivery_test.go` - P4 delivery
- [ ] Test: delivery content generated correctly
- [ ] Test: email delivery (mock or real)
- [ ] Test: file delivery to configured path
- [ ] Test: Delivery Agent generates content (only content)
- [ ] Test: DeliveryCenter reads preferences and decides channels
- [ ] Test: DeliveryCenter dispatches to multiple channels
- [ ] Test: EmailHandler with attachments
- [ ] Test: WebhookHandler POST JSON
- [ ] Test: Partial success (some channels fail)
- [ ] Test: DeliveryResult aggregation
---
## Phase 11: P5 Learning Implementation
## Phase 11: API & Integration
**Goal:** Implement P5 (Learning). Full execution flow complete.
**Goal:** Complete API implementation, end-to-end tests. Main flow: P0 → P1 → P2 → P3 → P4.
**Depends on:** Phase 10 (P4 Delivery)
### 11.1 Learning Agent Setup
> **Note:** P5 Learning is an advanced feature (async, background, user-invisible).
> Main flow works without it. Moved to Phase 12 (Advanced Features).
- [ ] `robot/learning/package.yao` - Learning Agent config
- [ ] `robot/learning/prompts.yml` - learning prompts
### 11.2 Store Implementation
- [ ] `store/store.go` - Store interface and struct
- [ ] `store/kb.go` - KB operations (create, save, search)
- [ ] `store/learning.go` - save learning entries to private KB
### 11.3 Implementation
- [ ] `executor/learning.go` - `RunLearning(ctx, exec, data)` - real implementation
- [ ] `executor/learning.go` - extract learnings from execution
- [ ] `executor/learning.go` - call Learning Agent
- [ ] `executor/learning.go` - save to private KB
### 11.4 Tests
- [ ] `executor/learning_test.go` - P5 learning
- [ ] Test: learnings extracted from execution
- [ ] Test: learnings saved to KB
- [ ] Test: KB can be queried for past learnings
---
## Phase 12: API & Integration
**Goal:** Complete API implementation, end-to-end tests.
### 12.1 API Implementation
### 11.1 API Implementation
- [ ] `api/api.go` - implement all Go API functions
- [ ] `api/process.go` - implement all Process handlers
- [ ] `api/jsapi.go` - implement JSAPI
### 12.2 End-to-End Tests
### 11.2 End-to-End Tests
- [ ] Full clock trigger flow (P0 → P5)
- [ ] Human intervention flow (P1 → P5)
- [ ] Event trigger flow (P1 → P5)
- [ ] Full clock trigger flow (P0 → P1 → P2 → P3 → P4)
- [ ] Human intervention flow (P1 → P2 → P3 → P4)
- [ ] Event trigger flow (P1 → P2 → P3 → P4)
- [ ] Concurrent execution test
- [ ] Pause/Resume/Stop test
### 12.3 Integration with OpenAPI
### 11.3 Integration with OpenAPI
- [ ] HTTP endpoints for human intervention
- [ ] Webhook endpoints for events
---
## Phase 13: Advanced Features
## Phase 12: Advanced Features
**Goal:** Implement dedup, semantic dedup, plan queue.
**Goal:** Implement P5 Learning, dedup, semantic dedup, plan queue.
### 13.1 Fast Dedup (Time-Window)
> **Note:** These are optional advanced features. Main flow works without them.
### 12.1 P5 Learning Implementation
> **Background:** P5 Learning is async, runs after P4 Delivery completes.
> User doesn't wait for it. Results stored in private KB for future reference.
#### 12.1.1 Learning Agent Setup
- [ ] `robot/learning/package.yao` - Learning Agent config
- [ ] `robot/learning/prompts.yml` - learning prompts
#### 12.1.2 Store Implementation
- [ ] `store/store.go` - Store interface and struct
- [ ] `store/kb.go` - KB operations (create, save, search)
- [ ] `store/learning.go` - save learning entries to private KB
#### 12.1.3 Implementation
- [ ] `executor/learning.go` - `RunLearning(ctx, exec, data)` - real implementation
- [ ] `executor/learning.go` - extract learnings from execution
- [ ] `executor/learning.go` - call Learning Agent
- [ ] `executor/learning.go` - save to private KB
#### 12.1.4 Tests
- [ ] `executor/learning_test.go` - P5 learning
- [ ] Test: learnings extracted from execution
- [ ] Test: learnings saved to KB
- [ ] Test: KB can be queried for past learnings
### 12.2 Fast Dedup (Time-Window)
> **Note:** Manager has `// TODO: dedup check` comment placeholder. Integrate after implementation.
@ -966,13 +1105,13 @@ Created new `yao/assert` package for universal assertion/validation:
- [ ] Integrate into Manager.Tick()
- [ ] Test: dedup check/mark, window expiry
### 13.2 Semantic Dedup
### 12.3 Semantic Dedup
- [ ] `dedup/semantic.go` - call Dedup Agent for goal/task level dedup
- [ ] Dedup Agent setup (`assistants/robot/dedup/`)
- [ ] Test: semantic dedup with real LLM
### 13.3 Plan Queue
### 12.4 Plan Queue
- [ ] `plan/plan.go` - plan queue implementation
- [ ] Store planned tasks/goals
@ -1100,13 +1239,15 @@ func TestWithLLM(t *testing.T) {
| 7. P1 Goals | ✅ | Goal Generation Agent integration |
| 8. P2 Tasks | ✅ | Task Planning Agent integration |
| 9. P3 Run | ✅ | Task execution + validation + yao/assert + multi-turn conversation |
| 10. P4 Delivery | ⬜ | Output delivery (email/file/webhook/notify) |
| 11. P5 Learning | ⬜ | Learning Agent + KB save |
| 12. API & Integration | ⬜ | Complete API, end-to-end tests |
| 13. Advanced | ⬜ | Semantic dedup, plan queue, Sandbox mode (requires container infrastructure) |
| 10. P4 Delivery | ⬜ | Output delivery (email/webhook, notify future) |
| 11. API & Integration | ⬜ | Complete API, end-to-end tests (main flow: P0→P1→P2→P3→P4) |
| 12. Advanced | ⬜ | P5 Learning, dedup, plan queue, Sandbox mode |
Legend: ⬜ Not started | 🟡 In progress | ✅ Complete
**Main Flow (MVP):** P0 Inspiration → P1 Goals → P2 Tasks → P3 Run → P4 Delivery
**Advanced (Optional):** P5 Learning (async), Dedup, Plan Queue, Sandbox
---
## Quick Commands