From 2f89943511031377ecbe24c9aa9677922aeb6e5b Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Fri, 6 Mar 2026 09:06:25 +0000 Subject: [PATCH] chore: move completed tasks to completed/ directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mueve las tareas completadas (01-bot-tools, 02-bot-memory, 04-bot-avatar) al directorio .claude/tasks/completed/ para organización. Añade nueva tarea 06. Co-Authored-By: Claude Opus 4.6 --- .claude/tasks/06-añadir-claude-p.md | 317 ++++++++++++++++++ .claude/tasks/{ => completed}/01-bot-tools.md | 0 .../tasks/{ => completed}/02-bot-memory.md | 0 .../tasks/{ => completed}/04-bot-avatar.md | 0 4 files changed, 317 insertions(+) create mode 100644 .claude/tasks/06-añadir-claude-p.md rename .claude/tasks/{ => completed}/01-bot-tools.md (100%) rename .claude/tasks/{ => completed}/02-bot-memory.md (100%) rename .claude/tasks/{ => completed}/04-bot-avatar.md (100%) diff --git a/.claude/tasks/06-añadir-claude-p.md b/.claude/tasks/06-añadir-claude-p.md new file mode 100644 index 0000000..4eaf100 --- /dev/null +++ b/.claude/tasks/06-añadir-claude-p.md @@ -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: pendiente + +--- + +## 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 \ No newline at end of file diff --git a/.claude/tasks/01-bot-tools.md b/.claude/tasks/completed/01-bot-tools.md similarity index 100% rename from .claude/tasks/01-bot-tools.md rename to .claude/tasks/completed/01-bot-tools.md diff --git a/.claude/tasks/02-bot-memory.md b/.claude/tasks/completed/02-bot-memory.md similarity index 100% rename from .claude/tasks/02-bot-memory.md rename to .claude/tasks/completed/02-bot-memory.md diff --git a/.claude/tasks/04-bot-avatar.md b/.claude/tasks/completed/04-bot-avatar.md similarity index 100% rename from .claude/tasks/04-bot-avatar.md rename to .claude/tasks/completed/04-bot-avatar.md