package agent import ( "fmt" "os" "os/exec" "path/filepath" "strings" "time" "ai-agent/internal/llm" "ai-agent/internal/memory" ) const systemTemplate = `You are a helpful personal assistant running locally on the user's machine. You have access to tools via MCP servers. You MUST use tools to accomplish tasks — do not guess or make up answers when a tool can provide the real information. %s Current date: %s %s%s %s%s%s ## Available Tools %s ## Guidelines - **ALWAYS use your tools** when the user asks you to read, explore, search, or modify files. You have filesystem tools — use them. - When the user says "read this codebase" or similar, use list/read tools starting from the working directory shown above. - Be concise and direct in your responses. - When a tool call fails, explain what happened and suggest alternatives. - For multi-step tasks, explain your plan briefly before executing. - Format responses in markdown when it improves readability. - If you're unsure about something, say so rather than guessing. - Never fabricate tool results — always call the actual tool. - Do NOT claim you cannot access files or the filesystem. You have tools for that — use them. %s` const smallModelTemplate = `You are a local AI assistant. Use tools to read/write files and run commands. %sDate: %s %s%s %s ## Tools %s Guidelines: - Be concise and direct - Use tools when needed to complete tasks - If a tool fails, continue with available information - Don't guess - use tools to verify - You can complete tasks even if some tools fail %s` func isSmallModel(modelName string) bool { lower := strings.ToLower(modelName) if strings.Contains(lower, "0.8b") || strings.Contains(lower, "1b") || strings.Contains(lower, "2b") { return true } return false } func buildSystemPrompt(modePrefix string, tools []llm.ToolDef, skillContent, loadedContext string, memStore *memory.Store, iceContext, workDir, ignoreContent string) string { return buildSystemPromptForModel(modePrefix, tools, skillContent, loadedContext, memStore, iceContext, workDir, ignoreContent, "") } func buildSystemPromptForModel(modePrefix string, tools []llm.ToolDef, skillContent, loadedContext string, memStore *memory.Store, iceContext, workDir, ignoreContent string, modelName string) string { useSmallModel := isSmallModel(modelName) var toolList string if len(tools) == 0 { toolList = "No tools currently available.\n" } else if useSmallModel { toolList = simplifyToolsForSmallModel(tools) } else { var b strings.Builder for _, t := range tools { fmt.Fprintf(&b, "- **%s**: %s\n", t.Name, t.Description) } toolList = b.String() } envSection := buildEnvironmentSection(workDir) var skillSection string if skillContent != "" { skillSection = fmt.Sprintf("\n## Active Skills\n%s\n", skillContent) } var ctxSection string if loadedContext != "" { ctxSection = fmt.Sprintf("\n## Loaded Context\n%s\n", loadedContext) } var memorySection string if iceContext != "" { memorySection = iceContext } else if memStore != nil { memorySection = buildMemorySection(memStore) } var memoryGuidelines string if memStore != nil { memoryGuidelines = ` ## Memory Guidelines - You have access to persistent memory via memory_save and memory_recall tools. - Proactively save important user preferences, project facts, and key decisions. - When the user shares personal information (name, preferences, etc.), save it. - Use memory_recall to look up previously saved information when relevant. - Don't save trivial or session-specific information. ` } var ignoreSection string if ignoreContent != "" { ignoreSection = fmt.Sprintf("\n## Ignored Paths\nThe following paths/patterns should be excluded from file operations:\n%s\n", ignoreContent) } var modePrefixSection string if modePrefix != "" { modePrefixSection = "\n" + modePrefix + "\n" } dateStr := time.Now().Format("Monday, January 2, 2006") if useSmallModel { return fmt.Sprintf(smallModelTemplate, modePrefixSection, dateStr, envSection, ignoreSection, skillSection, toolList, memoryGuidelines, ) } return fmt.Sprintf(systemTemplate, modePrefixSection, dateStr, envSection, ignoreSection, skillSection, ctxSection, memorySection, toolList, memoryGuidelines, ) } func simplifyToolsForSmallModel(tools []llm.ToolDef) string { var b strings.Builder for _, t := range tools { desc := t.Description if len(desc) > 50 { desc = desc[:47] + "..." } fmt.Fprintf(&b, "- %s: %s\n", t.Name, desc) } return b.String() } func buildEnvironmentSection(workDir string) string { if workDir == "" { return "" } var b strings.Builder b.WriteString("\n## Environment\n") b.WriteString(fmt.Sprintf("Working directory: %s\n", workDir)) if info := detectProjectInfo(workDir); info != "" { b.WriteString(info) } if gitInfo := detectGitInfo(workDir); gitInfo != "" { b.WriteString(gitInfo) } return b.String() } func detectProjectInfo(workDir string) string { markers := []struct { file string desc string }{ {"go.mod", "Go module"}, {"package.json", "Node.js/JavaScript"}, {"Cargo.toml", "Rust"}, {"pyproject.toml", "Python"}, {"setup.py", "Python"}, {"Makefile", ""}, {"Taskfile.yml", ""}, } var found []string for _, m := range markers { if _, err := os.Stat(filepath.Join(workDir, m.file)); err == nil { if m.desc != "" { found = append(found, fmt.Sprintf("%s (%s)", m.file, m.desc)) } else { found = append(found, m.file) } } } if len(found) == 0 { return "" } return fmt.Sprintf("Project markers: %s\n", strings.Join(found, ", ")) } func detectGitInfo(workDir string) string { gitDir := filepath.Join(workDir, ".git") if _, err := os.Stat(gitDir); err != nil { return "" } var b strings.Builder branch := runGitCommand(workDir, "rev-parse", "--abbrev-ref", "HEAD") if branch != "" { b.WriteString(fmt.Sprintf("Git branch: %s\n", branch)) } status := runGitCommand(workDir, "status", "--porcelain") if status != "" { lines := strings.Split(strings.TrimSpace(status), "\n") var modified, added, deleted int for _, line := range lines { if len(line) >= 2 { switch line[0] { case 'M', 'm': modified++ case 'A': added++ case 'D': deleted++ } } } if modified > 0 || added > 0 || deleted > 0 { statusParts := []string{} if modified > 0 { statusParts = append(statusParts, fmt.Sprintf("%d modified", modified)) } if added > 0 { statusParts = append(statusParts, fmt.Sprintf("%d added", added)) } if deleted > 0 { statusParts = append(statusParts, fmt.Sprintf("%d deleted", deleted)) } b.WriteString(fmt.Sprintf("Git status: %s\n", strings.Join(statusParts, ", "))) } } recentLog := runGitCommand(workDir, "log", "-3", "--oneline") if recentLog != "" { b.WriteString(fmt.Sprintf("Recent commits:\n")) for _, line := range strings.Split(strings.TrimSpace(recentLog), "\n") { b.WriteString(fmt.Sprintf(" - %s\n", line)) } } if b.Len() == 0 { return "" } return b.String() } func runGitCommand(dir string, args ...string) string { cmd := exec.Command("git", args...) cmd.Dir = dir out, err := cmd.Output() if err != nil { return "" } return strings.TrimSpace(string(out)) } func buildMemorySection(store *memory.Store) string { if store.Count() == 0 { return "" } recent := store.Recent(10) if len(recent) == 0 { return "" } var b strings.Builder b.WriteString("\n## Remembered Facts\n") for _, mem := range recent { b.WriteString(fmt.Sprintf("- %s", mem.Content)) if len(mem.Tags) > 0 { b.WriteString(fmt.Sprintf(" [tags: %s]", strings.Join(mem.Tags, ", "))) } b.WriteString("\n") } return b.String() }