diff --git a/.claude/tasks/07-logs-mejorados.md b/.claude/tasks/07-logs-mejorados.md new file mode 100644 index 0000000..8fd929f --- /dev/null +++ b/.claude/tasks/07-logs-mejorados.md @@ -0,0 +1,284 @@ +# Tarea: Implementar Sistema de Logging Estructurado para Agentes + +## Contexto del Proyecto + +Estamos construyendo un sistema multi-agente en Go con las siguientes características arquitectónicas: + +- **Separación pure core / impure shell**: el core retorna decisiones como datos, el shell las ejecuta e interactúa con el mundo exterior. +- **Monorepo en Go** con módulos separados. +- **Comunicación inter-agente via Matrix** (mautrix-go) como bus de mensajes. +- **Múltiples agentes** con identidades independientes (cada uno con su propio contexto Git, etc.). +- **Integración con múltiples LLM providers** (Anthropic, OpenAI-compatible, Ollama) via abstracción unificada. + +El logging vive en el **impure shell** — nunca en el core. + +## Objetivo + +Crear un paquete `pkg/logger` (o `internal/logger`) que provea logging estructurado en formato JSONL, optimizado para ser consumido tanto por humanos como por agentes LLM. Los logs deben ser fácilmente parseables, consultables por fecha/agente, y auto-gestionados (rotación, limpieza). + +## Requisitos Funcionales + +### 1. Formato de Salida: JSONL + +Cada línea de log es un objeto JSON independiente con los siguientes campos obligatorios: + +```json +{ + "time": "2026-03-06T10:00:00.000Z", + "level": "INFO", + "msg": "agent action completed", + "agent_id": "researcher-01", + "trace_id": "abc123", + "component": "shell" +} +``` + +Campos opcionales según contexto: + +```json +{ + "action": "web_search", + "duration_ms": 342, + "tokens_used": 1500, + "result": "success", + "error_type": "timeout", + "reason": "user requested summary of recent papers", + "metadata": {} +} +``` + +El campo `reason` es especialmente importante: cuando otro agente lee el log, necesita saber *por qué* se tomó una decisión, no solo *qué* se hizo. + +### 2. Segmentación de Archivos + +Estructura de directorios por agente y por día: + +``` +/var/log/agents/ +├── orchestrator/ +│ ├── 2026-03-04.jsonl +│ ├── 2026-03-05.jsonl +│ └── 2026-03-06.jsonl +├── researcher-01/ +│ ├── 2026-03-05.jsonl +│ └── 2026-03-06.jsonl +└── coder-01/ + └── 2026-03-06.jsonl +``` + +Reglas: +- Un archivo JSONL por agente por día. +- Si un archivo excede un tamaño máximo configurable (default: 50MB), se rota añadiendo un sufijo incremental: `2026-03-06.jsonl` → `2026-03-06.1.jsonl`. +- Nombres de archivo siempre en formato `YYYY-MM-DD.jsonl`. + +### 3. Rotación y Limpieza + +- **Retención configurable** (default: 7 días). +- **Goroutine de limpieza** que corre periódicamente (default: cada 24h) y elimina archivos que excedan la retención. +- **Compresión opcional** de archivos rotados (gzip). +- La limpieza debe ser segura para ejecución concurrente. + +### 4. API del Logger + +```go +// Config para crear un logger de agente +type LoggerConfig struct { + BaseDir string // directorio raíz de logs (default: "/var/log/agents") + AgentID string // identificador único del agente + MaxSizeMB int64 // tamaño máximo por archivo (default: 50) + MaxAgeDays int // días de retención (default: 7) + Compress bool // comprimir archivos rotados (default: true) + CleanupInterval time.Duration // intervalo de limpieza (default: 24h) + Level slog.Level // nivel mínimo de log (default: slog.LevelInfo) +} + +// Factory function +func NewAgentLogger(cfg LoggerConfig) (*slog.Logger, func(), error) +// Retorna: +// - *slog.Logger: logger configurado con slog +// - func(): función de cleanup para llamar en shutdown (cierra archivos, detiene goroutine de limpieza) +// - error: si no se puede crear el directorio o el archivo inicial + +// Uso esperado: +logger, cleanup, err := logger.NewAgentLogger(logger.LoggerConfig{ + AgentID: "researcher-01", +}) +defer cleanup() + +logger.InfoContext(ctx, "executing decision", + "action", decision.Action, + "reason", decision.Reason, + "trace_id", traceIDFromCtx(ctx), + "tokens_used", 1500, +) +``` + +### 5. Writer Personalizado + +Implementar un `io.Writer` que maneje la rotación diaria con fallback por tamaño: + +```go +type DailyRotatingWriter struct { + baseDir string + agentID string + maxSizeMB int64 + compress bool + + mu sync.Mutex + current *os.File + written int64 + currentDay string + suffix int // para rotación por tamaño dentro del mismo día +} + +// Debe implementar io.Writer +func (w *DailyRotatingWriter) Write(p []byte) (n int, err error) + +// Cierre limpio +func (w *DailyRotatingWriter) Close() error +``` + +Lógica de `Write`: +1. Adquirir lock. +2. Verificar si el día cambió (`time.Now().Format("2006-01-02")` vs `w.currentDay`). +3. Si cambió el día: cerrar archivo actual, comprimir si `compress=true`, abrir nuevo archivo del día, resetear `written` y `suffix`. +4. Si `written > maxSizeMB * 1024 * 1024`: incrementar `suffix`, abrir nuevo archivo (`2026-03-06.1.jsonl`), resetear `written`. +5. Escribir `p` al archivo actual. +6. Incrementar `written`. + +### 6. Helpers para Consulta por LLMs + +Proveer funciones utilitarias para que los agentes puedan consultar logs: + +```go +// Leer logs de un agente en un rango de fechas +func ReadLogs(baseDir, agentID string, from, to time.Time) ([]json.RawMessage, error) + +// Leer logs de un agente para un día específico +func ReadDayLogs(baseDir, agentID string, date time.Time) ([]json.RawMessage, error) + +// Buscar logs que contengan un campo con un valor específico +func SearchLogs(baseDir, agentID string, field, value string, from, to time.Time) ([]json.RawMessage, error) + +// Listar agentes disponibles (subdirectorios) +func ListAgents(baseDir string) ([]string, error) + +// Listar fechas disponibles para un agente +func ListDates(baseDir, agentID string) ([]time.Time, error) +``` + +Estas funciones permiten que un agente LLM solicite logs con interfaces simples. El agente orquestador puede usar `SearchLogs` para buscar errores, o `ReadDayLogs` para obtener contexto de lo que hizo otro agente ayer. + +## Requisitos No Funcionales + +- **Stdlib primero**: usar `log/slog` como base. No dependencias externas excepto lo estrictamente necesario (si lumberjack simplifica, se puede usar, pero la implementación custom del `DailyRotatingWriter` es preferida). +- **Thread-safe**: múltiples goroutines escribirán al mismo logger. +- **Mínimo overhead**: el logging no debe impactar significativamente el rendimiento del agente. Escribir en buffer si es necesario. +- **Consistencia de campos**: usar los mismos nombres de campo siempre. Definir constantes para campos estándar: + +```go +const ( + FieldAgentID = "agent_id" + FieldTraceID = "trace_id" + FieldAction = "action" + FieldReason = "reason" + FieldDurationMS = "duration_ms" + FieldTokensUsed = "tokens_used" + FieldResult = "result" + FieldErrorType = "error_type" + FieldComponent = "component" +) +``` + +- **Testeable**: incluir tests unitarios para: + - Rotación por día. + - Rotación por tamaño dentro del mismo día. + - Limpieza de archivos viejos. + - Formato de salida JSONL correcto. + - Concurrencia (múltiples writers simultáneos). + - Funciones de consulta (`ReadLogs`, `SearchLogs`). + +## Estructura de Archivos Esperada + +``` +pkg/logger/ +├── logger.go // NewAgentLogger, LoggerConfig, constantes de campos +├── writer.go // DailyRotatingWriter implementation +├── cleanup.go // Goroutine de limpieza y compresión +├── query.go // ReadLogs, SearchLogs, ListAgents, ListDates +├── logger_test.go // Tests del logger y formato +├── writer_test.go // Tests de rotación +├── cleanup_test.go // Tests de limpieza +└── query_test.go // Tests de consulta +``` + +## Restricciones + +- Go 1.21+ (para `log/slog` nativo). +- Sin CGO. +- Sin dependencias externas (stdlib pura). Si consideras que alguna dependencia aporta valor significativo, justifícala explícitamente. +- El logger debe poder funcionar tanto escribiendo a archivos como a stdout (para desarrollo/debugging), configurable via `LoggerConfig`. +- Todos los timestamps en UTC. + +## Ejemplo de Integración + +Así se vería el uso del logger dentro del shell de un agente: + +```go +package main + +import ( + "context" + "log/slog" + "myproject/pkg/logger" +) + +func main() { + log, cleanup, err := logger.NewAgentLogger(logger.LoggerConfig{ + AgentID: "researcher-01", + BaseDir: "/var/log/agents", + Level: slog.LevelInfo, + Compress: true, + }) + if err != nil { + panic(err) + } + defer cleanup() + + ctx := context.Background() + ctx = logger.WithTraceID(ctx, "trace-abc-123") + + // El core retorna una decisión pura + decision := core.Decide(input) + + // El shell loguea y ejecuta + log.InfoContext(ctx, "executing decision", + logger.FieldAction, decision.Action, + logger.FieldReason, decision.Reason, + logger.FieldComponent, "shell", + ) + + result, err := shell.Execute(ctx, decision) + if err != nil { + log.ErrorContext(ctx, "decision execution failed", + logger.FieldAction, decision.Action, + logger.FieldErrorType, categorizeError(err), + "error", err.Error(), + ) + return + } + + log.InfoContext(ctx, "decision executed successfully", + logger.FieldAction, decision.Action, + logger.FieldResult, "success", + logger.FieldDurationMS, result.DurationMS, + logger.FieldTokensUsed, result.TokensUsed, + ) +} +``` + +## Notas Adicionales + +- El `trace_id` permite correlacionar un flujo completo a través de múltiples agentes. Si el orchestrator inicia una tarea y delega al researcher, ambos usan el mismo `trace_id`. +- Considerar un helper `WithTraceID(ctx, id)` / `TraceIDFromCtx(ctx)` usando `context.Value`. +- El campo `reason` captura la intención detrás de la acción. Un LLM que lee "reason: user requested summary of recent AI papers" entiende el contexto sin necesidad de reconstruirlo desde mensajes anteriores. \ No newline at end of file