# 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 -> sin args !tool key=value -> arg simple !tool key="valor con espacios" -> arg con espacios !tool 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) ```go // 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 [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 } ``` ```go // 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 { ... } ``` ```go // pkg/command/builtins.go // Builtins retorna las specs de todos los comandos built-in. Puro. func Builtins() []Spec { ... } ``` ### Cambios en `agents/runtime.go` ```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: ```go // 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: ""`: ```yaml 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/`) - [x] Crear `pkg/command/types.go` — tipos Spec, ParsedArgs - [x] Crear `pkg/command/parse.go` — ParseArgs, ArgsToJSON - [x] Crear `pkg/command/parse_test.go` — tests del parser - [x] Crear `pkg/command/builtins.go` — specs de los 7 comandos built-in + BuiltinNames() ### Fase 2 — Handlers en runtime (`agents/`) - [x] Agregar campos `commands`, `cmdAliases`, `startTime` al Agent struct - [x] Implementar handlers: help, tools, ping, info, version, clear, status - [x] Implementar handler `tool` — parsea args key=value, ejecuta via Registry, formatea respuesta - [x] Registrar todos los handlers en `New()` via `registerBuiltinCommands()` - [x] Modificar `handleEvent` — nuevo flujo: rules custom -> built-in -> comando desconocido -> LLM fallback - [x] Extraer `executeActions()` helper para reutilizar en ambos flujos ### Fase 3 — Simple bot support - [x] Hacer LLM opcional en `New()` (no fallar si no hay provider) - [x] 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 - [x] Eliminar regla `!help` hardcodeada de assistant-bot/agent.go - [x] Eliminar regla `!help` hardcodeada de asistente-2/agent.go - [x] 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