rony-llm-agent/pkg/persona/persona.go
Victor Vargas 0f835a0802 feat(agent): wire AGENTS.md discovery into the agent loop's system prompt
Export persona.DiscoverAgentsMD and add agent.Config.AgentsMD so project
and global AGENTS.md rules actually reach the model. Previously
buildInitialMessages always called AssembleSystemPrompt with an empty
string, so no AGENTS.md content was ever injected despite the discovery
logic already existing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 23:27:17 -07:00

113 lines
2.8 KiB
Go

package persona
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/VictorVargas/rony-llm-agent/pkg/llm"
)
// Persona defines the personality and behavior of the agent.
type Persona struct {
ID string
Name string
Tone string
Style string
Language string
Constraints []string
FewShot []llm.Message
}
// Loader loads personas from files.
type Loader interface {
Load(ctx context.Context, configPath string) (Persona, error)
Discover(ctx context.Context, workdir string) (Persona, error)
}
// ErrPersonaNotFound is returned when a persona file is not found.
var ErrPersonaNotFound = fmt.Errorf("persona not found")
// DefaultPersona returns a basic persona with sensible defaults.
func DefaultPersona() Persona {
return Persona{
ID: "default",
Name: "Rony",
Tone: "professional and helpful",
Style: "clear and concise",
Language: "the user's language",
}
}
// AssembleSystemPrompt builds the final system prompt from persona, base prompt, and AGENTS.md.
func AssembleSystemPrompt(p Persona, agentsMD string) string {
var parts []string
// Base system prompt
parts = append(parts, "You are an AI agent. Be helpful, accurate, and safe.")
// Persona instructions
if p.Name != "" {
parts = append(parts, fmt.Sprintf("Your name is %s.", p.Name))
}
if p.Tone != "" {
parts = append(parts, fmt.Sprintf("Use a %s tone.", p.Tone))
}
if p.Style != "" {
parts = append(parts, fmt.Sprintf("Write in a %s style.", p.Style))
}
if p.Language != "" {
if strings.EqualFold(p.Language, "the user's language") {
parts = append(parts, "Respond in the same language as the user's messages.")
} else {
parts = append(parts, fmt.Sprintf("Respond in %s.", p.Language))
}
}
for _, c := range p.Constraints {
parts = append(parts, fmt.Sprintf("CONSTRAINT: %s", c))
}
// AGENTS.md content
if agentsMD != "" {
parts = append(parts, "---")
parts = append(parts, "Project instructions:")
parts = append(parts, agentsMD)
}
return strings.Join(parts, "\n\n")
}
// DiscoverAgentsMD walks up the directory tree looking for AGENTS.md files.
func DiscoverAgentsMD(root string) string {
var parts []string
current := root
for {
agentsPath := filepath.Join(current, "AGENTS.md")
if _, err := os.Stat(agentsPath); err == nil {
data, err := os.ReadFile(agentsPath)
if err == nil {
parts = append(parts, string(data))
}
}
parent := filepath.Dir(current)
if parent == current || parent == "." {
break
}
current = parent
}
// Also check ~/.config/rony/AGENTS.md
home, err := os.UserHomeDir()
if err == nil {
globalPath := filepath.Join(home, ".config", "rony", "AGENTS.md")
if data, err := os.ReadFile(globalPath); err == nil {
parts = append(parts, string(data))
}
}
return strings.Join(parts, "\n\n")
}