Enhance Goals and Delivery Structure in Robot Agent

- Introduced a structured Goals type with delivery metadata for improved task management and output handling.
- Updated the DeliveryTarget type to include additional fields for recipients, format, and options, enhancing flexibility in result delivery.
- Enhanced the Executor's RunGoals method to parse and handle delivery information from agent responses.
- Revised input formatting to include robot identity context, improving clarity in generated goals.
- Updated tests to validate the new structure and ensure comprehensive coverage of delivery functionalities.
This commit is contained in:
Max 2026-01-16 16:29:39 +08:00
parent 41e0544aba
commit 2d86c9caad
8 changed files with 767 additions and 56 deletions

View file

@ -309,8 +309,24 @@ type ClockContext struct {
**For Human/Event:** Uses the input directly as goals (or to generate goals).
```go
type Goals struct {
Content string // markdown text (for LLM)
Delivery *DeliveryTarget // where to send results (for P4)
}
type DeliveryTarget struct {
Type DeliveryType // email | webhook | report | notification
Recipients []string // email addresses, webhook URLs, user IDs
Format string // markdown | html | json | text
Template string // template name
Options map[string]interface{}
}
```
**Example prompt:**
```
Prompt:
You are [Sales Manager]. Your job: [track KPIs, make reports].
## Report
@ -326,20 +342,26 @@ You are [Sales Manager]. Your job: [track KPIs, make reports].
Make today's goals.
```
**Note:** Validation criteria (`ExpectedOutput`, `ValidationRules`) are defined at the **Task level** (P2), not Goals level. This allows each task to have specific validation rules for P3.
### 4.4 P2: Tasks
P2 Agent reads Goals markdown and breaks into executable tasks:
```go
type Task struct {
ID string // unique task ID
Messages []context.Message // original input (text, images, files, audio)
GoalRef string // reference to goal (e.g., "Goal 1")
Source TaskSource // auto | human | event
ExecutorType ExecutorType // assistant | mcp | process
ExecutorID string // agent ID or mcp tool name
Args []any // arguments for executor
Order int // execution order
ID string // unique task ID
Messages []context.Message // original input (text, images, files, audio)
GoalRef string // reference to goal (e.g., "Goal 1")
Source TaskSource // auto | human | event
ExecutorType ExecutorType // assistant | mcp | process
ExecutorID string // agent ID or mcp tool name
Args []any // arguments for executor
Order int // execution order
// Validation criteria (used in P3)
ExpectedOutput string // what the task should produce
ValidationRules []string // specific checks to perform
}
```
@ -349,9 +371,18 @@ For each task:
1. Call Assistant or MCP Tool
2. Get result
3. Validate
3. Validate against `ExpectedOutput` and `ValidationRules`
4. Update status
```go
type ValidationResult struct {
Passed bool // overall validation passed
Score float64 // 0-1 confidence score
Issues []string // what failed
Suggestions []string // how to improve
}
```
### 4.6 P4: Deliver
Send output:

View file

@ -1287,7 +1287,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
@ -1298,7 +1298,17 @@ type CurrentState struct {
// 3. [Low] Update CRM with new leads
// - Reason: 3 pending leads from yesterday
type Goals struct {
Content string `json:"content"` // markdown text
Content string `json:"content"` // markdown text
Delivery *DeliveryTarget `json:"delivery,omitempty"` // where to send results (for P4)
}
// 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)
@ -1313,6 +1323,10 @@ type Task struct {
ExecutorID string `json:"executor_id"`
Args []any `json:"args,omitempty"`
// Validation (defined in P2, used in P3)
ExpectedOutput string `json:"expected_output,omitempty"` // what the task should produce
ValidationRules []string `json:"validation_rules,omitempty"` // specific checks to perform
// Runtime
Status TaskStatus `json:"status"`
Order int `json:"order"` // execution order (0-based)
@ -1352,20 +1366,32 @@ const (
// 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 *ValidationResult `json:"validation,omitempty"` // P3 validation result
}
// 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
}
// 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 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

@ -1,6 +1,9 @@
package standard
import (
"fmt"
"strings"
robottypes "github.com/yaoapp/yao/agent/robot/types"
)
@ -12,14 +15,155 @@ import (
// - TriggerInput for human/event trigger
//
// Output:
// - Goals with markdown content listing prioritized objectives
//
// TODO: Implement real Agent call
// - Goals with markdown content and delivery info
func (e *Executor) RunGoals(ctx *robottypes.Context, exec *robottypes.Execution, _ interface{}) error {
e.simulateStreamDelay()
exec.Goals = &robottypes.Goals{
Content: "## Today's Goals\n\n1. [High] Review pending tasks\n2. [Medium] Process new requests\n3. [Low] Organize knowledge base",
// Get robot for identity and resources
robot := exec.GetRobot()
if robot == nil {
return fmt.Errorf("robot not found in execution")
}
// Get agent ID for goals phase
agentID := "__yao.goals" // default
if robot.Config != nil && robot.Config.Resources != nil {
agentID = robot.Config.Resources.GetPhaseAgent(robottypes.PhaseGoals)
}
// Build prompt based on trigger type
formatter := NewInputFormatter()
var userContent string
switch exec.TriggerType {
case robottypes.TriggerClock:
// For clock trigger: use InspirationReport from P0
if exec.Inspiration != nil {
userContent = formatter.FormatInspirationReport(exec.Inspiration)
} else {
// Fallback: if no inspiration report, create minimal context
userContent = formatter.FormatClockContext(
robottypes.NewClockContext(exec.StartTime, ""),
robot,
)
}
case robottypes.TriggerHuman, robottypes.TriggerEvent:
// For human/event trigger: use TriggerInput directly
if exec.Input != nil {
userContent = formatter.FormatTriggerInput(exec.Input)
}
}
// Add robot identity context if not already included
// For clock trigger with inspiration report, identity is not in the report
// For human/event trigger, identity provides context
if robot.Config != nil && robot.Config.Identity != nil {
if !strings.Contains(userContent, "## Robot Identity") {
userContent = formatter.FormatRobotIdentity(robot) + "\n\n" + userContent
}
}
if userContent == "" {
return fmt.Errorf("no input available for goals generation")
}
// Call agent
caller := NewAgentCaller()
result, err := caller.CallWithMessages(ctx, agentID, userContent)
if err != nil {
return fmt.Errorf("goals agent call failed: %w", err)
}
// Parse response as JSON
// Goals Agent returns: { "content": "...", "delivery": {...} }
data, err := result.GetJSON()
if err != nil {
// Fallback: if not JSON, use raw text as content
content := result.GetText()
if content == "" {
return fmt.Errorf("goals agent returned empty response")
}
exec.Goals = &robottypes.Goals{
Content: content,
}
return nil
}
// Build Goals from JSON
exec.Goals = &robottypes.Goals{}
// Extract content (markdown)
if content, ok := data["content"].(string); ok {
exec.Goals.Content = content
}
// Extract delivery
if delivery, ok := data["delivery"].(map[string]interface{}); ok {
exec.Goals.Delivery = parseDelivery(delivery)
}
// Validate: content is required
if exec.Goals.Content == "" {
return fmt.Errorf("goals agent returned empty content")
}
return nil
}
// parseDelivery converts map to DeliveryTarget struct
func parseDelivery(data map[string]interface{}) *robottypes.DeliveryTarget {
if data == nil {
return nil
}
target := &robottypes.DeliveryTarget{}
// Parse and validate type
if t, ok := data["type"].(string); ok {
deliveryType := robottypes.DeliveryType(t)
switch deliveryType {
case robottypes.DeliveryEmail, robottypes.DeliveryWebhook,
robottypes.DeliveryFile, robottypes.DeliveryNotify:
target.Type = deliveryType
default:
// Invalid type - still set it but caller should validate
target.Type = deliveryType
}
}
// Parse recipients
if recipients, ok := data["recipients"].([]interface{}); ok {
for _, r := range recipients {
if s, ok := r.(string); ok {
target.Recipients = append(target.Recipients, s)
}
}
}
// Parse format
if format, ok := data["format"].(string); ok {
target.Format = format
}
// Parse template
if template, ok := data["template"].(string); ok {
target.Template = template
}
// Parse options
if options, ok := data["options"].(map[string]interface{}); ok {
target.Options = options
}
return target
}
// IsValidDeliveryType checks if the delivery type is valid
func IsValidDeliveryType(t robottypes.DeliveryType) bool {
switch t {
case robottypes.DeliveryEmail, robottypes.DeliveryWebhook,
robottypes.DeliveryFile, robottypes.DeliveryNotify:
return true
default:
return false
}
}

View file

@ -0,0 +1,486 @@
package standard_test
import (
"context"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
agentcontext "github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/robot/executor/standard"
"github.com/yaoapp/yao/agent/robot/types"
"github.com/yaoapp/yao/agent/testutils"
)
// ============================================================================
// P1 Goals Phase Tests
// ============================================================================
func TestRunGoalsBasic(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
ctx := types.NewContext(context.Background(), testAuth())
t.Run("generates goals from inspiration report (clock trigger)", func(t *testing.T) {
// Create robot with goals agent configured
robot := createGoalsTestRobot(t, "robot.goals")
// Create execution with inspiration report (from P0)
exec := createGoalsTestExecution(robot, types.TriggerClock)
exec.Inspiration = &types.InspirationReport{
Clock: types.NewClockContext(time.Now(), ""),
Content: "## Summary\nToday is Monday morning. Focus on weekly planning.\n\n## Highlights\n- New sales leads arrived\n- Weekly report due Friday",
}
// Run goals phase
e := standard.New()
err := e.RunGoals(ctx, exec, nil)
require.NoError(t, err)
require.NotNil(t, exec.Goals)
assert.NotEmpty(t, exec.Goals.Content)
})
t.Run("includes priority markers in output", func(t *testing.T) {
robot := createGoalsTestRobot(t, "robot.goals")
exec := createGoalsTestExecution(robot, types.TriggerClock)
exec.Inspiration = &types.InspirationReport{
Clock: types.NewClockContext(time.Now(), ""),
Content: "## Summary\nUrgent: Customer complaint needs attention.\n\n## Highlights\n- Critical bug reported\n- Regular maintenance scheduled",
}
e := standard.New()
err := e.RunGoals(ctx, exec, nil)
require.NoError(t, err)
content := exec.Goals.Content
// Verify expected structure in markdown output
// Note: LLM output is non-deterministic, so we check for likely patterns
hasGoals := strings.Contains(content, "Goal") ||
strings.Contains(content, "##") ||
strings.Contains(content, "High") ||
strings.Contains(content, "Normal") ||
strings.Contains(content, "1.")
assert.True(t, hasGoals, "should contain goals structure, got: %s", content)
})
}
func TestRunGoalsHumanTrigger(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
ctx := types.NewContext(context.Background(), testAuth())
t.Run("generates goals from human intervention", func(t *testing.T) {
robot := createGoalsTestRobot(t, "robot.goals")
exec := createGoalsTestExecution(robot, types.TriggerHuman)
// Set human intervention input
exec.Input = &types.TriggerInput{
Action: "task.add",
UserID: "user-123",
Messages: []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "Please analyze the Q4 sales data and prepare a summary report for the management meeting tomorrow."},
},
}
e := standard.New()
err := e.RunGoals(ctx, exec, nil)
require.NoError(t, err)
require.NotNil(t, exec.Goals)
assert.NotEmpty(t, exec.Goals.Content)
// Goals should be related to the user request
content := strings.ToLower(exec.Goals.Content)
hasRelevantContent := strings.Contains(content, "sales") ||
strings.Contains(content, "report") ||
strings.Contains(content, "analysis") ||
strings.Contains(content, "data") ||
strings.Contains(content, "q4")
assert.True(t, hasRelevantContent, "goals should relate to user request, got: %s", exec.Goals.Content)
})
t.Run("includes robot identity for human trigger", func(t *testing.T) {
robot := &types.Robot{
MemberID: "test-robot-1",
TeamID: "test-team-1",
DisplayName: "Sales Analyst",
Config: &types.Config{
Identity: &types.Identity{
Role: "Sales Analyst",
Duties: []string{"Analyze sales data", "Generate reports"},
Rules: []string{"Focus on actionable insights"},
},
Resources: &types.Resources{
Phases: map[types.Phase]string{
types.PhaseGoals: "robot.goals",
},
},
},
}
exec := createGoalsTestExecution(robot, types.TriggerHuman)
exec.Input = &types.TriggerInput{
Action: "instruct",
Messages: []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "What should I focus on today?"},
},
}
e := standard.New()
err := e.RunGoals(ctx, exec, nil)
require.NoError(t, err)
assert.NotEmpty(t, exec.Goals.Content)
})
}
func TestRunGoalsEventTrigger(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
ctx := types.NewContext(context.Background(), testAuth())
t.Run("generates goals from event trigger", func(t *testing.T) {
robot := createGoalsTestRobot(t, "robot.goals")
exec := createGoalsTestExecution(robot, types.TriggerEvent)
// Set event input
exec.Input = &types.TriggerInput{
Source: "webhook",
EventType: "lead.created",
Data: map[string]interface{}{
"lead_id": "lead-456",
"company": "BigCorp Inc",
"contact_name": "John Smith",
"email": "john@bigcorp.com",
"interest": "Enterprise plan",
},
}
e := standard.New()
err := e.RunGoals(ctx, exec, nil)
require.NoError(t, err)
require.NotNil(t, exec.Goals)
assert.NotEmpty(t, exec.Goals.Content)
// Goals should be related to the event
content := strings.ToLower(exec.Goals.Content)
hasRelevantContent := strings.Contains(content, "lead") ||
strings.Contains(content, "bigcorp") ||
strings.Contains(content, "contact") ||
strings.Contains(content, "follow") ||
strings.Contains(content, "qualify")
assert.True(t, hasRelevantContent, "goals should relate to event, got: %s", exec.Goals.Content)
})
}
func TestRunGoalsErrorHandling(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
ctx := types.NewContext(context.Background(), testAuth())
t.Run("returns error when robot is nil", func(t *testing.T) {
exec := &types.Execution{
ID: "test-exec-1",
TriggerType: types.TriggerClock,
}
// Don't set robot
e := standard.New()
err := e.RunGoals(ctx, exec, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "robot not found")
})
t.Run("returns error when agent not found", func(t *testing.T) {
robot := &types.Robot{
MemberID: "test-robot-1",
TeamID: "test-team-1",
Config: &types.Config{
Identity: &types.Identity{Role: "Test"},
Resources: &types.Resources{
Phases: map[types.Phase]string{
types.PhaseGoals: "non.existent.agent",
},
},
},
}
exec := createGoalsTestExecution(robot, types.TriggerClock)
exec.Inspiration = &types.InspirationReport{
Content: "Test content",
}
e := standard.New()
err := e.RunGoals(ctx, exec, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "agent call failed")
})
t.Run("returns error when no input available and no identity", func(t *testing.T) {
// Robot without identity - should fail when no input is provided
robot := &types.Robot{
MemberID: "test-robot-1",
TeamID: "test-team-1",
Config: &types.Config{
// No Identity - so no fallback content
Resources: &types.Resources{
Phases: map[types.Phase]string{
types.PhaseGoals: "robot.goals",
},
},
},
}
exec := createGoalsTestExecution(robot, types.TriggerHuman)
exec.Input = nil // No input
e := standard.New()
err := e.RunGoals(ctx, exec, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "no input available")
})
}
func TestRunGoalsFallbackBehavior(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
ctx := types.NewContext(context.Background(), testAuth())
t.Run("falls back to clock context when no inspiration report", func(t *testing.T) {
robot := createGoalsTestRobot(t, "robot.goals")
exec := createGoalsTestExecution(robot, types.TriggerClock)
exec.Inspiration = nil // No inspiration report
e := standard.New()
err := e.RunGoals(ctx, exec, nil)
// Should still work with fallback clock context
require.NoError(t, err)
require.NotNil(t, exec.Goals)
assert.NotEmpty(t, exec.Goals.Content)
})
}
// ============================================================================
// Delivery Parsing Tests
// ============================================================================
func TestParseDeliveryFromGoalsResponse(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
ctx := types.NewContext(context.Background(), testAuth())
t.Run("parses delivery when agent returns it", func(t *testing.T) {
robot := createGoalsTestRobot(t, "robot.goals")
exec := createGoalsTestExecution(robot, types.TriggerHuman)
// Request that explicitly asks for email delivery
exec.Input = &types.TriggerInput{
Action: "task.add",
Messages: []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "Prepare a sales report and send it to team@example.com via email"},
},
}
e := standard.New()
err := e.RunGoals(ctx, exec, nil)
require.NoError(t, err)
require.NotNil(t, exec.Goals)
assert.NotEmpty(t, exec.Goals.Content)
// Delivery may or may not be present depending on LLM response
// If present, verify structure
if exec.Goals.Delivery != nil {
// Type should be valid if present
if exec.Goals.Delivery.Type != "" {
validTypes := []types.DeliveryType{
types.DeliveryEmail, types.DeliveryWebhook,
types.DeliveryFile, types.DeliveryNotify,
}
found := false
for _, vt := range validTypes {
if exec.Goals.Delivery.Type == vt {
found = true
break
}
}
// Note: LLM might return non-standard types, we accept them but log
t.Logf("Delivery type: %s (valid: %v)", exec.Goals.Delivery.Type, found)
}
}
})
}
func TestDeliveryTypeValidation(t *testing.T) {
t.Run("valid delivery types", func(t *testing.T) {
validTypes := []types.DeliveryType{
types.DeliveryEmail,
types.DeliveryWebhook,
types.DeliveryFile,
types.DeliveryNotify,
}
for _, dt := range validTypes {
assert.True(t, standard.IsValidDeliveryType(dt), "should be valid: %s", dt)
}
})
t.Run("invalid delivery types", func(t *testing.T) {
invalidTypes := []types.DeliveryType{
"invalid",
"sms",
"",
}
for _, dt := range invalidTypes {
assert.False(t, standard.IsValidDeliveryType(dt), "should be invalid: %s", dt)
}
})
}
// ============================================================================
// InputFormatter Tests for P1
// ============================================================================
func TestInputFormatterFormatRobotIdentity(t *testing.T) {
formatter := standard.NewInputFormatter()
t.Run("formats robot identity correctly", func(t *testing.T) {
robot := &types.Robot{
MemberID: "test-robot",
Config: &types.Config{
Identity: &types.Identity{
Role: "Sales Analyst",
Duties: []string{"Analyze sales data", "Generate reports"},
Rules: []string{"Be accurate", "Be concise"},
},
},
}
content := formatter.FormatRobotIdentity(robot)
assert.Contains(t, content, "## Robot Identity")
assert.Contains(t, content, "Sales Analyst")
assert.Contains(t, content, "Analyze sales data")
assert.Contains(t, content, "Generate reports")
assert.Contains(t, content, "Be accurate")
assert.Contains(t, content, "Be concise")
})
t.Run("returns empty for nil robot", func(t *testing.T) {
content := formatter.FormatRobotIdentity(nil)
assert.Empty(t, content)
})
t.Run("returns empty for robot without config", func(t *testing.T) {
robot := &types.Robot{MemberID: "test"}
content := formatter.FormatRobotIdentity(robot)
assert.Empty(t, content)
})
t.Run("returns empty for robot without identity", func(t *testing.T) {
robot := &types.Robot{
MemberID: "test",
Config: &types.Config{},
}
content := formatter.FormatRobotIdentity(robot)
assert.Empty(t, content)
})
t.Run("handles identity with only role", func(t *testing.T) {
robot := &types.Robot{
MemberID: "test",
Config: &types.Config{
Identity: &types.Identity{
Role: "Simple Bot",
},
},
}
content := formatter.FormatRobotIdentity(robot)
assert.Contains(t, content, "## Robot Identity")
assert.Contains(t, content, "Simple Bot")
assert.NotContains(t, content, "Duties")
assert.NotContains(t, content, "Rules")
})
}
// ============================================================================
// Helper Functions
// ============================================================================
// createGoalsTestRobot creates a test robot with specified goals agent
func createGoalsTestRobot(t *testing.T, agentID string) *types.Robot {
t.Helper()
return &types.Robot{
MemberID: "test-robot-1",
TeamID: "test-team-1",
DisplayName: "Test Robot",
Config: &types.Config{
Identity: &types.Identity{
Role: "Test Assistant",
Duties: []string{"Testing", "Validation"},
},
Resources: &types.Resources{
Phases: map[types.Phase]string{
types.PhaseGoals: agentID,
},
},
},
}
}
// createGoalsTestExecution creates a test execution for goals phase
func createGoalsTestExecution(robot *types.Robot, trigger types.TriggerType) *types.Execution {
exec := &types.Execution{
ID: "test-exec-goals-1",
MemberID: robot.MemberID,
TeamID: robot.TeamID,
TriggerType: trigger,
StartTime: time.Now(),
Status: types.ExecRunning,
Phase: types.PhaseGoals,
}
exec.SetRobot(robot)
return exec
}

