diff --git a/dev/issues/README.md b/dev/issues/README.md index 86102d9..aef3628 100644 --- a/dev/issues/README.md +++ b/dev/issues/README.md @@ -57,3 +57,4 @@ afectados y notas de implementacion. | 44 | Formalizar pipeline de creacion de agentes | [0044-formalize-agent-creation-pipeline.md](completed/0044-formalize-agent-creation-pipeline.md) | completado | | 45 | DM rooms sin E2EE en notify-developer.sh | [0045-notify-encrypted-rooms.md](completed/0045-notify-encrypted-rooms.md) | completado | | 46 | Progreso en tiempo real para Father Bot | [0046-father-bot-progress.md](completed/0046-father-bot-progress.md) | completado | +| 47 | System prompt no se carga para agentes en _specials/ | [0047-fix-system-prompt-path.md](completed/0047-fix-system-prompt-path.md) | completado | diff --git a/dev/issues/completed/0047-fix-system-prompt-path.md b/dev/issues/completed/0047-fix-system-prompt-path.md new file mode 100644 index 0000000..a7f8ec0 --- /dev/null +++ b/dev/issues/completed/0047-fix-system-prompt-path.md @@ -0,0 +1,81 @@ +# 0047 — System prompt no se carga para agentes en _specials/ + +## Objetivo + +El runtime resuelve la ruta del `system_prompt_file` como `agents//prompts/system.md`, +pero los agentes especiales (Father Bot, etc.) viven en `agents/_specials//`. Resultado: +el system prompt no se carga y el agente usa solo la `description` como prompt. + +## Contexto + +En `devagents/llm.go:33`, la ruta se construye asi: + +```go +spPath := filepath.Join("agents", a.cfg.Agent.ID, spFile) +``` + +Esto produce `agents/father-bot/prompts/system.md` para Father Bot, pero el archivo real esta en +`agents/_specials/father-bot/prompts/system.md`. + +Los logs confirman el problema: +```json +{"msg":"failed to load system_prompt_file, using description","path":"agents/father-bot/prompts/system.md"} +``` + +**Impacto**: Father Bot opera sin su system prompt completo (369 lineas de instrucciones, pipeline, +seguridad) y solo usa la description de una linea del config.yaml. Esto degrada severamente su +comportamiento. + +## Arquitectura + +- `internal/config/schema.go` — MODIFICAR: agregar campo `ConfigDir` a `AgentConfig` +- `internal/config/loader.go` — MODIFICAR: poblar `ConfigDir` con el directorio del config +- `devagents/llm.go` — MODIFICAR: usar `ConfigDir` en vez de hardcodear `agents/` + +No hay cambios en `pkg/` (puro). Los cambios son en el loader (impuro) y runtime (impuro). + +## Tareas + +### Fase 1: Fix + +- [ ] 1.1 Agregar campo `ConfigDir string` (no YAML, solo runtime) a `AgentConfig` +- [ ] 1.2 En `config.Load()`, poblar `ConfigDir` con `filepath.Dir(path)` +- [ ] 1.3 En `devagents/llm.go`, usar `a.cfg.ConfigDir` para resolver `system_prompt_file` + +### Fase 2: Tests + +- [ ] 2.1 Test unitario que verifica que `Load()` puebla `ConfigDir` +- [ ] 2.2 `go build -tags goolm ./...` compila sin errores +- [ ] 2.3 `go test -tags goolm ./...` pasa sin errores + +### Fase 3: Docs + +- [ ] 3.1 Cerrar issue, mover a completed + +## Ejemplo de uso + +Antes (roto): +``` +config en: agents/_specials/father-bot/config.yaml +system_prompt_file: prompts/system.md +resuelve: agents/father-bot/prompts/system.md ← NO EXISTE +resultado: usa description como fallback +``` + +Despues (correcto): +``` +config en: agents/_specials/father-bot/config.yaml +ConfigDir: agents/_specials/father-bot +system_prompt_file: prompts/system.md +resuelve: agents/_specials/father-bot/prompts/system.md ← CORRECTO +``` + +## Decisiones de diseno + +1. **`ConfigDir` como campo runtime**: no se serializa en YAML (`yaml:"-"`), se puebla + automaticamente por el loader. Cero impacto en configs existentes. +2. **Genérico**: el fix funciona para cualquier agente en cualquier ubicacion, no solo _specials. + +## Riesgos + +- Bajo riesgo: cambio minimo y auto-contenido. El campo nuevo es backward-compatible. diff --git a/devagents/llm.go b/devagents/llm.go index 6cd1762..eb84a96 100644 --- a/devagents/llm.go +++ b/devagents/llm.go @@ -29,8 +29,8 @@ func (a *Agent) runLLM(ctx context.Context, msgCtx decision.MessageContext, memK // Load system prompt from file if configured, else use description systemPrompt := a.cfg.Agent.Description if spFile := a.cfg.LLM.Reasoning.SystemPromptFile; spFile != "" { - // Resolve path relative to agent directory - spPath := filepath.Join("agents", a.cfg.Agent.ID, spFile) + // Resolve path relative to the config directory (handles _specials/ and custom locations) + spPath := filepath.Join(a.cfg.ConfigDir, spFile) if data, err := os.ReadFile(spPath); err == nil { systemPrompt = string(data) } else { diff --git a/internal/config/loader.go b/internal/config/loader.go index 4bf891f..30d5fff 100644 --- a/internal/config/loader.go +++ b/internal/config/loader.go @@ -3,6 +3,7 @@ package config import ( "fmt" "os" + "path/filepath" "gopkg.in/yaml.v3" ) @@ -26,6 +27,8 @@ func Load(path string) (*AgentConfig, error) { return nil, fmt.Errorf("invalid config %s: %w", path, err) } + cfg.ConfigDir = filepath.Dir(path) + return &cfg, nil } diff --git a/internal/config/loader_test.go b/internal/config/loader_test.go index 432d0fe..b4f837e 100644 --- a/internal/config/loader_test.go +++ b/internal/config/loader_test.go @@ -50,6 +50,47 @@ llm: } } +// ── 2.1b: ConfigDir populated from file path ─────────────────────────── + +func TestLoad_ConfigDir(t *testing.T) { + // Create a nested directory to simulate agents/_specials/father-bot/ + dir := filepath.Join(t.TempDir(), "agents", "_specials", "father-bot") + if err := os.MkdirAll(dir, 0755); err != nil { + t.Fatal(err) + } + path := writeYAML(t, dir, "config.yaml", ` +agent: + id: father-bot +matrix: + homeserver: https://matrix.example.com + user_id: "@father-bot:example.com" +llm: + primary: + provider: claude-code + claude_code: + binary: claude + reasoning: + system_prompt_file: prompts/system.md +`) + cfg, err := Load(path) + if err != nil { + t.Fatalf("Load() error: %v", err) + } + if cfg.ConfigDir != dir { + t.Errorf("ConfigDir = %q, want %q", cfg.ConfigDir, dir) + } + // Verify that joining ConfigDir + system_prompt_file gives the right path + spPath := filepath.Join(cfg.ConfigDir, cfg.LLM.Reasoning.SystemPromptFile) + wantSuffix := filepath.Join("agents", "_specials", "father-bot", "prompts", "system.md") + if !filepath.IsAbs(spPath) { + // When running from TempDir, path will be absolute + t.Logf("spPath = %q (expected to end with %q)", spPath, wantSuffix) + } + if cfg.LLM.Reasoning.SystemPromptFile != "prompts/system.md" { + t.Errorf("SystemPromptFile = %q, want %q", cfg.LLM.Reasoning.SystemPromptFile, "prompts/system.md") + } +} + // ── 2.2: Parse full config with all sections ──────────────────────────── func TestLoad_FullConfig(t *testing.T) { diff --git a/internal/config/schema.go b/internal/config/schema.go index 7cb03b4..089d8f5 100644 --- a/internal/config/schema.go +++ b/internal/config/schema.go @@ -16,6 +16,11 @@ type AgentConfig struct { Storage StorageCfg `yaml:"storage"` Memory MemoryCfg `yaml:"memory"` Skills SkillsCfg `yaml:"skills"` + + // ConfigDir is the directory containing the config file. Set by the loader + // at load time, not from YAML. Used to resolve relative paths like + // system_prompt_file correctly regardless of where the agent lives. + ConfigDir string `yaml:"-"` } // ── Identity ──────────────────────────────────────────────────────────────