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>
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.Parseya detectaCommandPrefix(!) y extraeCommand+ArgsenMessageContextdecision.MatchCommand()ya existe para matchear comandos en reglastools.Registryya tieneExecute(ctx, name, argsJSON)para ejecutar tools- Cada agente define sus reglas en
agent.goconRules() []decision.Rule - El flujo actual: solo
!helpexiste como comando hardcodeado en cada agente
Problema
- Los comandos estan hardcodeados en cada
agent.gocomo 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
!helpes 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
-
Built-in commands (disponibles en todos los agentes):
Comando Descripcion !helpLista comandos disponibles (built-in + custom) !toolsLista tools registradas con descripcion !pingAlive check, responde "pong" con timestamp !statusInfo del agente: uptime, rooms activos, window sizes !infoNombre, version, descripcion del agente !clearLimpia ventana de conversacion del room actual !versionVersion del agente -
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 argsEjemplos:
!tool ssh_command host=server1 command="uptime"!tool current_time!tool knowledge_search query="como configurar"
-
Custom commands — definidos por cada agente en su
agent.govia 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,startTimeal 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()viaregisterBuiltinCommands() - 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 == nily 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
!helphardcodeada de assistant-bot/agent.go - Eliminar regla
!helphardcodeada 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