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>
113 lines
2.8 KiB
Go
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")
|
|
}
|