View file

@ -80,6 +80,36 @@ func (f *InputFormatter) FormatClockContext(clock *robottypes.ClockContext, robo
return sb.String()
}
// FormatRobotIdentity formats robot identity as user message content
// Used to provide context about the robot's role and duties
func (f *InputFormatter) FormatRobotIdentity(robot *robottypes.Robot) string {
if robot == nil || robot.Config == nil || robot.Config.Identity == nil {
return ""
}
var sb strings.Builder
identity := robot.Config.Identity
sb.WriteString("## Robot Identity\n\n")
sb.WriteString(fmt.Sprintf("- **Role**: %s\n", identity.Role))
if len(identity.Duties) > 0 {
sb.WriteString("- **Duties**:\n")
for _, duty := range identity.Duties {
sb.WriteString(fmt.Sprintf(" - %s\n", duty))
}
}
if len(identity.Rules) > 0 {
sb.WriteString("- **Rules**:\n")
for _, rule := range identity.Rules {
sb.WriteString(fmt.Sprintf(" - %s\n", rule))
}
}
return sb.String()
}
// FormatInspirationReport formats InspirationReport as user message content
// Used by P1 (Goals) phase when trigger is Clock
func (f *InputFormatter) FormatInspirationReport(report *robottypes.InspirationReport) string {
@ -273,7 +303,8 @@ func (f *InputFormatter) FormatTaskResults(results []robottypes.TaskResult) stri
successCount := 0
failCount := 0
validatedCount := 0
validatedPassedCount := 0
validatedTotalCount := 0
for _, result := range results {
if result.Success {
@ -281,8 +312,11 @@ func (f *InputFormatter) FormatTaskResults(results []robottypes.TaskResult) stri
} else {
failCount++
}
if result.Validation != nil && result.Validation.Passed {
validatedCount++
if result.Validation != nil {
validatedTotalCount++
if result.Validation.Passed {
validatedPassedCount++
}
}
sb.WriteString(fmt.Sprintf("### Task: %s\n\n", result.TaskID))
@ -328,8 +362,8 @@ func (f *InputFormatter) FormatTaskResults(results []robottypes.TaskResult) stri
}
// Summary
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))
sb.WriteString(fmt.Sprintf("## Summary\n\n- Total: %d tasks\n- Success: %d\n- Failed: %d\n- Validated: %d/%d\n",
len(results), successCount, failCount, validatedPassedCount, validatedTotalCount))
return sb.String()
}

