chore: mover tareas 06-07 a completed y añadir tareas 08-09
Mueve las tareas completadas (06-añadir-claude-p, 07-logs-mejorados) al directorio completed/. Añade nuevas tareas pendientes: 08-knowledge_por_agente y 09-command_system. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
# Plan: Claude Code (`claude -p`) como proveedor LLM de la shell
|
||||
|
||||
## Objetivo
|
||||
|
||||
Que `claude -p` sea un backend LLM más dentro de `shell/llm/`, al mismo nivel que la API HTTP de Anthropic u otros proveedores. Los agentes no saben si su "modelo" es una llamada REST o un subproceso de Claude Code — simplemente envían un `CompletionRequest` y reciben un `CompletionResult`.
|
||||
|
||||
## Estado: Completado
|
||||
|
||||
---
|
||||
|
||||
## Casos de uso
|
||||
|
||||
- Configurar un agente con `model: claude-code` y que todas sus respuestas pasen por `claude -p`
|
||||
- Un agente usa Claude Code como modelo principal, obteniendo capacidades agenticas (bash, file I/O, git) gratis sin implementarlas en nuestra shell
|
||||
- Agentes que necesitan razonar sobre un repo completo delegan al modelo `claude-code` que ya tiene contexto del worktree
|
||||
- Migrar agentes entre proveedores cambiando solo el campo `model` en YAML
|
||||
- Combinar modelos: un agente usa `sonnet` para respuestas rápidas y `claude-code` para tareas que requieren ejecución
|
||||
|
||||
---
|
||||
|
||||
## Diseño
|
||||
|
||||
### Config YAML — el agente simplemente elige su modelo
|
||||
|
||||
```yaml
|
||||
agents:
|
||||
- name: "dev-bot"
|
||||
model: "claude-code" # ← usa claude -p como backend LLM
|
||||
model_config:
|
||||
binary: "claude" # path al binario (default: "claude")
|
||||
max_turns: 10 # turnos agenticos internos de claude -p
|
||||
timeout: "5m"
|
||||
allowed_tools: # tools que claude -p puede usar internamente
|
||||
- "bash"
|
||||
- "read_file"
|
||||
- "write_file"
|
||||
- "git"
|
||||
working_dir: "{{worktree}}"
|
||||
system_prompt_file: "prompts/dev-bot-system.md"
|
||||
|
||||
- name: "chat-bot"
|
||||
model: "sonnet" # ← usa API HTTP normal
|
||||
model_config:
|
||||
api_key_env: "ANTHROPIC_API_KEY"
|
||||
```
|
||||
|
||||
El campo `model` determina qué proveedor de `shell/llm/` se instancia. La `model_config` es específica de cada proveedor.
|
||||
|
||||
---
|
||||
|
||||
### Interfaz pura (core) — sin cambios
|
||||
|
||||
La interfaz del core no cambia. El contrato ya existe:
|
||||
|
||||
```go
|
||||
// core/llm/types.go — esto ya existe o debería existir
|
||||
|
||||
type CompletionRequest struct {
|
||||
SystemPrompt string
|
||||
Messages []Message
|
||||
Temperature float64
|
||||
MaxTokens int
|
||||
}
|
||||
|
||||
type CompletionResult struct {
|
||||
Content string
|
||||
TokensUsed TokenUsage
|
||||
FinishReason string // "stop", "max_turns", "timeout", "error"
|
||||
Metadata map[string]string
|
||||
}
|
||||
|
||||
type TokenUsage struct {
|
||||
Input int
|
||||
Output int
|
||||
}
|
||||
```
|
||||
|
||||
El core solo conoce esta interfaz. No sabe si detrás hay HTTP, un subproceso o una paloma mensajera.
|
||||
|
||||
---
|
||||
|
||||
### Shell — interfaz `Provider` y registro de proveedores
|
||||
|
||||
```go
|
||||
// shell/llm/provider.go
|
||||
|
||||
type Provider interface {
|
||||
Complete(ctx context.Context, req core.CompletionRequest) (core.CompletionResult, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Registry mapea nombres de modelo a constructores de Provider
|
||||
type Registry struct {
|
||||
factories map[string]Factory
|
||||
}
|
||||
|
||||
type Factory func(cfg map[string]any, logger *slog.Logger) (Provider, error)
|
||||
|
||||
func (r *Registry) Register(name string, f Factory)
|
||||
func (r *Registry) Build(name string, cfg map[string]any, logger *slog.Logger) (Provider, error)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Shell — proveedor HTTP (el que ya existe o existiría)
|
||||
|
||||
```go
|
||||
// shell/llm/anthropic/provider.go
|
||||
|
||||
type AnthropicProvider struct {
|
||||
client *http.Client
|
||||
apiKey string
|
||||
model string // "claude-sonnet-4-20250514", etc.
|
||||
baseURL string
|
||||
}
|
||||
|
||||
func NewAnthropicProvider(cfg map[string]any, logger *slog.Logger) (llm.Provider, error)
|
||||
|
||||
func (p *AnthropicProvider) Complete(ctx context.Context, req core.CompletionRequest) (core.CompletionResult, error) {
|
||||
// Construir JSON → POST /v1/messages → parsear respuesta
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Shell — proveedor Claude Code (el nuevo)
|
||||
|
||||
```go
|
||||
// shell/llm/claudecode/provider.go
|
||||
|
||||
type ClaudeCodeProvider struct {
|
||||
binary string
|
||||
maxTurns int
|
||||
timeout time.Duration
|
||||
allowedTools []string
|
||||
workingDir string
|
||||
systemPrompt string // contenido leído del archivo en construcción
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewClaudeCodeProvider(cfg map[string]any, logger *slog.Logger) (llm.Provider, error)
|
||||
|
||||
func (p *ClaudeCodeProvider) Complete(ctx context.Context, req core.CompletionRequest) (core.CompletionResult, error) {
|
||||
// 1. Construir el prompt final: system prompt del provider + messages del request
|
||||
// 2. Armar los args de claude -p
|
||||
// 3. Ejecutar subproceso
|
||||
// 4. Parsear JSON de salida
|
||||
// 5. Mapear a CompletionResult
|
||||
}
|
||||
```
|
||||
|
||||
#### Construcción del comando (interno del provider)
|
||||
|
||||
```go
|
||||
func (p *ClaudeCodeProvider) buildArgs() []string {
|
||||
args := []string{"-p", "--output-format", "json"}
|
||||
|
||||
if p.maxTurns > 0 {
|
||||
args = append(args, "--max-turns", strconv.Itoa(p.maxTurns))
|
||||
}
|
||||
if len(p.allowedTools) > 0 {
|
||||
args = append(args, "--allowedTools", strings.Join(p.allowedTools, ","))
|
||||
}
|
||||
if p.systemPrompt != "" {
|
||||
args = append(args, "--system-prompt", p.systemPrompt)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func (p *ClaudeCodeProvider) Complete(ctx context.Context, req core.CompletionRequest) (core.CompletionResult, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, p.timeout)
|
||||
defer cancel()
|
||||
|
||||
// Aplanar messages a un solo prompt para stdin
|
||||
prompt := flattenMessages(req.Messages)
|
||||
|
||||
cmd := exec.CommandContext(ctx, p.binary, p.buildArgs()...)
|
||||
cmd.Dir = p.workingDir
|
||||
cmd.Stdin = strings.NewReader(prompt)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
|
||||
return p.parseOutput(stdout.Bytes(), stderr.Bytes(), err)
|
||||
}
|
||||
```
|
||||
|
||||
#### Parseo de la salida JSON
|
||||
|
||||
```go
|
||||
// claude -p --output-format json devuelve JSON lines con cada mensaje
|
||||
// El último bloque con role:"assistant" contiene la respuesta final
|
||||
|
||||
type claudeOutputMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
// ... campos adicionales del formato JSON de claude
|
||||
}
|
||||
|
||||
func (p *ClaudeCodeProvider) parseOutput(stdout, stderr []byte, execErr error) (core.CompletionResult, error) {
|
||||
// Parsear JSON lines, extraer último mensaje assistant
|
||||
// Mapear exit code a FinishReason
|
||||
// Extraer token usage si está disponible
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Registro en el arranque
|
||||
|
||||
```go
|
||||
// shell/llm/registry_defaults.go
|
||||
|
||||
func NewDefaultRegistry() *Registry {
|
||||
r := &Registry{factories: make(map[string]Factory)}
|
||||
|
||||
r.Register("sonnet", anthropic.NewAnthropicProvider)
|
||||
r.Register("haiku", anthropic.NewAnthropicProvider)
|
||||
r.Register("opus", anthropic.NewAnthropicProvider)
|
||||
r.Register("claude-code", claudecode.NewClaudeCodeProvider) // ← nuevo
|
||||
|
||||
return r
|
||||
}
|
||||
```
|
||||
|
||||
### Instanciación en el runtime del agente
|
||||
|
||||
```go
|
||||
// agents/runtime.go
|
||||
|
||||
func (a *Agent) init(registry *llm.Registry) error {
|
||||
provider, err := registry.Build(a.cfg.Model, a.cfg.ModelConfig, a.logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("building LLM provider %q: %w", a.cfg.Model, err)
|
||||
}
|
||||
a.llm = provider
|
||||
return nil
|
||||
}
|
||||
|
||||
// Después, cuando el agente necesita razonar:
|
||||
func (a *Agent) handleMessage(ctx context.Context, msg Message) (string, error) {
|
||||
req := core.CompletionRequest{
|
||||
SystemPrompt: a.systemPrompt,
|
||||
Messages: a.buildMessages(msg),
|
||||
}
|
||||
result, err := a.llm.Complete(ctx, req) // ← no sabe si es HTTP o subproceso
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result.Content, nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diferencia clave vs. modelo HTTP
|
||||
|
||||
| Aspecto | Proveedor HTTP (`sonnet`) | Proveedor Claude Code (`claude-code`) |
|
||||
|---|---|---|
|
||||
| Transporte | HTTP a `api.anthropic.com` | Subproceso local `claude -p` |
|
||||
| Auth | API key | Session de Claude Code (login previo) |
|
||||
| Capacidades extra | Solo texto in/out | Agentic: bash, files, git dentro de `claude -p` |
|
||||
| Latencia | Baja por request | Mayor (startup del proceso + múltiples turnos internos) |
|
||||
| Costo | Por tokens via API | Por tokens via Claude Code (misma cuenta) |
|
||||
| Estado | Stateless | Puede mantener sesión (`--session-id`) |
|
||||
| Working dir | N/A | El worktree del agente |
|
||||
|
||||
---
|
||||
|
||||
## Flatten de mensajes para `claude -p`
|
||||
|
||||
`claude -p` recibe el prompt por stdin como texto plano. Hay que aplanar el historial:
|
||||
|
||||
```go
|
||||
func flattenMessages(msgs []core.Message) string {
|
||||
var b strings.Builder
|
||||
for _, m := range msgs {
|
||||
switch m.Role {
|
||||
case "user":
|
||||
fmt.Fprintf(&b, "User: %s\n\n", m.Content)
|
||||
case "assistant":
|
||||
fmt.Fprintf(&b, "Assistant: %s\n\n", m.Content)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
```
|
||||
|
||||
Alternativa para conversaciones largas: usar `--session-id` y enviar solo el último mensaje.
|
||||
|
||||
---
|
||||
|
||||
## Archivos a crear/modificar
|
||||
|
||||
- `core/llm/types.go` — revisar que `CompletionRequest`/`CompletionResult` estén completos
|
||||
- `shell/llm/provider.go` — interfaz `Provider`, `Registry`, `Factory`
|
||||
- `shell/llm/anthropic/provider.go` — proveedor HTTP (refactorizar si ya existe)
|
||||
- **`shell/llm/claudecode/provider.go`** — proveedor Claude Code (nuevo)
|
||||
- `shell/llm/claudecode/parser.go` — parseo de JSON output de `claude -p`
|
||||
- `shell/llm/registry_defaults.go` — registro de proveedores disponibles
|
||||
- `agents/runtime.go` — usar `Registry.Build()` para instanciar el provider del agente
|
||||
- `internal/config/schema.go` — validar `model_config` según el `model` elegido
|
||||
|
||||
---
|
||||
|
||||
## Notas
|
||||
|
||||
- **Fase 1**: Provider básico — stdin/stdout, sin sesiones, timeout simple
|
||||
- **Fase 2**: Soporte de `--session-id` para conversaciones con estado (el agente mantiene el session ID entre interacciones)
|
||||
- **Fase 3**: Streaming — `claude -p --output-format stream-json` para respuestas parciales en tiempo real a la sala Matrix
|
||||
- **Fase 4**: Pool de procesos — reutilizar sesiones de Claude Code para reducir latencia de startup
|
||||
- El agente no necesita implementar tools propios para bash/git/files si usa `claude-code` como modelo — Claude Code ya los tiene
|
||||
- Respetar `ctx` de shutdown: matar el subproceso con `cmd.Process.Kill()` si el contexto se cancela
|
||||
- El `working_dir` debería ser el worktree del agente para que Claude Code tenga contexto del repo
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user