merge: issue/0047-fix-system-prompt-path — system prompt para _specials
Corrige bug donde el system prompt de agentes en agents/_specials/ (como Father Bot) no se cargaba porque el runtime resolvia la ruta como agents/<id>/... en vez del directorio real del config. Agrega ConfigDir al schema de config, poblado por el loader.
This commit is contained in:
@@ -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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
||||||
|
|||||||
@@ -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/<agent-id>/prompts/system.md`,
|
||||||
|
pero los agentes especiales (Father Bot, etc.) viven en `agents/_specials/<agent-id>/`. 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/<id>`
|
||||||
|
|
||||||
|
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.
|
||||||
+2
-2
@@ -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
|
// Load system prompt from file if configured, else use description
|
||||||
systemPrompt := a.cfg.Agent.Description
|
systemPrompt := a.cfg.Agent.Description
|
||||||
if spFile := a.cfg.LLM.Reasoning.SystemPromptFile; spFile != "" {
|
if spFile := a.cfg.LLM.Reasoning.SystemPromptFile; spFile != "" {
|
||||||
// Resolve path relative to agent directory
|
// Resolve path relative to the config directory (handles _specials/ and custom locations)
|
||||||
spPath := filepath.Join("agents", a.cfg.Agent.ID, spFile)
|
spPath := filepath.Join(a.cfg.ConfigDir, spFile)
|
||||||
if data, err := os.ReadFile(spPath); err == nil {
|
if data, err := os.ReadFile(spPath); err == nil {
|
||||||
systemPrompt = string(data)
|
systemPrompt = string(data)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
@@ -26,6 +27,8 @@ func Load(path string) (*AgentConfig, error) {
|
|||||||
return nil, fmt.Errorf("invalid config %s: %w", path, err)
|
return nil, fmt.Errorf("invalid config %s: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg.ConfigDir = filepath.Dir(path)
|
||||||
|
|
||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 ────────────────────────────
|
// ── 2.2: Parse full config with all sections ────────────────────────────
|
||||||
|
|
||||||
func TestLoad_FullConfig(t *testing.T) {
|
func TestLoad_FullConfig(t *testing.T) {
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ type AgentConfig struct {
|
|||||||
Storage StorageCfg `yaml:"storage"`
|
Storage StorageCfg `yaml:"storage"`
|
||||||
Memory MemoryCfg `yaml:"memory"`
|
Memory MemoryCfg `yaml:"memory"`
|
||||||
Skills SkillsCfg `yaml:"skills"`
|
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 ──────────────────────────────────────────────────────────────
|
// ── Identity ──────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user