- Introduced a new `workspace` field across various robot-related structures, including `CreateRobotRequest`, `UpdateRobotRequest`, and `RobotResponse`, allowing for better organization and management of robots within specific workspaces. - Updated database queries and response mappings to accommodate the new workspace field, ensuring seamless integration with existing functionalities. - Enhanced agent execution context to include workspace information, improving the contextual awareness of agents during operations. - Added tests to validate the creation and updating of robots with workspace data, ensuring robust functionality and backward compatibility.
397 lines
11 KiB
Go
397 lines
11 KiB
Go
package standard
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
agentcontext "github.com/yaoapp/yao/agent/context"
|
|
robottypes "github.com/yaoapp/yao/agent/robot/types"
|
|
)
|
|
|
|
// RunTasks executes P2: Tasks phase
|
|
// Calls the Tasks Agent to break down goals into executable tasks
|
|
//
|
|
// Input:
|
|
// - Goals (from P1) with markdown content
|
|
// - Available resources (Agents, MCP tools, KB, DB)
|
|
//
|
|
// Output:
|
|
// - List of Task objects with executor assignments, expected outputs, and validation rules
|
|
func (e *Executor) RunTasks(ctx *robottypes.Context, exec *robottypes.Execution, _ interface{}) error {
|
|
// §18.2: confirming phase may have already populated Tasks — skip regeneration
|
|
if len(exec.Tasks) > 0 {
|
|
return nil
|
|
}
|
|
|
|
// Get robot for 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, "breaking_down_tasks"))
|
|
|
|
// Validate: Goals must exist (from P1)
|
|
if exec.Goals == nil || exec.Goals.Content == "" {
|
|
return fmt.Errorf("goals not available for task planning")
|
|
}
|
|
|
|
// Get agent ID for tasks phase (per-robot config > global Uses > empty)
|
|
agentID := robottypes.ResolvePhaseAgent(robot.Config, robottypes.PhaseTasks)
|
|
if agentID == "" {
|
|
return fmt.Errorf("no Tasks Agent configured (set uses.tasks in agent.yml or resources.phases in robot config)")
|
|
}
|
|
|
|
// Build prompt with goals and available resources
|
|
formatter := NewInputFormatter()
|
|
userContent := formatter.FormatGoals(exec.Goals, robot)
|
|
|
|
if userContent == "" {
|
|
return fmt.Errorf("tasks agent (%s) received empty input for task planning", agentID)
|
|
}
|
|
|
|
// Call agent
|
|
caller := NewAgentCaller()
|
|
caller.log = newExecLogger(robot, exec.ID)
|
|
caller.Connector = robot.LanguageModel
|
|
caller.Workspace = robot.Workspace
|
|
result, err := caller.CallWithMessages(ctx, agentID, userContent)
|
|
if err != nil {
|
|
return fmt.Errorf("tasks agent (%s) call failed: %w", agentID, err)
|
|
}
|
|
|
|
// Parse response as JSON
|
|
// Tasks Agent returns: { "tasks": [...] }
|
|
data, err := result.GetJSON()
|
|
if err != nil {
|
|
return fmt.Errorf("tasks agent (%s) returned invalid JSON: %w", agentID, err)
|
|
}
|
|
|
|
// Extract tasks array
|
|
tasksData, ok := data["tasks"].([]interface{})
|
|
if !ok || len(tasksData) == 0 {
|
|
return fmt.Errorf("tasks agent (%s) returned no tasks", agentID)
|
|
}
|
|
|
|
// Parse tasks
|
|
tasks, err := ParseTasks(tasksData)
|
|
if err != nil {
|
|
return fmt.Errorf("tasks agent (%s) returned invalid task structure: %w", agentID, err)
|
|
}
|
|
|
|
// Validate tasks
|
|
if err := ValidateTasks(tasks); err != nil {
|
|
return fmt.Errorf("tasks validation failed: %w", err)
|
|
}
|
|
|
|
exec.Tasks = tasks
|
|
|
|
// Log task overview for developer observability
|
|
el := newExecLogger(robot, exec.ID)
|
|
el.logTaskOverview(tasks)
|
|
|
|
return nil
|
|
}
|
|
|
|
// ParseTasks converts raw JSON array to []Task
|
|
// Tasks are sorted by Order field after parsing
|
|
func ParseTasks(data []interface{}) ([]robottypes.Task, error) {
|
|
tasks := make([]robottypes.Task, 0, len(data))
|
|
|
|
for i, item := range data {
|
|
taskMap, ok := item.(map[string]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("task %d is not a valid object", i)
|
|
}
|
|
|
|
task, err := ParseTask(taskMap, i)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("task %d: %w", i, err)
|
|
}
|
|
|
|
tasks = append(tasks, *task)
|
|
}
|
|
|
|
// Sort tasks by Order field to ensure correct execution sequence
|
|
SortTasksByOrder(tasks)
|
|
|
|
return tasks, nil
|
|
}
|
|
|
|
// ParseTask converts a map to Task struct
|
|
func ParseTask(data map[string]interface{}, index int) (*robottypes.Task, error) {
|
|
task := &robottypes.Task{
|
|
Status: robottypes.TaskPending,
|
|
Order: index,
|
|
}
|
|
|
|
// Required: id
|
|
if id, ok := data["id"].(string); ok && id != "" {
|
|
task.ID = id
|
|
} else {
|
|
task.ID = fmt.Sprintf("task-%03d", index+1)
|
|
}
|
|
|
|
// Required: executor_type
|
|
if execType, ok := data["executor_type"].(string); ok {
|
|
task.ExecutorType = ParseExecutorType(execType)
|
|
} else {
|
|
return nil, fmt.Errorf("missing executor_type")
|
|
}
|
|
|
|
// Required: executor_id
|
|
if execID, ok := data["executor_id"].(string); ok && execID != "" {
|
|
task.ExecutorID = execID
|
|
} else {
|
|
return nil, fmt.Errorf("missing executor_id")
|
|
}
|
|
|
|
// Optional: goal_ref
|
|
if goalRef, ok := data["goal_ref"].(string); ok {
|
|
task.GoalRef = goalRef
|
|
}
|
|
|
|
// Optional: source (default to auto)
|
|
if source, ok := data["source"].(string); ok {
|
|
task.Source = robottypes.TaskSource(source)
|
|
} else {
|
|
task.Source = robottypes.TaskSourceAuto
|
|
}
|
|
|
|
// Optional: order (override default)
|
|
if order, ok := data["order"].(float64); ok {
|
|
task.Order = int(order)
|
|
}
|
|
|
|
// Optional: messages (task instructions)
|
|
if messages, ok := data["messages"].([]interface{}); ok {
|
|
task.Messages = ParseMessages(messages)
|
|
}
|
|
|
|
// Optional: description - save to Description field and convert to message if no messages
|
|
if desc, ok := data["description"].(string); ok && desc != "" {
|
|
task.Description = desc
|
|
// Also convert to Messages for execution if no explicit messages provided
|
|
if len(task.Messages) == 0 {
|
|
task.Messages = []agentcontext.Message{
|
|
{Role: agentcontext.RoleUser, Content: desc},
|
|
}
|
|
}
|
|
}
|
|
|
|
// Optional: args
|
|
if args, ok := data["args"].([]interface{}); ok {
|
|
task.Args = make([]any, len(args))
|
|
copy(task.Args, args)
|
|
}
|
|
|
|
// MCP-specific fields (required when executor_type is "mcp")
|
|
if mcpServer, ok := data["mcp_server"].(string); ok {
|
|
task.MCPServer = mcpServer
|
|
}
|
|
if mcpTool, ok := data["mcp_tool"].(string); ok {
|
|
task.MCPTool = mcpTool
|
|
}
|
|
|
|
// Optional: expected_output (for P3 validation)
|
|
if expectedOutput, ok := data["expected_output"].(string); ok {
|
|
task.ExpectedOutput = expectedOutput
|
|
}
|
|
|
|
// Optional: validation_rules (for P3 validation)
|
|
if rules, ok := data["validation_rules"].([]interface{}); ok {
|
|
task.ValidationRules = make([]string, 0, len(rules))
|
|
for _, r := range rules {
|
|
if s, ok := r.(string); ok {
|
|
task.ValidationRules = append(task.ValidationRules, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
return task, nil
|
|
}
|
|
|
|
// ParseMessages converts raw message array to []Message
|
|
func ParseMessages(data []interface{}) []agentcontext.Message {
|
|
messages := make([]agentcontext.Message, 0, len(data))
|
|
|
|
for _, item := range data {
|
|
msgMap, ok := item.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
msg := agentcontext.Message{}
|
|
|
|
// Role
|
|
if role, ok := msgMap["role"].(string); ok {
|
|
msg.Role = agentcontext.MessageRole(role)
|
|
} else {
|
|
msg.Role = agentcontext.RoleUser
|
|
}
|
|
|
|
// Content
|
|
if content, ok := msgMap["content"].(string); ok {
|
|
msg.Content = content
|
|
} else if content, ok := msgMap["content"]; ok {
|
|
// Handle non-string content (multimodal)
|
|
msg.Content = content
|
|
}
|
|
|
|
if msg.Content != nil {
|
|
messages = append(messages, msg)
|
|
}
|
|
}
|
|
|
|
return messages
|
|
}
|
|
|
|
// ParseExecutorType converts string to ExecutorType
|
|
func ParseExecutorType(s string) robottypes.ExecutorType {
|
|
switch s {
|
|
case "agent", "assistant":
|
|
return robottypes.ExecutorAssistant
|
|
case "mcp":
|
|
return robottypes.ExecutorMCP
|
|
case "process":
|
|
return robottypes.ExecutorProcess
|
|
default:
|
|
return robottypes.ExecutorAssistant // default to assistant
|
|
}
|
|
}
|
|
|
|
// ValidateTasks validates the task list
|
|
func ValidateTasks(tasks []robottypes.Task) error {
|
|
if len(tasks) == 0 {
|
|
return fmt.Errorf("no tasks generated")
|
|
}
|
|
|
|
seenIDs := make(map[string]bool)
|
|
|
|
for i, task := range tasks {
|
|
// Check unique ID
|
|
if seenIDs[task.ID] {
|
|
return fmt.Errorf("task %d: duplicate task ID '%s'", i, task.ID)
|
|
}
|
|
seenIDs[task.ID] = true
|
|
|
|
// Check executor
|
|
if task.ExecutorID == "" {
|
|
return fmt.Errorf("task %d (%s): missing executor_id", i, task.ID)
|
|
}
|
|
|
|
// Check messages or description
|
|
if len(task.Messages) == 0 {
|
|
return fmt.Errorf("task %d (%s): missing messages or description", i, task.ID)
|
|
}
|
|
|
|
// Note: Executor existence is NOT validated here
|
|
// - ValidateExecutorExists() can be called separately if needed
|
|
// - Unknown executors will fail at P3 runtime with clear error message
|
|
// - This allows flexibility for dynamically registered executors
|
|
|
|
// Note: Validation rules are optional
|
|
// - P3 can still do basic validation without explicit rules
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateTasksWithResources validates tasks and checks executor existence
|
|
// Returns a list of warnings for unknown executors (does not fail)
|
|
func ValidateTasksWithResources(tasks []robottypes.Task, robot *robottypes.Robot) (warnings []string, err error) {
|
|
// First do basic validation
|
|
if err := ValidateTasks(tasks); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Then check executor existence (warnings only)
|
|
for _, task := range tasks {
|
|
if !ValidateExecutorExists(task.ExecutorID, task.ExecutorType, robot) {
|
|
warnings = append(warnings, fmt.Sprintf(
|
|
"task %s: executor '%s' (%s) not found in available resources",
|
|
task.ID, task.ExecutorID, task.ExecutorType,
|
|
))
|
|
}
|
|
}
|
|
|
|
return warnings, nil
|
|
}
|
|
|
|
// IsValidExecutorType checks if the executor type is valid
|
|
func IsValidExecutorType(t robottypes.ExecutorType) bool {
|
|
switch t {
|
|
case robottypes.ExecutorAssistant, robottypes.ExecutorMCP, robottypes.ExecutorProcess:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// SortTasksByOrder sorts tasks by their Order field (ascending)
|
|
// This ensures tasks are executed in the correct sequence regardless of
|
|
// the order they appear in the LLM response
|
|
func SortTasksByOrder(tasks []robottypes.Task) {
|
|
for i := 0; i < len(tasks)-1; i++ {
|
|
for j := i + 1; j < len(tasks); j++ {
|
|
if tasks[j].Order < tasks[i].Order {
|
|
tasks[i], tasks[j] = tasks[j], tasks[i]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ValidateExecutorExists checks if the executor ID exists in available resources
|
|
// This is an optional validation - tasks with unknown executors will still be created
|
|
// but may fail during P3 execution
|
|
// For MCP tasks, pass mcpServer as the second parameter (executorID is ignored for MCP)
|
|
func ValidateExecutorExists(executorID string, executorType robottypes.ExecutorType, robot *robottypes.Robot) bool {
|
|
if robot == nil || robot.Config == nil || robot.Config.Resources == nil {
|
|
return true // Skip validation if no resources configured
|
|
}
|
|
|
|
switch executorType {
|
|
case robottypes.ExecutorAssistant:
|
|
for _, agent := range robot.Config.Resources.Agents {
|
|
if agent == executorID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
|
|
case robottypes.ExecutorMCP:
|
|
// For MCP, executorID can be either:
|
|
// 1. The mcp_server value (new format)
|
|
// 2. The combined mcp_server.mcp_tool format (for display)
|
|
// We validate against mcp_server (the MCP server/client ID)
|
|
for _, mcp := range robot.Config.Resources.MCP {
|
|
if mcp.ID == executorID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
|
|
case robottypes.ExecutorProcess:
|
|
// Process executors are not validated against resources
|
|
// They are validated at runtime by the Yao process system
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// ValidateMCPTask validates MCP task fields
|
|
// Returns an error if mcp_server or mcp_tool is missing for MCP tasks
|
|
func ValidateMCPTask(task *robottypes.Task) error {
|
|
if task.ExecutorType != robottypes.ExecutorMCP {
|
|
return nil
|
|
}
|
|
if task.MCPServer == "" {
|
|
return fmt.Errorf("MCP task %s: mcp_server field is required", task.ID)
|
|
}
|
|
if task.MCPTool == "" {
|
|
return fmt.Errorf("MCP task %s: mcp_tool field is required", task.ID)
|
|
}
|
|
return nil
|
|
}
|