package standard import ( "fmt" "strings" robottypes "github.com/yaoapp/yao/agent/robot/types" ) // RunGoals executes P1: Goals phase // Calls the Goals Agent to plan daily objectives // // Input: // - InspirationReport (from P0) for clock trigger // - TriggerInput for human/event trigger // // Output: // - Goals with markdown content and delivery info func (e *Executor) RunGoals(ctx *robottypes.Context, exec *robottypes.Execution, _ interface{}) error { // §18.2: confirming phase may have already populated Goals — skip regeneration if exec.Goals != nil && exec.Goals.Content != "" { return nil } // Get robot for identity and resources robot := exec.GetRobot() if robot == nil { return fmt.Errorf("robot not found in execution") } // Update UI field with i18n locale := getEffectiveLocale(robot, exec.Input) e.updateUIFields(ctx, exec, "", getLocalizedMessage(locale, "planning_goals")) // Get agent ID for goals phase (per-robot config > global Uses > empty) agentID := robottypes.ResolvePhaseAgent(robot.Config, robottypes.PhaseGoals) if agentID == "" { return fmt.Errorf("no Goals Agent configured (set uses.goals in agent.yml or resources.phases in robot config)") } // 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 } } // Add available resources - critical for generating achievable goals // Without knowing what tools are available, goals might be unachievable resourcesContent := formatter.FormatAvailableResources(robot) if resourcesContent != "" { userContent += "\n\n" + resourcesContent } if userContent == "" { return fmt.Errorf("no input available for goals generation") } // Call agent caller := NewAgentCaller() caller.Workspace = robot.Workspace result, err := caller.CallWithMessages(ctx, agentID, userContent) if err != nil { return fmt.Errorf("goals agent (%s) call failed: %w", agentID, 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 (%s) returned empty content", agentID) } // Update Name from goals content (extract first line as execution title) if goalName := extractGoalName(exec.Goals); goalName != "" { e.updateUIFields(ctx, exec, goalName, "") } return nil } // ParseDelivery converts map to DeliveryTarget struct // Returns nil if data is nil or type is invalid/missing func ParseDelivery(data map[string]interface{}) *robottypes.DeliveryTarget { if data == nil { return nil } // Type is required - if missing or invalid, return nil t, ok := data["type"].(string) if !ok || t == "" { return nil } deliveryType := robottypes.DeliveryType(t) if !IsValidDeliveryType(deliveryType) { // Invalid type - return nil to indicate parsing failure return nil } target := &robottypes.DeliveryTarget{ 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.DeliveryProcess, robottypes.DeliveryNotify: return true default: return false } }