# Policy: Crear un comando para un agente Los comandos (`!xxx`) son respuestas directas que no pasan por reglas ni por el LLM. Siempre se resuelven primero en el flujo de eventos. ## Arquitectura del sistema de comandos ``` Usuario envía "!help" → listener.go parsea → msgCtx.Command = "help" → runtime.go handleEvent: 1. Busca en built-in commands (help, ping, tools, etc.) → match → responde → FIN 2. Si no match → "Comando desconocido" → FIN (Nunca llega a reglas ni LLM) Usuario envía "hola" → Command == "" → reglas → LLM → respuesta normal ``` ## Tipos de comandos ### Built-in (todos los agentes) Definidos en `pkg/command/builtins.go` (specs puras) y `agents/commands.go` (handlers). Todos los agentes los tienen automáticamente: `!help`, `!ping`, `!tools`, `!tool`, `!status`, `!info`, `!clear`, `!version`. **No modificar los built-in para agregar funcionalidad por agente.** Usar `RegisterCommand` en su lugar. ### Agent-specific (por agente) Se registran con `agent.RegisterCommand(spec, handler)` en el launcher, después de `devagents.New()` y antes de `agent.Run()`. ## Pasos para crear un comando de agente ### 1. Definir el handler en el paquete del agente Crear un archivo `commands.go` en `agents//`: ```go package import ( "context" "fmt" "github.com/enmanuel/agents/pkg/command" "github.com/enmanuel/agents/pkg/decision" ) // Commands returns the command specs and handlers for this agent. // Handlers are functions, but must NOT do I/O directly — they receive // dependencies via closure when registered in the launcher. func Commands() []CommandEntry { return []CommandEntry{ { Spec: command.Spec{ Name: "deploy", Aliases: []string{"d"}, Description: "Despliega al entorno indicado", Usage: "!deploy ", }, Handler: func(ctx context.Context, msgCtx decision.MessageContext) string { if len(msgCtx.Args) < 2 { return "Uso: !deploy " } env := msgCtx.Args[1] return fmt.Sprintf("Desplegando a %s...", env) }, }, } } // CommandEntry pairs a spec with its handler. type CommandEntry struct { Spec command.Spec Handler func(ctx context.Context, msgCtx decision.MessageContext) string } ``` ### 2. Registrar en el launcher (`cmd/launcher/main.go`) Después de `devagents.New()` y antes de `wg.Add(1)`: ```go a, err := devagents.New(cfg, rules, agentLogger) if err != nil { ... } // Register agent-specific commands if cfg.Agent.ID == "" { for _, cmd := range agent.Commands() { a.RegisterCommand(cmd.Spec, cmd.Handler) } } ``` ### 3. Documentar en el system prompt Si el agente tiene LLM, mencionar los comandos en `prompts/system.md` para que el LLM pueda informar al usuario: ```markdown ## Comandos disponibles - `!deploy ` — Despliega al entorno indicado - `!help` — Lista todos los comandos ``` ## API de registro ```go // En agents/runtime.go func (a *Agent) RegisterCommand(spec command.Spec, handler CommandHandler) ``` - `spec.Name`: nombre del comando (lo que va después de `!`) - `spec.Aliases`: nombres cortos alternativos (opcional) - `spec.Description`: texto que aparece en `!help` - `spec.Usage`: ejemplo de uso que aparece en `!help` - `spec.Hidden`: si es `true`, no aparece en `!help` - `handler`: recibe `(ctx, msgCtx)` y devuelve un string El handler tiene acceso a: - `msgCtx.Args` — argumentos parseados por `strings.Fields` (incluye el nombre del comando en `Args[0]` solo si viene de `!tool xxx`) - `msgCtx.Command` — nombre del comando (ya sin `!`) - `msgCtx.SenderID`, `msgCtx.RoomID`, `msgCtx.IsDirectMsg`, etc. ## Prioridad de resolución ``` 1. Built-in commands (help, ping, tools, etc.) ← siempre ganan 2. Agent-specific commands (RegisterCommand) ← segundo 3. Si no hay match → "Comando desconocido" ← nunca llega al LLM ``` Un agent-specific command **no puede** sobrescribir un built-in. Si se registra un comando con el mismo nombre que un built-in, el built-in prevalece. ## Comandos sin prefijo (robots) Los robots (`agent.type: robot`) pueden configurarse con `command_prefix: ""` para aceptar comandos sin el prefijo `!`. En este modo: - Todo mensaje se trata como un posible comando (el primer token es el nombre del comando) - `!help` sigue funcionando por retrocompatibilidad (el `!` se quita automaticamente) - Si el comando no existe, se responde "Comando desconocido" con sugerencia de usar `help` - El `!help` del robot muestra los comandos sin prefijo cuando `command_prefix` es vacio ```yaml # config.yaml del robot matrix: filters: command_prefix: "" # sin prefijo — todo mensaje es potencial comando ``` **Solo para robots.** Los agentes con LLM necesitan `command_prefix: "!"` para distinguir comandos de mensajes naturales. ## Reglas - **No usar reglas (`agent.go`) para comandos.** Las reglas son para lógica de decisión sobre mensajes normales. - **Los handlers pueden ser impuros** (HTTP, SSH, etc.) — se ejecutan en el contexto del runtime. - **Respuesta siempre es string** — el runtime lo envía por Matrix automáticamente. - **Validar argumentos** al inicio del handler y devolver usage si faltan. - **Logs automáticos** — el runtime loguea `command_received` y `command_executed` a nivel INFO. - **`msgCtx.Args`** para `!deploy prod` contiene `["prod"]` (sin el nombre del comando). ## Verificación - [ ] `go build -tags goolm ./...` compila - [ ] `!help` muestra el comando nuevo en la sección "Comandos del agente" - [ ] Enviar el comando por Matrix produce la respuesta esperada - [ ] En logs aparece `command_received` y `command_executed`