blob: 8e70230aab2f31674c19811ae5c67cc717c5e8a0 [file] [log] [blame]
iomodoa53240a2025-07-30 17:33:35 +04001package agent
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "log/slog"
8 "strings"
9
10 "github.com/iomodo/staff/llm"
11 "github.com/iomodo/staff/tm"
12 "golang.org/x/text/cases"
13 "golang.org/x/text/language"
14)
15
16type Thinker struct {
17 roles []string
18 llmProvider llm.LLMProvider
19 model string // TODO: abstract away in llmProvider
20 systemPrompt string // TODO abstract away in llmProvider
21 maxTokens int
22 temperature float64
23 logger *slog.Logger
24}
25
26func NewThinker(llmProvider llm.LLMProvider, model string, systemPrompt string, maxTokens int, temperature float64, roles []string, logger *slog.Logger) *Thinker {
27 return &Thinker{llmProvider: llmProvider, model: model, maxTokens: maxTokens, temperature: temperature, roles: roles, logger: logger}
28}
29
30// shouldGenerateSubtasks determines if a task should be broken down into subtasks using LLM
31func (t *Thinker) ShouldGenerateSubtasks(task *tm.Task) bool {
32 // Don't generate subtasks for subtasks
33 if task.ParentTaskID != "" {
34 return false
35 }
36
37 // Don't generate if already evaluated
38 if task.SubtasksEvaluated {
39 return false
40 }
41
42 // Ask LLM to decide
43 ctx := context.Background()
44 decision, err := t.shouldGenerateSubtasks(ctx, task)
45 if err != nil {
46 t.logger.Warn("Failed to get LLM subtask decision for task",
47 slog.String("task_id", task.ID),
48 slog.String("error", err.Error()))
49 // Fallback to simple heuristics
50 return task.Priority == tm.PriorityHigh || len(task.Description) > 200
51 }
52
53 task.SubtasksEvaluated = true
54 t.logger.Info("LLM subtask decision for task",
55 slog.String("task_id", task.ID),
56 slog.Bool("needs_subtasks", decision.NeedsSubtasks),
57 slog.Int("complexity_score", decision.ComplexityScore),
58 slog.String("reasoning", decision.Reasoning))
59
60 return decision.NeedsSubtasks
61}
62
63// AnalyzeTaskForSubtasks uses LLM to analyze a task and propose subtasks
64func (t *Thinker) GenerateSubtasksForTask(ctx context.Context, task *tm.Task) (*tm.SubtaskAnalysis, error) {
65 prompt := buildSubtaskAnalysisPrompt(task)
66
67 req := llm.ChatCompletionRequest{
68 Model: t.model,
69 Messages: []llm.Message{
70 {
71 Role: llm.RoleSystem,
72 Content: getSubtaskAnalysisSystemPrompt(t.roles),
73 },
74 {
75 Role: llm.RoleUser,
76 Content: prompt,
77 },
78 },
79 MaxTokens: &[]int{4000}[0],
80 Temperature: &[]float64{0.3}[0],
81 }
82
83 resp, err := t.llmProvider.ChatCompletion(ctx, req)
84 if err != nil {
85 return nil, fmt.Errorf("LLM analysis failed: %w", err)
86 }
87
88 if len(resp.Choices) == 0 {
89 return nil, fmt.Errorf("no response from LLM")
90 }
91
92 // Parse the LLM response
93 analysis, err := parseSubtaskAnalysis(resp.Choices[0].Message.Content, task.ID, t.roles, t.logger)
94 if err != nil {
95 return nil, fmt.Errorf("failed to parse LLM response: %w", err)
96 }
97
98 return analysis, nil
99}
100
101// generateSolution uses the agent's LLM to generate a solution
102func (t *Thinker) GenerateSolution(ctx context.Context, task *tm.Task) (string, error) {
103 prompt := buildTaskPrompt(task)
104
105 req := llm.ChatCompletionRequest{
106 Model: t.model,
107 Messages: []llm.Message{
108 {
109 Role: llm.RoleSystem,
110 Content: t.systemPrompt,
111 },
112 {
113 Role: llm.RoleUser,
114 Content: prompt,
115 },
116 },
117 MaxTokens: &t.maxTokens,
118 Temperature: &t.temperature,
119 }
120
121 resp, err := t.llmProvider.ChatCompletion(ctx, req)
122 if err != nil {
123 return "", fmt.Errorf("LLM request failed: %w", err)
124 }
125
126 if len(resp.Choices) == 0 {
127 return "", fmt.Errorf("no response from LLM")
128 }
129
130 return resp.Choices[0].Message.Content, nil
131}
132
133// ShouldGenerateSubtasks asks LLM whether a task needs subtasks based on existing agents
134func (t *Thinker) shouldGenerateSubtasks(ctx context.Context, task *tm.Task) (*tm.SubtaskDecision, error) {
135 prompt := buildSubtaskDecisionPrompt(task)
136
137 req := llm.ChatCompletionRequest{
138 Model: t.model,
139 Messages: []llm.Message{
140 {
141 Role: llm.RoleSystem,
142 Content: getSubtaskDecisionSystemPrompt(t.roles),
143 },
144 {
145 Role: llm.RoleUser,
146 Content: prompt,
147 },
148 },
149 MaxTokens: &[]int{1000}[0],
150 Temperature: &[]float64{0.3}[0],
151 }
152
153 resp, err := t.llmProvider.ChatCompletion(ctx, req)
154 if err != nil {
155 return nil, fmt.Errorf("LLM decision failed: %w", err)
156 }
157
158 if len(resp.Choices) == 0 {
159 return nil, fmt.Errorf("no response from LLM")
160 }
161
162 // Parse the LLM response
163 decision, err := parseSubtaskDecision(resp.Choices[0].Message.Content)
164 if err != nil {
165 return nil, fmt.Errorf("failed to parse LLM decision: %w", err)
166 }
167
168 return decision, nil
169}
170
171func buildSubtaskDecisionPrompt(task *tm.Task) string {
172 return fmt.Sprintf(`Please evaluate whether the following task needs to be broken down into subtasks:
173
174**Task Title:** %s
175
176**Description:** %s
177
178**Priority:** %s
179
180**Current Status:** %s
181
182Consider:
183- Can this be completed by a single agent with existing capabilities?
184- Does it require multiple specialized skills?
185- Is the scope too large for one person?
186- Are there logical components that could be parallelized?
187
188Provide your decision in the JSON format specified in the system prompt.`,
189 task.Title,
190 task.Description,
191 task.Priority,
192 task.Status)
193}
194
195func getSubtaskDecisionSystemPrompt(roles []string) string {
196 availableRoles := strings.Join(roles, ", ")
197
198 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.
199
200Currently available team roles and their capabilities: %s
201
202When evaluating a task, consider:
2031. Task complexity and scope
2042. Whether multiple specialized skills are needed
2053. If the task can be completed by a single agent with current capabilities
2064. Whether new agent roles might be needed for specialized skills
207
208Respond with a JSON object in this exact format:
209{
210 "needs_subtasks": true/false,
211 "reasoning": "Clear explanation of why subtasks are or aren't needed",
212 "complexity_score": 5,
213 "required_skills": ["skill1", "skill2", "skill3"]
214}
215
216Complexity score should be 1-10 where:
217- 1-3: Simple tasks that can be handled by one agent
218- 4-6: Moderate complexity, might benefit from subtasks
219- 7-10: Complex tasks that definitely need breaking down
220
221Required skills should list all technical/domain skills needed to complete the task.`, availableRoles)
222}
223
224func parseSubtaskDecision(response string) (*tm.SubtaskDecision, error) {
225 // Try to extract JSON from the response
226 jsonStart := strings.Index(response, "{")
227 jsonEnd := strings.LastIndex(response, "}")
228
229 if jsonStart == -1 || jsonEnd == -1 {
230 return nil, fmt.Errorf("no JSON found in LLM response")
231 }
232
233 jsonStr := response[jsonStart : jsonEnd+1]
234
235 var decision tm.SubtaskDecision
236 if err := json.Unmarshal([]byte(jsonStr), &decision); err != nil {
237 return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
238 }
239
240 return &decision, nil
241}
242
243func buildSubtaskAnalysisPrompt(task *tm.Task) string {
244 return fmt.Sprintf(`Please analyze the following task and break it down into subtasks:
245
246**Task Title:** %s
247
248**Description:** %s
249
250**Priority:** %s
251
252**Current Status:** %s
253
254Please analyze this task and provide a detailed breakdown into subtasks. Consider:
255- Technical complexity and requirements
256- Logical task dependencies
257- Appropriate skill sets needed for each subtask
258- Risk factors and potential blockers
259- Estimated effort for each component
260
261Provide the analysis in the JSON format specified in the system prompt.`,
262 task.Title,
263 task.Description,
264 task.Priority,
265 task.Status)
266}
267
268func getSubtaskAnalysisSystemPrompt(roles []string) string {
269 availableRoles := strings.Join(roles, ", ")
270
271 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.
272
273Currently available team roles: %s
274
275When analyzing a task, you should:
2761. Understand the task requirements and scope
2772. Break it down into logical, manageable subtasks
2783. Assign each subtask to the most appropriate team role OR propose creating new agents
2794. Estimate effort and identify dependencies
2805. Provide a clear execution strategy
281
282If you need specialized skills not covered by existing roles, propose new agent creation.
283
284Respond with a JSON object in this exact format:
285{
286 "analysis_summary": "Brief analysis of the task and approach",
287 "subtasks": [
288 {
289 "title": "Subtask title",
290 "description": "Detailed description of what needs to be done",
291 "priority": "high|medium|low",
292 "assigned_to": "role_name",
293 "estimated_hours": 8,
294 "dependencies": ["subtask_index_1", "subtask_index_2"],
295 "required_skills": ["skill1", "skill2"]
296 }
297 ],
298 "agent_creations": [
299 {
300 "role": "new_role_name",
301 "skills": ["specialized_skill1", "specialized_skill2"],
302 "description": "Description of what this agent does",
303 "justification": "Why this new agent is needed"
304 }
305 ],
306 "recommended_approach": "High-level strategy for executing these subtasks",
307 "estimated_total_hours": 40,
308 "risk_assessment": "Potential risks and mitigation strategies"
309}
310
311For existing roles, use: %s
312For new agents, propose appropriate role names and skill sets.
313Dependencies should reference subtask indices (e.g., ["0", "1"] means depends on first and second subtasks).`, availableRoles, availableRoles)
314}
315
316func parseSubtaskAnalysis(response string, parentTaskID string, agentRoles []string, logger *slog.Logger) (*tm.SubtaskAnalysis, error) {
317 // Try to extract JSON from the response (LLM might wrap it in markdown)
318 jsonStart := strings.Index(response, "{")
319 jsonEnd := strings.LastIndex(response, "}")
320
321 if jsonStart == -1 || jsonEnd == -1 {
322 return nil, fmt.Errorf("no JSON found in LLM response")
323 }
324
325 jsonStr := response[jsonStart : jsonEnd+1]
326
327 var rawAnalysis struct {
328 AnalysisSummary string `json:"analysis_summary"`
329 Subtasks []struct {
330 Title string `json:"title"`
331 Description string `json:"description"`
332 Priority string `json:"priority"`
333 AssignedTo string `json:"assigned_to"`
334 EstimatedHours int `json:"estimated_hours"`
335 Dependencies []string `json:"dependencies"`
336 RequiredSkills []string `json:"required_skills"`
337 } `json:"subtasks"`
338 AgentCreations []tm.AgentCreationProposal `json:"agent_creations"`
339 RecommendedApproach string `json:"recommended_approach"`
340 EstimatedTotalHours int `json:"estimated_total_hours"`
341 RiskAssessment string `json:"risk_assessment"`
342 }
343
344 if err := json.Unmarshal([]byte(jsonStr), &rawAnalysis); err != nil {
345 return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
346 }
347
348 // Convert to our types
349 analysis := &tm.SubtaskAnalysis{
350 ParentTaskID: parentTaskID,
351 AnalysisSummary: rawAnalysis.AnalysisSummary,
352 AgentCreations: rawAnalysis.AgentCreations,
353 RecommendedApproach: rawAnalysis.RecommendedApproach,
354 EstimatedTotalHours: rawAnalysis.EstimatedTotalHours,
355 RiskAssessment: rawAnalysis.RiskAssessment,
356 }
357
358 // Convert subtasks
359 for _, st := range rawAnalysis.Subtasks {
360 priority := tm.PriorityMedium // default
361 switch strings.ToLower(st.Priority) {
362 case "high":
363 priority = tm.PriorityHigh
364 case "low":
365 priority = tm.PriorityLow
366 }
367
368 subtask := tm.SubtaskProposal{
369 Title: st.Title,
370 Description: st.Description,
371 Priority: priority,
372 AssignedTo: st.AssignedTo,
373 EstimatedHours: st.EstimatedHours,
374 Dependencies: st.Dependencies,
375 RequiredSkills: st.RequiredSkills,
376 }
377
378 analysis.Subtasks = append(analysis.Subtasks, subtask)
379 }
380
381 // Validate agent assignments and handle new agent creation
382 if err := validateAndHandleAgentAssignments(analysis, agentRoles, logger); err != nil {
383 logger.Warn("Warning during agent assignment handling", slog.String("error", err.Error()))
384 }
385
386 return analysis, nil
387}
388
389func validateAndHandleAgentAssignments(analysis *tm.SubtaskAnalysis, agentRoles []string, logger *slog.Logger) error {
390 // Collect all agent roles that will be available (existing + proposed new ones)
391 availableRoles := make(map[string]bool)
392 for _, role := range agentRoles {
393 availableRoles[role] = true
394 }
395
396 // Add proposed new agent roles
397 for _, agentCreation := range analysis.AgentCreations {
398 availableRoles[agentCreation.Role] = true
399
400 // Create a subtask for agent creation
401 agentCreationSubtask := tm.SubtaskProposal{
402 Title: fmt.Sprintf("Create %s Agent", cases.Title(language.English).String(agentCreation.Role)),
403 Description: fmt.Sprintf("Create and configure a new %s agent with skills: %s. %s", agentCreation.Role, strings.Join(agentCreation.Skills, ", "), agentCreation.Justification),
404 Priority: tm.PriorityHigh, // Agent creation is high priority
405 AssignedTo: "ceo", // CEO creates new agents
406 EstimatedHours: 4, // Estimated time to set up new agent
407 Dependencies: []string{}, // No dependencies for agent creation
408 RequiredSkills: []string{"agent_configuration", "system_design"},
409 }
410
411 // Insert at the beginning so agent creation happens first
412 analysis.Subtasks = append([]tm.SubtaskProposal{agentCreationSubtask}, analysis.Subtasks...)
413
414 // Update dependencies to account for the new subtask at index 0
415 for i := 1; i < len(analysis.Subtasks); i++ {
416 for j, dep := range analysis.Subtasks[i].Dependencies {
417 // Convert dependency index and increment by 1
418 if depIndex := parseDependencyIndex(dep); depIndex >= 0 {
419 analysis.Subtasks[i].Dependencies[j] = fmt.Sprintf("%d", depIndex+1)
420 }
421 }
422 }
423 }
424
425 // Now validate all assignments against available roles
426 defaultRole := "ceo" // fallback role
427 if len(agentRoles) > 0 {
428 defaultRole = agentRoles[0]
429 }
430
431 for i := range analysis.Subtasks {
432 if !availableRoles[analysis.Subtasks[i].AssignedTo] {
433 logger.Warn("Unknown agent role for subtask, using default",
434 slog.String("unknown_role", analysis.Subtasks[i].AssignedTo),
435 slog.String("subtask_title", analysis.Subtasks[i].Title),
436 slog.String("assigned_role", defaultRole))
437 analysis.Subtasks[i].AssignedTo = defaultRole
438 }
439 }
440
441 return nil
442}
443
444func parseDependencyIndex(dep string) int {
445 var idx int
446 if _, err := fmt.Sscanf(dep, "%d", &idx); err == nil {
447 return idx
448 }
449 return -1 // Invalid dependency format
450}
451
452func generateSubtaskPRContent(analysis *tm.SubtaskAnalysis) string {
453 var content strings.Builder
454
455 content.WriteString(fmt.Sprintf("# Subtasks Created for Task %s\n\n", analysis.ParentTaskID))
456 content.WriteString(fmt.Sprintf("This PR creates **%d individual task files** in `/operations/tasks/` ready for agent assignment.\n\n", len(analysis.Subtasks)))
457 content.WriteString(fmt.Sprintf("✅ **Parent task `%s` has been marked as completed** - the complex task has been successfully broken down into actionable subtasks.\n\n", analysis.ParentTaskID))
458 content.WriteString(fmt.Sprintf("## Analysis Summary\n%s\n\n", analysis.AnalysisSummary))
459 content.WriteString(fmt.Sprintf("## Recommended Approach\n%s\n\n", analysis.RecommendedApproach))
460 content.WriteString(fmt.Sprintf("**Estimated Total Hours:** %d\n\n", analysis.EstimatedTotalHours))
461
462 // List the created task files
463 content.WriteString("## Created Task Files\n\n")
464 for i, subtask := range analysis.Subtasks {
465 taskID := fmt.Sprintf("%s-subtask-%d", analysis.ParentTaskID, i+1)
466 content.WriteString(fmt.Sprintf("### %d. `%s.md`\n", i+1, taskID))
467 content.WriteString(fmt.Sprintf("- **Title:** %s\n", subtask.Title))
468 content.WriteString(fmt.Sprintf("- **Assigned to:** %s\n", subtask.AssignedTo))
469 content.WriteString(fmt.Sprintf("- **Priority:** %s\n", subtask.Priority))
470 content.WriteString(fmt.Sprintf("- **Estimated Hours:** %d\n", subtask.EstimatedHours))
471 content.WriteString(fmt.Sprintf("- **Description:** %s\n\n", subtask.Description))
472 }
473
474 if analysis.RiskAssessment != "" {
475 content.WriteString(fmt.Sprintf("## Risk Assessment\n%s\n\n", analysis.RiskAssessment))
476 }
477
478 content.WriteString("## Proposed Subtasks\n\n")
479
480 for i, subtask := range analysis.Subtasks {
481 content.WriteString(fmt.Sprintf("### %d. %s\n", i+1, subtask.Title))
482 content.WriteString(fmt.Sprintf("- **Assigned to:** %s\n", subtask.AssignedTo))
483 content.WriteString(fmt.Sprintf("- **Priority:** %s\n", subtask.Priority))
484 content.WriteString(fmt.Sprintf("- **Estimated Hours:** %d\n", subtask.EstimatedHours))
485
486 if len(subtask.Dependencies) > 0 {
487 deps := strings.Join(subtask.Dependencies, ", ")
488 content.WriteString(fmt.Sprintf("- **Dependencies:** %s\n", deps))
489 }
490
491 content.WriteString(fmt.Sprintf("- **Description:** %s\n\n", subtask.Description))
492 }
493
494 content.WriteString("---\n")
495 content.WriteString("*Generated by Staff AI Agent System*\n\n")
496 content.WriteString("**Instructions:**\n")
497 content.WriteString("- Review the proposed subtasks\n")
498 content.WriteString("- Approve or request changes\n")
499 content.WriteString("- When merged, the subtasks will be automatically created and assigned\n")
500
501 return content.String()
502}
503
504// buildTaskPrompt creates a detailed prompt for the LLM
505func buildTaskPrompt(task *tm.Task) string {
506 return fmt.Sprintf(`Task: %s
507
508Priority: %s
509Description: %s
510
511Please provide a complete solution for this task. Include:
5121. Detailed implementation plan
5132. Code changes needed (if applicable)
5143. Files to be created or modified
5154. Testing considerations
5165. Any dependencies or prerequisites
517
518Your response should be comprehensive and actionable.`,
519 task.Title,
520 task.Priority,
521 task.Description)
522}