Files
agents_and_robots/dev/issues/completed/009-command_system.md
T
egutierrez f561f686c4 refactor: migrar tasks/ a dev/issues/ con estructura de desarrollo
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>
2026-03-07 17:41:16 +00:00

7.5 KiB

Task 09 — Sistema de comandos directos (!command)

Objetivo

Implementar un sistema de comandos que permita a los usuarios ejecutar acciones directamente via !comando sin depender del LLM. Soportar agentes "simple_bot" que no tienen LLM y solo responden a comandos.

Contexto actual

  • message.Parse ya detecta CommandPrefix (!) y extrae Command + Args en MessageContext
  • decision.MatchCommand() ya existe para matchear comandos en reglas
  • tools.Registry ya tiene Execute(ctx, name, argsJSON) para ejecutar tools
  • Cada agente define sus reglas en agent.go con Rules() []decision.Rule
  • El flujo actual: solo !help existe como comando hardcodeado en cada agente

Problema

  • Los comandos estan hardcodeados en cada agent.go como reglas individuales
  • No hay forma de ejecutar tools directamente sin pasar por el LLM
  • No hay comandos built-in compartidos entre agentes
  • No se puede crear un bot sin LLM (simple_bot)
  • El !help es estatico y no refleja las tools reales del agente

Diseno

Arquitectura (pure core / impure shell)

pkg/command/         -> PURE: tipos Command, parser de args, specs built-in
agents/runtime.go    -> composicion: conecta commands con tools y shell

Tipos de comandos

  1. Built-in commands (disponibles en todos los agentes):

    Comando Descripcion
    !help Lista comandos disponibles (built-in + custom)
    !tools Lista tools registradas con descripcion
    !ping Alive check, responde "pong" con timestamp
    !status Info del agente: uptime, rooms activos, window sizes
    !info Nombre, version, descripcion del agente
    !clear Limpia ventana de conversacion del room actual
    !version Version del agente
  2. Tool commands — ejecutar tools directas:

    !tool <nombre>                          -> sin args
    !tool <nombre> key=value                -> arg simple
    !tool <nombre> key="valor con espacios" -> arg con espacios
    !tool <nombre> key=value key2=value2    -> multiples args
    

    Ejemplos:

    • !tool ssh_command host=server1 command="uptime"
    • !tool current_time
    • !tool knowledge_search query="como configurar"
  3. Custom commands — definidos por cada agente en su agent.go via Rules con MatchCommand (como ahora, pero mejor integrados)

Flujo de ejecucion

Matrix event
  -> message.Parse (ya extrae Command + Args)
  -> handleEvent:
       1. Si hay Command (empieza con !prefix):
          a. Custom command del agente (rules con MatchCommand)? -> ejecutar regla
          b. Built-in command? -> ejecutar handler, responder
          c. "tool" command? -> parsear args, ejecutar via tools.Registry, responder
          d. No encontrado? -> responder "comando desconocido, usa !help"
       2. Si NO es comando: flujo actual (rules -> LLM fallback si hay LLM)
       3. Si NO es comando y NO hay LLM: ignorar (solo responde a comandos)

Nota: las reglas custom del agente tienen prioridad sobre built-ins. Si un agente define una regla MatchCommand("help") propia, esa gana sobre el built-in.

Nuevo paquete pkg/command/ (puro)

// pkg/command/types.go

// Spec es la spec pura de un comando. Solo datos.
type Spec struct {
    Name        string
    Aliases     []string   // e.g. ["h"] para help
    Description string     // descripcion corta para !help
    Usage       string     // e.g. "!tool <name> [key=value ...]"
    Hidden      bool       // no mostrar en !help
}

// ParsedArgs resultado de parsear "key=value key2=value2"
type ParsedArgs struct {
    Positional []string          // args sin key=
    Named      map[string]string // args con key=value
    Raw        []string          // args originales
}
// pkg/command/parse.go

