fc644ecd6e
Reemplaza el scaffold del echobot por la plataforma completa de bots traida desde ~/DataProyects/Github/agents_and_robots tras la operacion Matrix-out: los bots ya no hablan por Matrix sino por el bus unibus (modelo todo-rooms + E2E via shell/transportunibus sobre github.com/enmanuel/unibus/pkg/client). - go.mod: replace de unibus -> ../unibus y de fn-registry -> ../../../.. (paths relativos reajustados a la nueva ubicacion dentro de fn_registry). - app.md: bump a 0.2.0, descripcion + arquitectura + comandos + gotchas reales. - modulo Go conservado como github.com/enmanuel/agents (sin reescribir imports). agents_and_robots queda archivado como museo de la era Matrix.
284 lines
9.8 KiB
Markdown
284 lines
9.8 KiB
Markdown
# 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. |