f561f686c4
Se mueve la documentación de issues/tasks de .claude/tasks/ a dev/issues/ para separar la planificación de desarrollo de la configuración de Claude. Se añade dev/README.md como índice de la carpeta de desarrollo. Los issues completados se mueven a dev/issues/completed/. Esto permite que dev/ sea el punto central de documentación interna del proyecto. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9.8 KiB
9.8 KiB
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-codey que todas sus respuestas pasen porclaude -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-codeque ya tiene contexto del worktree - Migrar agentes entre proveedores cambiando solo el campo
modelen YAML - Combinar modelos: un agente usa
sonnetpara respuestas rápidas yclaude-codepara tareas que requieren ejecución
Diseño
Config YAML — el agente simplemente elige su modelo
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:
// 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
// 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)
// 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)
// 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)
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
// 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
// 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
// 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:
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 queCompletionRequest/CompletionResultestén completosshell/llm/provider.go— interfazProvider,Registry,Factoryshell/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 declaude -pshell/llm/registry_defaults.go— registro de proveedores disponiblesagents/runtime.go— usarRegistry.Build()para instanciar el provider del agenteinternal/config/schema.go— validarmodel_configsegún elmodelelegido
Notas
- Fase 1: Provider básico — stdin/stdout, sin sesiones, timeout simple
- Fase 2: Soporte de
--session-idpara conversaciones con estado (el agente mantiene el session ID entre interacciones) - Fase 3: Streaming —
claude -p --output-format stream-jsonpara 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-codecomo modelo — Claude Code ya los tiene - Respetar
ctxde shutdown: matar el subproceso concmd.Process.Kill()si el contexto se cancela - El
working_dirdebería ser el worktree del agente para que Claude Code tenga contexto del repo