package executor import ( "context" "encoding/json" "fmt" "os/exec" "strings" ) type Classification struct { AgentType string `json:"agent_type"` Model string `json:"model"` Reason string `json:"reason"` } type SystemStatus struct { ActiveTasks map[string]int RateLimited map[string]bool } type Classifier struct { GeminiBinaryPath string } const classificationPrompt = ` You are a task classifier for Claudomator. Given a task description and system status, select the best agent (claude or gemini) and model to use. Agent Types: - claude: Best for complex coding, reasoning, and tool use. - gemini: Best for large context, fast reasoning, and multimodal tasks. Available Models: Claude: - claude-3-5-sonnet-latest (balanced) - claude-3-5-sonnet-20241022 (stable) - claude-3-opus-20240229 (most powerful, expensive) - claude-3-5-haiku-20241022 (fast, cheap) Gemini: - gemini-2.5-flash-lite (fastest, most efficient, best for simple tasks) - gemini-3-flash-preview (fast, multimodal) - gemini-1.5-flash (fast, balanced) - gemini-1.5-pro (more powerful, larger context) Selection Criteria: - Agent: Prefer the one with least running tasks and no active rate limit. - Model: Select based on task complexity. Use powerful models (opus, pro, pro-preview) for complex reasoning/coding, flash-lite/flash/haiku for simple tasks. Task: Name: %s Instructions: %s System Status: %s Respond with ONLY a JSON object: { "agent_type": "claude" | "gemini", "model": "model-name", "reason": "brief reason" } ` func (c *Classifier) Classify(ctx context.Context, taskName, instructions string, status SystemStatus) (*Classification, error) { statusStr := "" for agent, active := range status.ActiveTasks { statusStr += fmt.Sprintf("- Agent %s: %d active tasks, Rate Limited: %t\n", agent, active, status.RateLimited[agent]) } prompt := fmt.Sprintf(classificationPrompt, taskName, instructions, statusStr, ) binary := c.GeminiBinaryPath if binary == "" { binary = "gemini" } // Use a minimal model for classification to be fast and cheap. args := []string{ "--prompt", prompt, "--model", "gemini-2.5-flash-lite", "--output-format", "json", } cmd := exec.CommandContext(ctx, binary, args...) out, err := cmd.Output() if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { return nil, fmt.Errorf("classifier failed (%v): %s", err, string(exitErr.Stderr)) } return nil, fmt.Errorf("classifier failed: %w", err) } // 1. Parse the JSON envelope from the gemini CLI. var cliOut struct { Response string `json:"response"` } if err := json.Unmarshal(out, &cliOut); err != nil { // If it's not JSON, it might be raw text (though we requested JSON). // This can happen if the CLI prints "Loaded cached credentials" or other info. cliOut.Response = string(out) } // 2. Extract the model response from the "response" field if present. // If it was already raw text, cliOut.Response will have it. cleanOut := strings.TrimSpace(cliOut.Response) // 3. Clean up "Loaded cached credentials" or other noise that might be in the string // if we fell back to string(out). if strings.Contains(cleanOut, "Loaded cached credentials.") { lines := strings.Split(cleanOut, "\n") var modelLines []string for _, line := range lines { if !strings.Contains(line, "Loaded cached credentials.") { modelLines = append(modelLines, line) } } cleanOut = strings.TrimSpace(strings.Join(modelLines, "\n")) } // 4. Gemini might wrap the JSON in markdown code blocks. cleanOut = strings.TrimPrefix(cleanOut, "```json") cleanOut = strings.TrimPrefix(cleanOut, "```") // fallback cleanOut = strings.TrimSuffix(cleanOut, "```") cleanOut = strings.TrimSpace(cleanOut) var cls Classification if err := json.Unmarshal([]byte(cleanOut), &cls); err != nil { return nil, fmt.Errorf("failed to parse classification JSON: %w\nOriginal Output: %s\nCleaned Output: %s", err, string(out), cleanOut) } return &cls, nil }