diff --git a/agents/commands.go b/agents/commands.go index 4200d12..562566f 100644 --- a/agents/commands.go +++ b/agents/commands.go @@ -22,7 +22,7 @@ func (a *Agent) registerBuiltinCommands() { a.commands["version"] = a.cmdVersion } -// cmdHelp lists all available commands (built-in + custom rules with MatchCommand). +// cmdHelp lists all available commands (built-in + agent-specific). func (a *Agent) cmdHelp(_ context.Context, _ decision.MessageContext) string { var b strings.Builder b.WriteString("Comandos disponibles:\n\n") @@ -32,25 +32,36 @@ func (a *Agent) cmdHelp(_ context.Context, _ decision.MessageContext) string { if spec.Hidden { continue } - aliases := "" - if len(spec.Aliases) > 0 { - aliases = " (" + strings.Join(prefixAll(spec.Aliases, "!"), ", ") + ")" - } - fmt.Fprintf(&b, " %s%s — %s\n", spec.Usage, aliases, spec.Description) + writeSpec(&b, spec) } - // Custom commands from agent rules (rules named with MatchCommand pattern) - customRules := a.customCommandRules() - if len(customRules) > 0 { + // Agent-specific commands (registered via RegisterCommand) + if len(a.customSpecs) > 0 { b.WriteString("\nComandos del agente:\n") - for _, name := range customRules { - fmt.Fprintf(&b, " !%s\n", name) + for _, spec := range a.customSpecs { + if spec.Hidden { + continue + } + writeSpec(&b, spec) } } return b.String() } +// writeSpec formats a single command spec for the help output. +func writeSpec(b *strings.Builder, spec command.Spec) { + aliases := "" + if len(spec.Aliases) > 0 { + aliases = " (" + strings.Join(prefixAll(spec.Aliases, "!"), ", ") + ")" + } + usage := spec.Usage + if usage == "" { + usage = "!" + spec.Name + } + fmt.Fprintf(b, " %s%s — %s\n", usage, aliases, spec.Description) +} + // cmdTools lists all tools registered in the agent's tool registry. func (a *Agent) cmdTools(_ context.Context, _ decision.MessageContext) string { names := a.toolReg.Names() @@ -159,17 +170,6 @@ func (a *Agent) cmdVersion(_ context.Context, _ decision.MessageContext) string return fmt.Sprintf("%s %s", a.cfg.Agent.Name, v) } -// customCommandRules extracts rule names that look like command handlers. -func (a *Agent) customCommandRules() []string { - var names []string - for _, r := range a.rules { - if r.Name != "" { - names = append(names, r.Name) - } - } - return names -} - // prefixAll adds a prefix to each string in a slice. func prefixAll(ss []string, prefix string) []string { out := make([]string, len(ss)) diff --git a/agents/runtime.go b/agents/runtime.go index 2bfec41..e51b902 100644 --- a/agents/runtime.go +++ b/agents/runtime.go @@ -52,9 +52,10 @@ type Agent struct { logger *slog.Logger cryptoStore io.Closer // non-nil when E2EE is enabled; closed on shutdown - // Commands — built-in command handlers keyed by name (including aliases) + // Commands — handlers keyed by canonical name; cmdAliases maps alias → canonical commands map[string]CommandHandler - cmdAliases map[string]string // alias → canonical name + cmdAliases map[string]string // alias → canonical name + customSpecs []command.Spec // specs from RegisterCommand (for !help) startTime time.Time // Memory @@ -233,6 +234,19 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, logger *slog.Logger) (* return a, nil } +// RegisterCommand adds a custom command handler for this agent. +// The spec provides metadata (aliases, description, usage) for !help. +// Must be called before Run(). +func (a *Agent) RegisterCommand(spec command.Spec, handler CommandHandler) { + a.commands[spec.Name] = handler + a.cmdAliases[spec.Name] = spec.Name + for _, alias := range spec.Aliases { + a.cmdAliases[alias] = spec.Name + } + a.customSpecs = append(a.customSpecs, spec) + a.logger.Info("command_registered", "command", spec.Name, "aliases", spec.Aliases) +} + // SetBus attaches the agent to the inter-agent bus for orchestration. // Must be called before Run(). func (a *Agent) SetBus(b *bus.Bus) { @@ -420,29 +434,31 @@ func (a *Agent) handleEvent(ctx context.Context, msgCtx decision.MessageContext, } // ── Command flow ───────────────────────────────────────────────── + // Commands (!xxx) always resolve before rules or LLM. Never reach the LLM. + // Priority: built-in → unknown (agent-specific commands can be added via RegisterCommand). if msgCtx.Command != "" { - // 1. Custom rules from agent (can override built-ins) - actions := decision.Evaluate(msgCtx, a.rules) - if len(actions) > 0 { - a.logger.Debug("command matched custom rule", "command", msgCtx.Command) - a.executeActions(ctx, roomID, msgCtx, actions) - return - } + a.logger.Info("command_received", + "command", msgCtx.Command, + "sender", msgCtx.SenderID, + "room", roomID, + "args", msgCtx.Args, + ) - // 2. Built-in commands (resolve aliases first) + // Resolve aliases cmdName := msgCtx.Command if canonical, ok := a.cmdAliases[cmdName]; ok { cmdName = canonical } + if handler, ok := a.commands[cmdName]; ok { - a.logger.Debug("executing built-in command", "command", cmdName) + a.logger.Info("command_executed", "command", cmdName) reply := handler(ctx, msgCtx) _ = a.matrix.SendText(ctx, roomID, reply) return } - // 3. Unknown command - a.logger.Debug("unknown command", "command", msgCtx.Command) + // Unknown command — never falls through to rules or LLM + a.logger.Info("command_unknown", "command", msgCtx.Command) _ = a.matrix.SendText(ctx, roomID, fmt.Sprintf("Comando desconocido: !%s. Usa !help para ver comandos disponibles.", msgCtx.Command)) return