feat: sistema RegisterCommand para comandos por agente
Añade RegisterCommand(spec, handler) al Agent para que cada agente pueda registrar comandos propios (!xxx) sin modificar built-ins ni usar reglas. Cambios principales: - agents/runtime.go: nuevo método RegisterCommand + campo customSpecs - agents/commands.go: !help muestra comandos del agente en sección separada, extrae writeSpec helper, elimina customCommandRules (ya no se usan reglas para comandos) - handleEvent simplificado: los comandos se resuelven por lookup directo (built-in + registered), nunca pasan por reglas ni LLM Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+22
-22
@@ -22,7 +22,7 @@ func (a *Agent) registerBuiltinCommands() {
|
|||||||
a.commands["version"] = a.cmdVersion
|
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 {
|
func (a *Agent) cmdHelp(_ context.Context, _ decision.MessageContext) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("Comandos disponibles:\n\n")
|
b.WriteString("Comandos disponibles:\n\n")
|
||||||
@@ -32,25 +32,36 @@ func (a *Agent) cmdHelp(_ context.Context, _ decision.MessageContext) string {
|
|||||||
if spec.Hidden {
|
if spec.Hidden {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
aliases := ""
|
writeSpec(&b, spec)
|
||||||
if len(spec.Aliases) > 0 {
|
|
||||||
aliases = " (" + strings.Join(prefixAll(spec.Aliases, "!"), ", ") + ")"
|
|
||||||
}
|
|
||||||
fmt.Fprintf(&b, " %s%s — %s\n", spec.Usage, aliases, spec.Description)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom commands from agent rules (rules named with MatchCommand pattern)
|
// Agent-specific commands (registered via RegisterCommand)
|
||||||
customRules := a.customCommandRules()
|
if len(a.customSpecs) > 0 {
|
||||||
if len(customRules) > 0 {
|
|
||||||
b.WriteString("\nComandos del agente:\n")
|
b.WriteString("\nComandos del agente:\n")
|
||||||
for _, name := range customRules {
|
for _, spec := range a.customSpecs {
|
||||||
fmt.Fprintf(&b, " !%s\n", name)
|
if spec.Hidden {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
writeSpec(&b, spec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.String()
|
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.
|
// cmdTools lists all tools registered in the agent's tool registry.
|
||||||
func (a *Agent) cmdTools(_ context.Context, _ decision.MessageContext) string {
|
func (a *Agent) cmdTools(_ context.Context, _ decision.MessageContext) string {
|
||||||
names := a.toolReg.Names()
|
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)
|
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.
|
// prefixAll adds a prefix to each string in a slice.
|
||||||
func prefixAll(ss []string, prefix string) []string {
|
func prefixAll(ss []string, prefix string) []string {
|
||||||
out := make([]string, len(ss))
|
out := make([]string, len(ss))
|
||||||
|
|||||||
+29
-13
@@ -52,9 +52,10 @@ type Agent struct {
|
|||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
cryptoStore io.Closer // non-nil when E2EE is enabled; closed on shutdown
|
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
|
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
|
startTime time.Time
|
||||||
|
|
||||||
// Memory
|
// Memory
|
||||||
@@ -233,6 +234,19 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, logger *slog.Logger) (*
|
|||||||
return a, nil
|
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.
|
// SetBus attaches the agent to the inter-agent bus for orchestration.
|
||||||
// Must be called before Run().
|
// Must be called before Run().
|
||||||
func (a *Agent) SetBus(b *bus.Bus) {
|
func (a *Agent) SetBus(b *bus.Bus) {
|
||||||
@@ -420,29 +434,31 @@ func (a *Agent) handleEvent(ctx context.Context, msgCtx decision.MessageContext,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Command flow ─────────────────────────────────────────────────
|
// ── 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 != "" {
|
if msgCtx.Command != "" {
|
||||||
// 1. Custom rules from agent (can override built-ins)
|
a.logger.Info("command_received",
|
||||||
actions := decision.Evaluate(msgCtx, a.rules)
|
"command", msgCtx.Command,
|
||||||
if len(actions) > 0 {
|
"sender", msgCtx.SenderID,
|
||||||
a.logger.Debug("command matched custom rule", "command", msgCtx.Command)
|
"room", roomID,
|
||||||
a.executeActions(ctx, roomID, msgCtx, actions)
|
"args", msgCtx.Args,
|
||||||
return
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Built-in commands (resolve aliases first)
|
// Resolve aliases
|
||||||
cmdName := msgCtx.Command
|
cmdName := msgCtx.Command
|
||||||
if canonical, ok := a.cmdAliases[cmdName]; ok {
|
if canonical, ok := a.cmdAliases[cmdName]; ok {
|
||||||
cmdName = canonical
|
cmdName = canonical
|
||||||
}
|
}
|
||||||
|
|
||||||
if handler, ok := a.commands[cmdName]; ok {
|
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)
|
reply := handler(ctx, msgCtx)
|
||||||
_ = a.matrix.SendText(ctx, roomID, reply)
|
_ = a.matrix.SendText(ctx, roomID, reply)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Unknown command
|
// Unknown command — never falls through to rules or LLM
|
||||||
a.logger.Debug("unknown command", "command", msgCtx.Command)
|
a.logger.Info("command_unknown", "command", msgCtx.Command)
|
||||||
_ = a.matrix.SendText(ctx, roomID,
|
_ = a.matrix.SendText(ctx, roomID,
|
||||||
fmt.Sprintf("Comando desconocido: !%s. Usa !help para ver comandos disponibles.", msgCtx.Command))
|
fmt.Sprintf("Comando desconocido: !%s. Usa !help para ver comandos disponibles.", msgCtx.Command))
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user