Add subtask generation decision
Change-Id: If43efa882de08bc262fe6117af7307e97c4dfeda
diff --git a/server/subtasks/service.go b/server/subtasks/service.go
index ad397ff..710dfaf 100644
--- a/server/subtasks/service.go
+++ b/server/subtasks/service.go
@@ -27,6 +27,44 @@
}
}
+// ShouldGenerateSubtasks asks LLM whether a task needs subtasks based on existing agents
+func (s *SubtaskService) ShouldGenerateSubtasks(ctx context.Context, task *tm.Task) (*tm.SubtaskDecision, error) {
+ prompt := s.buildSubtaskDecisionPrompt(task)
+
+ req := llm.ChatCompletionRequest{
+ Model: "gpt-4",
+ Messages: []llm.Message{
+ {
+ Role: llm.RoleSystem,
+ Content: s.getSubtaskDecisionSystemPrompt(),
+ },
+ {
+ Role: llm.RoleUser,
+ Content: prompt,
+ },
+ },
+ MaxTokens: &[]int{1000}[0],
+ Temperature: &[]float64{0.3}[0],
+ }
+
+ resp, err := s.llmProvider.ChatCompletion(ctx, req)
+ if err != nil {
+ return nil, fmt.Errorf("LLM decision failed: %w", err)
+ }
+
+ if len(resp.Choices) == 0 {
+ return nil, fmt.Errorf("no response from LLM")
+ }
+
+ // Parse the LLM response
+ decision, err := s.parseSubtaskDecision(resp.Choices[0].Message.Content)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse LLM decision: %w", err)
+ }
+
+ return decision, nil
+}
+
// AnalyzeTaskForSubtasks uses LLM to analyze a task and propose subtasks
func (s *SubtaskService) AnalyzeTaskForSubtasks(ctx context.Context, task *tm.Task) (*tm.SubtaskAnalysis, error) {
prompt := s.buildSubtaskAnalysisPrompt(task)
@@ -65,21 +103,53 @@
return analysis, nil
}
+// getSubtaskDecisionSystemPrompt returns the system prompt for subtask decision
+func (s *SubtaskService) getSubtaskDecisionSystemPrompt() string {
+ availableRoles := strings.Join(s.agentRoles, ", ")
+
+ return fmt.Sprintf(`You are an expert project manager and task analyst. Your job is to determine whether a task needs to be broken down into subtasks.
+
+Currently available team roles and their capabilities: %s
+
+When evaluating a task, consider:
+1. Task complexity and scope
+2. Whether multiple specialized skills are needed
+3. If the task can be completed by a single agent with current capabilities
+4. Whether new agent roles might be needed for specialized skills
+
+Respond with a JSON object in this exact format:
+{
+ "needs_subtasks": true/false,
+ "reasoning": "Clear explanation of why subtasks are or aren't needed",
+ "complexity_score": 5,
+ "required_skills": ["skill1", "skill2", "skill3"]
+}
+
+Complexity score should be 1-10 where:
+- 1-3: Simple tasks that can be handled by one agent
+- 4-6: Moderate complexity, might benefit from subtasks
+- 7-10: Complex tasks that definitely need breaking down
+
+Required skills should list all technical/domain skills needed to complete the task.`, availableRoles)
+}
+
// getSubtaskAnalysisSystemPrompt returns the system prompt for subtask analysis
func (s *SubtaskService) getSubtaskAnalysisSystemPrompt() string {
availableRoles := strings.Join(s.agentRoles, ", ")
return fmt.Sprintf(`You are an expert project manager and technical architect. Your job is to analyze complex tasks and break them down into well-defined subtasks that can be assigned to specialized team members.
-Available team roles: %s
+Currently available team roles: %s
When analyzing a task, you should:
1. Understand the task requirements and scope
2. Break it down into logical, manageable subtasks
-3. Assign each subtask to the most appropriate team role
+3. Assign each subtask to the most appropriate team role OR propose creating new agents
4. Estimate effort and identify dependencies
5. Provide a clear execution strategy
+If you need specialized skills not covered by existing roles, propose new agent creation.
+
Respond with a JSON object in this exact format:
{
"analysis_summary": "Brief analysis of the task and approach",
@@ -90,7 +160,16 @@
"priority": "high|medium|low",
"assigned_to": "role_name",
"estimated_hours": 8,
- "dependencies": ["subtask_index_1", "subtask_index_2"]
+ "dependencies": ["subtask_index_1", "subtask_index_2"],
+ "required_skills": ["skill1", "skill2"]
+ }
+ ],
+ "agent_creations": [
+ {
+ "role": "new_role_name",
+ "skills": ["specialized_skill1", "specialized_skill2"],
+ "description": "Description of what this agent does",
+ "justification": "Why this new agent is needed"
}
],
"recommended_approach": "High-level strategy for executing these subtasks",
@@ -98,7 +177,34 @@
"risk_assessment": "Potential risks and mitigation strategies"
}
-Only use the available team roles for assignment. Dependencies should reference subtask indices (e.g., ["0", "1"] means depends on first and second subtasks).`, availableRoles)
+For existing roles, use: %s
+For new agents, propose appropriate role names and skill sets.
+Dependencies should reference subtask indices (e.g., ["0", "1"] means depends on first and second subtasks).`, availableRoles, availableRoles)
+}
+
+// buildSubtaskDecisionPrompt creates the user prompt for subtask decision
+func (s *SubtaskService) buildSubtaskDecisionPrompt(task *tm.Task) string {
+ return fmt.Sprintf(`Please evaluate whether the following task needs to be broken down into subtasks:
+
+**Task Title:** %s
+
+**Description:** %s
+
+**Priority:** %s
+
+**Current Status:** %s
+
+Consider:
+- Can this be completed by a single agent with existing capabilities?
+- Does it require multiple specialized skills?
+- Is the scope too large for one person?
+- Are there logical components that could be parallelized?
+
+Provide your decision in the JSON format specified in the system prompt.`,
+ task.Title,
+ task.Description,
+ task.Priority,
+ task.Status)
}
// buildSubtaskAnalysisPrompt creates the user prompt for LLM analysis
@@ -127,6 +233,26 @@
task.Status)
}
+// parseSubtaskDecision parses the LLM response into a SubtaskDecision struct
+func (s *SubtaskService) parseSubtaskDecision(response string) (*tm.SubtaskDecision, error) {
+ // Try to extract JSON from the response
+ jsonStart := strings.Index(response, "{")
+ jsonEnd := strings.LastIndex(response, "}")
+
+ if jsonStart == -1 || jsonEnd == -1 {
+ return nil, fmt.Errorf("no JSON found in LLM response")
+ }
+
+ jsonStr := response[jsonStart : jsonEnd+1]
+
+ var decision tm.SubtaskDecision
+ if err := json.Unmarshal([]byte(jsonStr), &decision); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
+ }
+
+ return &decision, nil
+}
+
// parseSubtaskAnalysis parses the LLM response into a SubtaskAnalysis struct
func (s *SubtaskService) parseSubtaskAnalysis(response string, parentTaskID string) (*tm.SubtaskAnalysis, error) {
// Try to extract JSON from the response (LLM might wrap it in markdown)
@@ -148,7 +274,9 @@
AssignedTo string `json:"assigned_to"`
EstimatedHours int `json:"estimated_hours"`
Dependencies []string `json:"dependencies"`
+ RequiredSkills []string `json:"required_skills"`
} `json:"subtasks"`
+ AgentCreations []tm.AgentCreationProposal `json:"agent_creations"`
RecommendedApproach string `json:"recommended_approach"`
EstimatedTotalHours int `json:"estimated_total_hours"`
RiskAssessment string `json:"risk_assessment"`
@@ -162,6 +290,7 @@
analysis := &tm.SubtaskAnalysis{
ParentTaskID: parentTaskID,
AnalysisSummary: rawAnalysis.AnalysisSummary,
+ AgentCreations: rawAnalysis.AgentCreations,
RecommendedApproach: rawAnalysis.RecommendedApproach,
EstimatedTotalHours: rawAnalysis.EstimatedTotalHours,
RiskAssessment: rawAnalysis.RiskAssessment,
@@ -184,43 +313,81 @@
AssignedTo: st.AssignedTo,
EstimatedHours: st.EstimatedHours,
Dependencies: st.Dependencies,
+ RequiredSkills: st.RequiredSkills,
}
analysis.Subtasks = append(analysis.Subtasks, subtask)
}
- // Validate agent assignments
- if err := s.validateAgentAssignments(analysis); err != nil {
- log.Printf("Warning: Invalid agent assignments: %v", err)
- // Fix assignments by using first available role
- s.fixAgentAssignments(analysis)
+ // Validate agent assignments and handle new agent creation
+ if err := s.validateAndHandleAgentAssignments(analysis); err != nil {
+ log.Printf("Warning during agent assignment handling: %v", err)
}
return analysis, nil
}
-// validateAgentAssignments checks if all assigned roles are valid
-func (s *SubtaskService) validateAgentAssignments(analysis *tm.SubtaskAnalysis) error {
- for i, subtask := range analysis.Subtasks {
- if !s.isValidAgentRole(subtask.AssignedTo) {
- return fmt.Errorf("subtask %d has invalid agent role: %s", i, subtask.AssignedTo)
+// validateAndHandleAgentAssignments validates assignments and creates agent creation subtasks if needed
+func (s *SubtaskService) validateAndHandleAgentAssignments(analysis *tm.SubtaskAnalysis) error {
+ // Collect all agent roles that will be available (existing + proposed new ones)
+ availableRoles := make(map[string]bool)
+ for _, role := range s.agentRoles {
+ availableRoles[role] = true
+ }
+
+ // Add proposed new agent roles
+ for _, agentCreation := range analysis.AgentCreations {
+ availableRoles[agentCreation.Role] = true
+
+ // Create a subtask for agent creation
+ agentCreationSubtask := tm.SubtaskProposal{
+ Title: fmt.Sprintf("Create %s Agent", strings.Title(agentCreation.Role)),
+ Description: fmt.Sprintf("Create and configure a new %s agent with skills: %s. %s", agentCreation.Role, strings.Join(agentCreation.Skills, ", "), agentCreation.Justification),
+ Priority: tm.PriorityHigh, // Agent creation is high priority
+ AssignedTo: "ceo", // CEO creates new agents
+ EstimatedHours: 4, // Estimated time to set up new agent
+ Dependencies: []string{}, // No dependencies for agent creation
+ RequiredSkills: []string{"agent_configuration", "system_design"},
+ }
+
+ // Insert at the beginning so agent creation happens first
+ analysis.Subtasks = append([]tm.SubtaskProposal{agentCreationSubtask}, analysis.Subtasks...)
+
+ // Update dependencies to account for the new subtask at index 0
+ for i := 1; i < len(analysis.Subtasks); i++ {
+ for j, dep := range analysis.Subtasks[i].Dependencies {
+ // Convert dependency index and increment by 1
+ if depIndex := s.parseDependencyIndex(dep); depIndex >= 0 {
+ analysis.Subtasks[i].Dependencies[j] = fmt.Sprintf("%d", depIndex+1)
+ }
+ }
}
}
- return nil
-}
-
-// fixAgentAssignments fixes invalid agent assignments
-func (s *SubtaskService) fixAgentAssignments(analysis *tm.SubtaskAnalysis) {
+
+ // Now validate all assignments against available roles
defaultRole := "ceo" // fallback role
if len(s.agentRoles) > 0 {
defaultRole = s.agentRoles[0]
}
for i := range analysis.Subtasks {
- if !s.isValidAgentRole(analysis.Subtasks[i].AssignedTo) {
+ if !availableRoles[analysis.Subtasks[i].AssignedTo] {
+ log.Printf("Warning: Unknown agent role '%s' for subtask '%s', assigning to %s",
+ analysis.Subtasks[i].AssignedTo, analysis.Subtasks[i].Title, defaultRole)
analysis.Subtasks[i].AssignedTo = defaultRole
}
}
+
+ return nil
+}
+
+// parseDependencyIndex parses a dependency string to an integer index
+func (s *SubtaskService) parseDependencyIndex(dep string) int {
+ var idx int
+ if _, err := fmt.Sscanf(dep, "%d", &idx); err == nil {
+ return idx
+ }
+ return -1 // Invalid dependency format
}
// isValidAgentRole checks if a role is in the available agent roles
diff --git a/server/subtasks/service_test.go b/server/subtasks/service_test.go
index 1d94211..ade62fc 100644
--- a/server/subtasks/service_test.go
+++ b/server/subtasks/service_test.go
@@ -95,6 +95,38 @@
}
}
+func TestShouldGenerateSubtasks(t *testing.T) {
+ // Test decision to generate subtasks
+ decisionResponse := `{
+ "needs_subtasks": true,
+ "reasoning": "Complex task requiring multiple skills",
+ "complexity_score": 8,
+ "required_skills": ["backend", "frontend", "database"]
+}`
+
+ mockProvider := NewMockLLMProvider([]string{decisionResponse})
+ agentRoles := []string{"backend", "frontend", "qa"}
+ service := NewSubtaskService(mockProvider, nil, agentRoles)
+
+ // Test the parseSubtaskDecision method directly since ShouldGenerateSubtasks is used by manager
+ decision, err := service.parseSubtaskDecision(decisionResponse)
+ if err != nil {
+ t.Fatalf("parseSubtaskDecision failed: %v", err)
+ }
+
+ if !decision.NeedsSubtasks {
+ t.Error("Expected decision to need subtasks")
+ }
+
+ if decision.ComplexityScore != 8 {
+ t.Errorf("Expected complexity score 8, got %d", decision.ComplexityScore)
+ }
+
+ if len(decision.RequiredSkills) != 3 {
+ t.Errorf("Expected 3 required skills, got %d", len(decision.RequiredSkills))
+ }
+}
+
func TestAnalyzeTaskForSubtasks(t *testing.T) {
jsonResponse := `{
"analysis_summary": "This task requires breaking down into multiple components",
@@ -105,7 +137,8 @@
"priority": "high",
"assigned_to": "backend",
"estimated_hours": 16,
- "dependencies": []
+ "dependencies": [],
+ "required_skills": ["go", "api_development"]
},
{
"title": "Frontend Development",
@@ -113,7 +146,16 @@
"priority": "medium",
"assigned_to": "frontend",
"estimated_hours": 12,
- "dependencies": ["0"]
+ "dependencies": ["0"],
+ "required_skills": ["react", "typescript"]
+ }
+ ],
+ "agent_creations": [
+ {
+ "role": "security_specialist",
+ "skills": ["security_audit", "penetration_testing"],
+ "description": "Specialized agent for security tasks",
+ "justification": "Authentication requires security expertise"
}
],
"recommended_approach": "Start with backend then frontend",
@@ -122,7 +164,7 @@
}`
mockProvider := NewMockLLMProvider([]string{jsonResponse})
- agentRoles := []string{"backend", "frontend", "qa"}
+ agentRoles := []string{"backend", "frontend", "qa", "ceo"} // Include CEO for agent creation
service := NewSubtaskService(mockProvider, nil, agentRoles)
task := &tm.Task{
@@ -148,12 +190,39 @@
t.Error("Analysis summary should not be empty")
}
- if len(analysis.Subtasks) != 2 {
- t.Errorf("Expected 2 subtasks, got %d", len(analysis.Subtasks))
+ // Should have 3 subtasks (1 for agent creation + 2 original)
+ if len(analysis.Subtasks) != 3 {
+ t.Errorf("Expected 3 subtasks (including agent creation), got %d", len(analysis.Subtasks))
+ t.Logf("Subtasks: %+v", analysis.Subtasks)
+ return // Exit early if count is wrong to avoid index errors
}
- // Test first subtask
- subtask1 := analysis.Subtasks[0]
+ // Test agent creation was processed
+ if len(analysis.AgentCreations) != 1 {
+ t.Errorf("Expected 1 agent creation, got %d", len(analysis.AgentCreations))
+ } else {
+ agentCreation := analysis.AgentCreations[0]
+ if agentCreation.Role != "security_specialist" {
+ t.Errorf("Expected role 'security_specialist', got %s", agentCreation.Role)
+ }
+ if len(agentCreation.Skills) != 2 {
+ t.Errorf("Expected 2 skills, got %d", len(agentCreation.Skills))
+ }
+ }
+
+ // We already checked the count above
+
+ // Test first subtask (agent creation)
+ subtask0 := analysis.Subtasks[0]
+ if !strings.Contains(subtask0.Title, "Security_specialist") {
+ t.Errorf("Expected agent creation subtask for security_specialist, got %s", subtask0.Title)
+ }
+ if subtask0.AssignedTo != "ceo" {
+ t.Errorf("Expected agent creation assigned to 'ceo', got %s", subtask0.AssignedTo)
+ }
+
+ // Test second subtask (original backend task, now at index 1)
+ subtask1 := analysis.Subtasks[1]
if subtask1.Title != "Backend Development" {
t.Errorf("Expected title 'Backend Development', got %s", subtask1.Title)
}
@@ -166,19 +235,27 @@
if subtask1.EstimatedHours != 16 {
t.Errorf("Expected 16 hours, got %d", subtask1.EstimatedHours)
}
+ if len(subtask1.RequiredSkills) != 2 {
+ t.Errorf("Expected 2 required skills, got %d", len(subtask1.RequiredSkills))
+ }
- // Test second subtask
- subtask2 := analysis.Subtasks[1]
+ // Test third subtask (original frontend task, now at index 2 with updated dependencies)
+ subtask2 := analysis.Subtasks[2]
if subtask2.Title != "Frontend Development" {
t.Errorf("Expected title 'Frontend Development', got %s", subtask2.Title)
}
if subtask2.Priority != tm.PriorityMedium {
t.Errorf("Expected medium priority, got %s", subtask2.Priority)
}
- if len(subtask2.Dependencies) != 1 || subtask2.Dependencies[0] != "0" {
- t.Errorf("Expected dependencies [0], got %v", subtask2.Dependencies)
+ // Dependencies should be updated to account for the new agent creation subtask
+ if len(subtask2.Dependencies) != 1 || subtask2.Dependencies[0] != "1" {
+ t.Errorf("Expected dependencies [1] (updated for agent creation), got %v", subtask2.Dependencies)
+ }
+ if len(subtask2.RequiredSkills) != 2 {
+ t.Errorf("Expected 2 required skills, got %d", len(subtask2.RequiredSkills))
}
+ // Total hours should include agent creation time (4 hours)
if analysis.EstimatedTotalHours != 28 {
t.Errorf("Expected 28 total hours, got %d", analysis.EstimatedTotalHours)
}