Enhance Task Validation and Update TODO.md

- Introduced a validation mechanism for task results, including a detailed validation structure with scores and issues.
- Updated the input formatter to include validation results in the output, improving clarity on task success and validation status.
- Enhanced test cases to cover the new validation fields and ensure comprehensive testing of task results.
- Revised TODO.md to reflect the addition of validation features and the current status of the agent's development phases.
This commit is contained in:
Max 2026-01-16 10:30:37 +08:00
parent bcb04f8677
commit ac5e3e484f
6 changed files with 327 additions and 100 deletions

View file

@ -428,85 +428,180 @@ Trigger → Manager → Cache → Dedup → Pool → Worker → Executor(stub)
## Phase 5: Test Scenario & Assistants Setup
**Goal:** Create a realistic test scenario with all required assistants.
**Goal:** Create realistic test scenarios with all required assistants.
**Test Scenario: Sales Analyst Robot**
A Sales Analyst robot that:
- Wakes up at 09:00 on weekdays
- Checks sales data and market news
- Generates daily goals based on findings
- Creates 2-3 actionable tasks
- (Future: executes tasks, delivers report, learns)
Example flow:
**Architecture:**
```
Clock: Monday 09:00
P0 Inspiration:
- Clock: Monday morning, start of week
- Data: 15 new orders (+20% vs last week)
- News: Competitor launched new product
P1 Goals:
1. [High] Analyze weekend sales spike
2. [Normal] Review competitor product launch
3. [Low] Update weekly forecast
P2 Tasks:
Task 1: Query sales DB for weekend orders
Task 2: Search web for competitor news
Task 3: Generate forecast update
┌─────────────────────────────────────────────────────────────────────────────┐
│ 6 Generic Phase Agents (P0-P5) │
├─────────────────────────────────────────────────────────────────────────────┤
│ inspiration │ goals │ tasks │ validation │ delivery │ learning │
│ (P0) │ (P1) │ (P2) │ (P3) │ (P4) │ (P5) │
└───────────────┴─────────┴─────────┴──────────────┴────────────┴─────────────┘
↓ P2 assigns tasks to
┌─────────────────────────────────────────────────────────────────────────────┐
│ Expert Agents (Task Executors) │
├─────────────────────────────────────────────────────────────────────────────┤
│ text-writer │ web-reader │ data-analyst │ summarizer │ ... │
│ (Generate) │ (Fetch URL) │ (Analyze) │ (Summarize) │ │
└───────────────┴──────────────┴────────────────┴──────────────┴─────────────┘
```
### 5.1 Test Assistant Directory Structure
**Test Strategy:**
Create `yao-dev-app/assistants/robot/` directory:
- Phase Agents (P0-P5) are **generic** and reusable across all robot types
- Expert Agents are **specialized** for specific tasks (text, web, data, etc.)
- Each P0-P5 test uses **different expert combinations** to cover real scenarios
- Tests use `interval: 1s` or `TriggerManual()` for easy triggering (no time dependency)
### 5.1 Directory Structure
```
assistants/robot/
├── inspiration/ # P0: Inspiration Agent
│ ├── package.yao # Assistant config
│ └── prompts.yml # System prompt & templates
├── goals/ # P1: Goal Generation Agent
│ ├── package.yao
│ └── prompts.yml
├── tasks/ # P2: Task Planning Agent
│ ├── package.yao
│ └── prompts.yml
├── validation/ # P3: Validation Agent (future)
│ ├── package.yao
│ └── prompts.yml
├── delivery/ # P4: Delivery Agent (future)
│ ├── package.yao
│ └── prompts.yml
└── learning/ # P5: Learning Agent (future)
├── package.yao
└── prompts.yml
yao-dev-app/assistants/
├── robot/ # Generic Phase Agents
│ ├── inspiration/ # P0: Analyze clock context, generate insights
│ │ ├── package.yao
│ │ └── prompts.yml
│ ├── goals/ # P1: Generate prioritized goals
│ │ ├── package.yao
│ │ └── prompts.yml
│ ├── tasks/ # P2: Split goals into executable tasks
│ │ ├── package.yao
│ │ └── prompts.yml
│ ├── validation/ # P3: Validate task results
│ │ ├── package.yao
│ │ └── prompts.yml
│ ├── delivery/ # P4: Format and deliver results
│ │ ├── package.yao
│ │ └── prompts.yml
│ └── learning/ # P5: Summarize execution, extract insights
│ ├── package.yao
│ └── prompts.yml
└── experts/ # Expert Agents (Task Executors)
├── text-writer/ # Generate text content (reports, emails, summaries)
│ ├── package.yao
│ └── prompts.yml
├── web-reader/ # Fetch and parse web page content
│ ├── package.yao
│ └── prompts.yml
├── data-analyst/ # Analyze data, generate insights
│ ├── package.yao
│ └── prompts.yml
└── summarizer/ # Summarize long text into key points
├── package.yao
└── prompts.yml
```
### 5.2 Inspiration Assistant (P0)
### 5.2 Generic Phase Agents
#### 5.2.1 Inspiration Agent (P0)
- [ ] `robot/inspiration/package.yao` - config with model, temperature
- [ ] `robot/inspiration/prompts.yml` - system prompt for P0:
- Input: Clock context, robot identity, data sources
- [ ] `robot/inspiration/prompts.yml` - system prompt:
- Input: Clock context (time, day, markers), robot identity
- Output: Markdown report with Summary, Highlights, Opportunities, Risks
- Style: Analytical, context-aware
### 5.3 Goals Assistant (P1)
#### 5.2.2 Goals Agent (P1)
- [ ] `robot/goals/package.yao` - config
- [ ] `robot/goals/prompts.yml` - system prompt for P1:
- Input: Inspiration report, robot duties
- Output: Prioritized goals in markdown format
- [ ] `robot/goals/prompts.yml` - system prompt:
- Input: Inspiration report OR trigger input (human/event)
- Output: Prioritized goals in markdown (High/Normal/Low)
- Style: Strategic, actionable
### 5.4 Tasks Assistant (P2)
#### 5.2.3 Tasks Agent (P2)
- [ ] `robot/tasks/package.yao` - config
- [ ] `robot/tasks/prompts.yml` - system prompt for P2:
- Input: Goals, available tools/agents
- Output: Structured task list (JSON)
- [ ] `robot/tasks/prompts.yml` - system prompt:
- Input: Goals, available expert agents list
- Output: Structured task list (JSON) with executor assignments
- Style: Detailed, executable
#### 5.2.4 Validation Agent (P3)
- [ ] `robot/validation/package.yao` - config
- [ ] `robot/validation/prompts.yml` - system prompt:
- Input: Task result, expected outcome
- Output: Validation result (pass/fail, issues, suggestions)
- Style: Critical, thorough
#### 5.2.5 Delivery Agent (P4)
- [ ] `robot/delivery/package.yao` - config
- [ ] `robot/delivery/prompts.yml` - system prompt:
- Input: Task results, delivery target (email, report, notification)
- Output: Formatted delivery content
- Style: Clear, professional
#### 5.2.6 Learning Agent (P5)
- [ ] `robot/learning/package.yao` - config
- [ ] `robot/learning/prompts.yml` - system prompt:
- Input: Full execution summary
- Output: Insights, patterns, improvement suggestions
- Style: Reflective, insightful
### 5.3 Expert Agents (Task Executors)
#### 5.3.1 Text Writer
- [ ] `experts/text-writer/package.yao` - config
- [ ] `experts/text-writer/prompts.yml` - system prompt:
- Input: Topic, key points, style (formal/casual), length
- Output: Generated text content
- Use cases: Weekly reports, email drafts, summaries
#### 5.3.2 Web Reader
- [ ] `experts/web-reader/package.yao` - config
- [ ] `experts/web-reader/prompts.yml` - system prompt:
- Input: URL or topic to search
- Output: Extracted content, key information
- Use cases: News fetching, competitor monitoring, research
- Note: May use MCP tools for actual web access
#### 5.3.3 Data Analyst
- [ ] `experts/data-analyst/package.yao` - config
- [ ] `experts/data-analyst/prompts.yml` - system prompt:
- Input: Data description, analysis goal
- Output: Analysis report, trends, insights
- Use cases: Sales analysis, performance review
#### 5.3.4 Summarizer
- [ ] `experts/summarizer/package.yao` - config
- [ ] `experts/summarizer/prompts.yml` - system prompt:
- Input: Long text content
- Output: Concise summary with key points
- Use cases: Document summarization, meeting notes
### 5.4 Test Scenarios
Each phase test uses different expert combinations:
| Test | Phase | Trigger | Expert Agents Used | Verification |
| ---- | ----- | ---------------- | ------------------------ | ----------------------------- |
| T1 | P0 | Clock (interval) | - | Clock → Inspiration report |
| T2 | P1 | Clock | - | Inspiration → Goals |
| T3 | P1 | Human | - | User input → Goals |
| T4 | P2 | Clock | text-writer, web-reader | Goals → Tasks with executors |
| T5 | P3 | Clock | text-writer | Task exec → Result validation |
| T6 | P3 | Human | summarizer | Task exec → Result validation |
| T7 | P4 | Clock | - | Results → Delivery format |
| T8 | P5 | Clock | - | Full execution → Insights |
| T9 | E2E | Clock | text-writer, summarizer | Full P0→P5 flow |
| T10 | E2E | Human | web-reader, data-analyst | Full P1→P5 flow |
### 5.5 Test Data Setup
- [ ] Create test robot config in `__yao.member` with:
- `trigger.clock.mode: interval`, `interval: 1s` (for fast testing)
- `resources.agents: [experts.text-writer, experts.web-reader, ...]`
- [ ] Create test trigger data for Human/Event scenarios
---