// ParseArgs convierte []string{"host=server1", "command=uptime"} en ParsedArgs. Puro.
func ParseArgs(args []string) ParsedArgs { ... }

// ArgsToJSON convierte ParsedArgs.Named a JSON string para tools.Registry.Execute. Puro.
func ArgsToJSON(named map[string]string) string { ... }
// pkg/command/builtins.go

// Builtins retorna las specs de todos los comandos built-in. Puro.
func Builtins() []Spec { ... }

Cambios en agents/runtime.go

// CommandHandler ejecuta un comando built-in y devuelve la respuesta texto.
type CommandHandler func(ctx context.Context, msgCtx decision.MessageContext) string

// Nuevos campos en Agent:
type Agent struct {
    // ... existente ...
    commands    map[string]CommandHandler  // built-in command handlers
    startTime  time.Time                  // para !status
}

En handleEvent, el flujo cambia a:

// 1. Evaluar reglas custom primero (pueden overridear built-ins)
if msgCtx.Command != "" {
    actions := decision.Evaluate(msgCtx, a.rules)
    if len(actions) > 0 {
        // ejecutar como ahora (expand LLM actions, runner.Execute)
        return
    }
    // 2. Buscar en built-ins
    if handler, ok := a.commands[msgCtx.Command]; ok {
        reply := handler(ctx, msgCtx)
        a.matrix.SendText(ctx, roomID, reply)
        return
    }
    // 3. Comando desconocido
    a.matrix.SendText(ctx, roomID, "Comando desconocido. Usa !help")
    return
}

// 4. Sin comando: LLM fallback (si hay LLM) o ignorar
if a.llm == nil {
    return // simple_bot: solo responde a comandos
}
// ... flujo LLM actual (DM/mention -> LLM) ...

Simple bots (sin LLM)

Un simple_bot se configura sin seccion llm o con llm.primary.provider: "":

agent:
  id: monitor-bot
  name: Monitor Bot
  enabled: true
  description: "Bot de monitoreo, solo comandos"

tools:
  ssh:
    enabled: true
    allowed_targets: ["webserver"]

En New(), si no hay LLM configurado, a.llm queda nil. El bot solo responde a comandos.

Tareas de implementacion

Fase 1 — Core puro (pkg/command/)

  • Crear pkg/command/types.go — tipos Spec, ParsedArgs
  • Crear pkg/command/parse.go — ParseArgs, ArgsToJSON
  • Crear pkg/command/parse_test.go — tests del parser
  • Crear pkg/command/builtins.go — specs de los 7 comandos built-in + BuiltinNames()

Fase 2 — Handlers en runtime (agents/)

  • Agregar campos commands, cmdAliases, startTime al Agent struct
  • Implementar handlers: help, tools, ping, info, version, clear, status
  • Implementar handler tool — parsea args key=value, ejecuta via Registry, formatea respuesta
  • Registrar todos los handlers en New() via registerBuiltinCommands()
  • Modificar handleEvent — nuevo flujo: rules custom -> built-in -> comando desconocido -> LLM fallback
  • Extraer executeActions() helper para reutilizar en ambos flujos

Fase 3 — Simple bot support

  • Hacer LLM opcional en New() (no fallar si no hay provider)
  • Si a.llm == nil y no hay comando, ignorar mensaje
  • Verificar que un agente sin LLM arranca y responde a !help, !tool, !ping

Fase 4 — Integracion con agentes existentes

  • Eliminar regla !help hardcodeada de assistant-bot/agent.go
  • Eliminar regla !help hardcodeada de asistente-2/agent.go
  • Verificar que reglas custom (llm-all, etc.) siguen funcionando (build OK)
  • Test manual: !help, !tools, !tool current_time, !ping, !status, !clear, !info, !version

Fase 5 (futura) — Simple bot de ejemplo

  • Crear agente simple_bot de ejemplo sin LLM
  • Documentar patron simple_bot