diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 8b6bdf4..7868104 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -68,6 +68,7 @@ Ver índice completo en `.claude/policies/index.md`. |----------|------------------| | `.claude/policies/create_agent.md` | Al crear un nuevo bot/agente Matrix | | `.claude/policies/create_tool.md` | Al añadir una nueva tool para function calling | +| `.claude/policies/create_command.md` | Al añadir un comando directo (!xxx) a un agente | Documentación detallada para humanos en `docs/creating-agents.md`. diff --git a/.claude/policies/create_agent.md b/.claude/policies/create_agent.md index 8b1fe9a..5763c8a 100644 --- a/.claude/policies/create_agent.md +++ b/.claude/policies/create_agent.md @@ -40,16 +40,9 @@ import "github.com/enmanuel/agents/pkg/decision" func Rules() []decision.Rule { return []decision.Rule{ + // Any DM or mention → LLM { - Name: "help", - Match: decision.MatchCommand("help"), - Actions: []decision.Action{{ - Kind: decision.ActionKindReply, - Reply: &decision.ReplyAction{Content: ""}, - }}, - }, - { - Name: "llm-fallback", + Name: "llm-all", Match: func(ctx decision.MessageContext) bool { return ctx.IsDirectMsg || ctx.IsMention }, @@ -65,10 +58,10 @@ func Rules() []decision.Rule { **Reglas estrictas:** - **PURO**: solo imports de `pkg/decision`, cero I/O, cero side effects - Package name = ID sin guiones ni `_bot` (e.g. `monitor-bot` → `package monitor`) -- Reglas se evalúan en orden — específicas arriba, catch-all al final -- El catch-all DEBE cubrir `ctx.IsDirectMsg || ctx.IsMention` +- **No usar reglas para comandos** (`!help`, `!ping`, etc.) — los comandos se gestionan via `RegisterCommand` (ver policy `create_command.md`) +- Las reglas solo aplican a mensajes normales (sin prefijo `!`) -Para añadir reglas extra, insertar antes del catch-all. Tipos de acción disponibles: +Tipos de acción disponibles: - `ActionKindReply` — respuesta estática (con `ReplyAction{Content: "..."}`) - `ActionKindLLM` — pasa al LLM (con `LLMAction{}`) diff --git a/.claude/policies/create_command.md b/.claude/policies/create_command.md new file mode 100644 index 0000000..f328c77 --- /dev/null +++ b/.claude/policies/create_command.md @@ -0,0 +1,149 @@ +# 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 `agents.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 `agents.New()` y antes de `wg.Add(1)`: + +```go +a, err := agents.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. + +## 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` diff --git a/.claude/policies/index.md b/.claude/policies/index.md index 471c8c2..eb499b4 100644 --- a/.claude/policies/index.md +++ b/.claude/policies/index.md @@ -8,11 +8,13 @@ Guías operativas para LLMs que trabajan en este codebase. Cada política descri |----------|---------|------------------| | **Crear agente** | [create_agent.md](create_agent.md) | Al crear un nuevo bot/agente Matrix completo | | **Crear herramienta** | [create_tool.md](create_tool.md) | Al añadir una nueva tool para LLM function calling | +| **Crear comando** | [create_command.md](create_command.md) | Al añadir un comando directo (!xxx) a un agente | ## Cuándo consultar las políticas - **Crear agente**: cuando el usuario pida crear un nuevo bot, agente, o asistente. Incluye la estructura de archivos, reglas puras, config YAML, system prompt y registro en el launcher. - **Crear herramienta**: cuando el usuario pida añadir una nueva herramienta/tool al sistema. Incluye el patrón Def (puro) + Exec (impuro), registro en runtime.go y habilitación en config. +- **Crear comando**: cuando el usuario pida añadir un comando directo (!xxx) a un agente. Los comandos se resuelven sin pasar por reglas ni LLM. ## Principio general