View file

@ -320,7 +320,7 @@ func TestInputFormatterFormatTaskResults(t *testing.T) {
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")
assert.Contains(t, result, "Validated: 1/2")
})
t.Run("returns message for empty results", func(t *testing.T) {

View file

@ -197,12 +197,8 @@ type CurrentState struct {
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"`
// Delivery for P4 (where to send results)
Delivery *DeliveryTarget `json:"delivery,omitempty"`
}
// DeliveryTarget - where to deliver results (defined in P1, used in P4)

View file

@ -418,11 +418,7 @@ func TestTaskStructure(t *testing.T) {
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{
Delivery: &types.DeliveryTarget{
Type: types.DeliveryEmail,
Recipients: []string{"team@example.com"},
Format: "markdown",
@ -431,10 +427,8 @@ func TestGoalsStructure(t *testing.T) {
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)
assert.NotNil(t, goals.Delivery)
assert.Equal(t, types.DeliveryEmail, goals.Delivery.Type)
}
func TestTaskResultStructure(t *testing.T) {
@ -497,7 +491,7 @@ func TestDeliveryResultStructure(t *testing.T) {
}
func TestDeliveryTargetStructure(t *testing.T) {
target := &types.DeliveryTarget{
delivery := &types.DeliveryTarget{
Type: types.DeliveryEmail,
Recipients: []string{"team@example.com"},
Format: "markdown",
@ -507,10 +501,10 @@ func TestDeliveryTargetStructure(t *testing.T) {
},
}
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)
assert.Equal(t, types.DeliveryEmail, delivery.Type)
assert.Len(t, delivery.Recipients, 1)
assert.Equal(t, "markdown", delivery.Format)
assert.Equal(t, "weekly-report", delivery.Template)
}
func TestLearningEntryStructure(t *testing.T) {