View file

@ -273,6 +273,7 @@ func (f *InputFormatter) FormatTaskResults(results []types.TaskResult) string {
successCount := 0
failCount := 0
validatedCount := 0
for _, result := range results {
if result.Success {
@ -280,6 +281,9 @@ func (f *InputFormatter) FormatTaskResults(results []types.TaskResult) string {
} else {
failCount++
}
if result.Validation != nil && result.Validation.Passed {
validatedCount++
}
sb.WriteString(fmt.Sprintf("### Task: %s\n\n", result.TaskID))
if result.Success {
@ -288,7 +292,21 @@ func (f *InputFormatter) FormatTaskResults(results []types.TaskResult) string {
sb.WriteString("- **Status**: ✗ Failed\n")
}
sb.WriteString(fmt.Sprintf("- **Duration**: %dms\n", result.Duration))
sb.WriteString(fmt.Sprintf("- **Validated**: %t\n", result.Validated))
// Validation result (P3)
if result.Validation != nil {
if result.Validation.Passed {
sb.WriteString(fmt.Sprintf("- **Validation**: ✓ Passed (score: %.2f)\n", result.Validation.Score))
} else {
sb.WriteString("- **Validation**: ✗ Failed\n")
if len(result.Validation.Issues) > 0 {
sb.WriteString(" - Issues:\n")
for _, issue := range result.Validation.Issues {
sb.WriteString(fmt.Sprintf(" - %s\n", issue))
}
}
}
}
// Output
if result.Output != nil {
@ -310,8 +328,8 @@ func (f *InputFormatter) FormatTaskResults(results []types.TaskResult) string {
}
// Summary
sb.WriteString(fmt.Sprintf("## Summary\n\n- Total: %d tasks\n- Success: %d\n- Failed: %d\n",
len(results), successCount, failCount))
sb.WriteString(fmt.Sprintf("## Summary\n\n- Total: %d tasks\n- Success: %d\n- Failed: %d\n- Validated: %d\n",
len(results), successCount, failCount, validatedCount))
return sb.String()
}

View file

@ -282,18 +282,24 @@ func TestInputFormatterFormatTaskResults(t *testing.T) {
t.Run("formats task results with summary", func(t *testing.T) {
results := []types.TaskResult{
{
TaskID: "task-1",
Success: true,
Duration: 150,
Validated: true,
Output: map[string]interface{}{"rows": 100},
TaskID: "task-1",
Success: true,
Duration: 150,
Validation: &types.ValidationResult{
Passed: true,
Score: 0.95,
},
Output: map[string]interface{}{"rows": 100},
},
{
TaskID: "task-2",
Success: false,
Duration: 50,
Validated: false,
Error: "Connection timeout",
TaskID: "task-2",
Success: false,
Duration: 50,
Validation: &types.ValidationResult{
Passed: false,
Issues: []string{"Connection timeout"},
},
Error: "Connection timeout",
},
}
@ -303,14 +309,18 @@ func TestInputFormatterFormatTaskResults(t *testing.T) {
assert.Contains(t, result, "### Task: task-1")
assert.Contains(t, result, "✓ Success")
assert.Contains(t, result, "150ms")
assert.Contains(t, result, "**Validation**: ✓ Passed")
assert.Contains(t, result, "score: 0.95")
assert.Contains(t, result, "**Output**")
assert.Contains(t, result, "### Task: task-2")
assert.Contains(t, result, "✗ Failed")
assert.Contains(t, result, "**Validation**: ✗ Failed")
assert.Contains(t, result, "Connection timeout")
assert.Contains(t, result, "## Summary")
assert.Contains(t, result, "Total: 2 tasks")
assert.Contains(t, result, "Success: 1")
assert.Contains(t, result, "Failed: 1")
assert.Contains(t, result, "Validated: 1")
})
t.Run("returns message for empty results", func(t *testing.T) {

View file

@ -66,11 +66,14 @@ func (e *Executor) RunExecution(_ *types.Context, exec *types.Execution, _ inter
// Generate mock result with actual duration
exec.Results[i] = types.TaskResult{
TaskID: exec.Tasks[i].ID,
Success: true,
Output: fmt.Sprintf("Mock output for %s: Task completed successfully", exec.Tasks[i].ID),
Duration: endTime.Sub(startTime).Milliseconds(),
Validated: true,
TaskID: exec.Tasks[i].ID,
Success: true,
Output: fmt.Sprintf("Mock output for %s: Task completed successfully", exec.Tasks[i].ID),
Duration: endTime.Sub(startTime).Milliseconds(),
Validation: &types.ValidationResult{
Passed: true,
Score: 1.0,
},
}
}

View file

@ -172,7 +172,7 @@ type CurrentState struct {
Progress string `json:"progress,omitempty"` // human-readable progress (e.g., "2/5 tasks")
}
// Goals - P1 output (markdown for LLM)
// Goals - P1 output (markdown for LLM + structured metadata)
// P1 Agent reads InspirationReport and generates goals as markdown
// Example:
// ## Goals
@ -186,6 +186,22 @@ type CurrentState struct {
// - Reason: 3 pending leads from yesterday
type Goals struct {
Content string `json:"content"` // markdown text
// Success criteria for P3 validation (semantic validation by LLM)
// Example: ["Report must include sales summary", "Growth rate must be calculated"]
SuccessCriteria []string `json:"success_criteria,omitempty"`
// Delivery target for P4 (where to send results)
DeliveryTarget *DeliveryTarget `json:"delivery_target,omitempty"`
}
// DeliveryTarget - where to deliver results (defined in P1, used in P4)
type DeliveryTarget struct {
Type DeliveryType `json:"type"` // email | webhook | report | notification
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
Options map[string]interface{} `json:"options,omitempty"` // channel-specific options
}
// Task - planned task (structured, for execution)
@ -200,6 +216,12 @@ type Task struct {
ExecutorID string `json:"executor_id"`
Args []any `json:"args,omitempty"`
// Validation (defined in P2, used in P3)
// ExpectedOutput describes what the task should produce (for LLM semantic validation)
ExpectedOutput string `json:"expected_output,omitempty"` // e.g., "JSON with sales_total, growth_rate fields"
// ValidationRules are specific checks to perform (can be semantic or structural)
ValidationRules []string `json:"validation_rules,omitempty"` // e.g., ["output must be valid JSON", "sales_total > 0"]
// Runtime
Status TaskStatus `json:"status"`
Order int `json:"order"` // execution order (0-based)
@ -209,20 +231,34 @@ type Task struct {
// TaskResult - task execution result
type TaskResult struct {
TaskID string `json:"task_id"`
Success bool `json:"success"`
Output interface{} `json:"output,omitempty"`
Error string `json:"error,omitempty"`
Duration int64 `json:"duration_ms"`
Validated bool `json:"validated"`
TaskID string `json:"task_id"`
Success bool `json:"success"`
Output interface{} `json:"output,omitempty"`
Error string `json:"error,omitempty"`
Duration int64 `json:"duration_ms"`
// Validation result (populated by P3)
Validation *ValidationResult `json:"validation,omitempty"`
}
// DeliveryResult - delivery output
// ValidationResult - P3 semantic validation result
type ValidationResult struct {
Passed bool `json:"passed"` // overall validation passed
Score float64 `json:"score,omitempty"` // 0-1 confidence score
Issues []string `json:"issues,omitempty"` // what failed
Suggestions []string `json:"suggestions,omitempty"` // how to improve
Details string `json:"details,omitempty"` // detailed validation report (markdown)
}
// DeliveryResult - P4 delivery output
type DeliveryResult struct {
Type DeliveryType `json:"type"`
Success bool `json:"success"`
Details interface{} `json:"details,omitempty"`
Error string `json:"error,omitempty"`
Type DeliveryType `json:"type"`
Success bool `json:"success"`
Recipients []string `json:"recipients,omitempty"` // who received
Content string `json:"content,omitempty"` // formatted content that was delivered
Details interface{} `json:"details,omitempty"` // channel-specific response
Error string `json:"error,omitempty"`
SentAt *time.Time `json:"sent_at,omitempty"`
}
// LearningEntry - knowledge to save

View file

@ -2,6 +2,7 @@ package types_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/agent/robot/types"
@ -397,6 +398,9 @@ func TestTaskStructure(t *testing.T) {
ExecutorID: "assistant1",
Status: types.TaskPending,
Order: 0,
// P3 validation fields
ExpectedOutput: "JSON with sales_total and growth_rate fields",
ValidationRules: []string{"sales_total > 0", "growth_rate is a percentage"},
}
assert.Equal(t, "task1", task.ID)
@ -406,46 +410,107 @@ func TestTaskStructure(t *testing.T) {
assert.Equal(t, "assistant1", task.ExecutorID)
assert.Equal(t, types.TaskPending, task.Status)
assert.Equal(t, 0, task.Order)
// Validation fields
assert.Contains(t, task.ExpectedOutput, "sales_total")
assert.Len(t, task.ValidationRules, 2)
}
func TestGoalsStructure(t *testing.T) {
goals := &types.Goals{
Content: "## Goals\n1. [High] Complete project\n2. [Normal] Review code",
SuccessCriteria: []string{
"Project deliverables submitted",
"Code review completed with no blockers",
},
DeliveryTarget: &types.DeliveryTarget{
Type: types.DeliveryEmail,
Recipients: []string{"team@example.com"},
Format: "markdown",
},
}
assert.Contains(t, goals.Content, "Goals")
assert.Contains(t, goals.Content, "Complete project")
assert.Len(t, goals.SuccessCriteria, 2)
assert.Contains(t, goals.SuccessCriteria[0], "deliverables")
assert.NotNil(t, goals.DeliveryTarget)
assert.Equal(t, types.DeliveryEmail, goals.DeliveryTarget.Type)
}
func TestTaskResultStructure(t *testing.T) {
result := &types.TaskResult{
TaskID: "task1",
Success: true,
Output: "Task completed successfully",
Duration: 1500,
Validated: true,
TaskID: "task1",
Success: true,
Output: "Task completed successfully",
Duration: 1500,
Validation: &types.ValidationResult{
Passed: true,
Score: 0.98,
},
}
assert.Equal(t, "task1", result.TaskID)
assert.True(t, result.Success)
assert.Equal(t, "Task completed successfully", result.Output)
assert.Equal(t, int64(1500), result.Duration)
assert.True(t, result.Validated)
assert.NotNil(t, result.Validation)
assert.True(t, result.Validation.Passed)
assert.Equal(t, 0.98, result.Validation.Score)
}
func TestValidationResultStructure(t *testing.T) {
validation := &types.ValidationResult{
Passed: false,
Score: 0.45,
Issues: []string{"Missing required field: sales_total", "Growth rate is negative"},
Suggestions: []string{"Add sales_total calculation", "Verify data source"},
Details: "Detailed validation report...",
}
assert.False(t, validation.Passed)
assert.Equal(t, 0.45, validation.Score)
assert.Len(t, validation.Issues, 2)
assert.Contains(t, validation.Issues[0], "sales_total")
assert.Len(t, validation.Suggestions, 2)
}
func TestDeliveryResultStructure(t *testing.T) {
sentAt := time.Now()
delivery := &types.DeliveryResult{
Type: types.DeliveryEmail,
Success: true,
Type: types.DeliveryEmail,
Success: true,
Recipients: []string{"user@example.com", "manager@example.com"},
Content: "# Weekly Report\n\nSales increased by 20%...",
Details: map[string]interface{}{
"to": "user@example.com",
"subject": "Daily Report",
"message_id": "msg-12345",
"subject": "Daily Report",
},
SentAt: &sentAt,
}
assert.Equal(t, types.DeliveryEmail, delivery.Type)
assert.True(t, delivery.Success)
assert.Len(t, delivery.Recipients, 2)
assert.Contains(t, delivery.Content, "Weekly Report")
assert.NotNil(t, delivery.Details)
assert.NotNil(t, delivery.SentAt)
}
func TestDeliveryTargetStructure(t *testing.T) {
target := &types.DeliveryTarget{
Type: types.DeliveryEmail,
Recipients: []string{"team@example.com"},
Format: "markdown",
Template: "weekly-report",
Options: map[string]interface{}{
"cc": []string{"manager@example.com"},
},
}
assert.Equal(t, types.DeliveryEmail, target.Type)
assert.Len(t, target.Recipients, 1)
assert.Equal(t, "markdown", target.Format)
assert.Equal(t, "weekly-report", target.Template)
}
func TestLearningEntryStructure(t *testing.T) {