merge: issue/0028-decouple-launcher — auto-discovery de agentes via init()
# Conflicts: # dev/issues/README.md
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
// Package _template es un agente plantilla (no lanzable).
|
||||
// Sirve como referencia canonica para crear nuevos agentes.
|
||||
// Al crear un nuevo agente, new-agent.sh reemplaza _template y AGENT_ID_PLACEHOLDER.
|
||||
package _template
|
||||
|
||||
import "github.com/enmanuel/agents/pkg/decision"
|
||||
import (
|
||||
"github.com/enmanuel/agents/agents"
|
||||
"github.com/enmanuel/agents/pkg/decision"
|
||||
)
|
||||
|
||||
func init() {
|
||||
agents.Register("AGENT_ID_PLACEHOLDER", Rules)
|
||||
}
|
||||
|
||||
// Rules devuelve las reglas de este agente (vacio para el template).
|
||||
func Rules() []decision.Rule {
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
package asistente2
|
||||
|
||||
import (
|
||||
"github.com/enmanuel/agents/agents"
|
||||
"github.com/enmanuel/agents/pkg/decision"
|
||||
)
|
||||
|
||||
func init() {
|
||||
agents.Register("asistente-2", Rules)
|
||||
}
|
||||
|
||||
// Rules returns the decision rules for the asistente-2 bot.
|
||||
// Note: !help is now handled by the built-in command system.
|
||||
func Rules() []decision.Rule {
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
package assistant
|
||||
|
||||
import (
|
||||
"github.com/enmanuel/agents/agents"
|
||||
"github.com/enmanuel/agents/pkg/decision"
|
||||
)
|
||||
|
||||
func init() {
|
||||
agents.Register("assistant-bot", Rules)
|
||||
}
|
||||
|
||||
// Rules returns the decision rules for the assistant bot.
|
||||
// Note: !help is now handled by the built-in command system.
|
||||
func Rules() []decision.Rule {
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
package meteorologo
|
||||
|
||||
import (
|
||||
"github.com/enmanuel/agents/agents"
|
||||
"github.com/enmanuel/agents/pkg/decision"
|
||||
)
|
||||
|
||||
func init() {
|
||||
agents.Register("meteorologo", Rules)
|
||||
}
|
||||
|
||||
// Rules returns the decision rules for the meteorologo bot.
|
||||
func Rules() []decision.Rule {
|
||||
return []decision.Rule{
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Package agents provides a global registry for agent rule factories.
|
||||
//
|
||||
// Each agent package self-registers via init() using Register.
|
||||
// The launcher retrieves rules via GetRules without importing agent
|
||||
// packages explicitly (only blank imports are needed).
|
||||
package agents
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/enmanuel/agents/pkg/decision"
|
||||
)
|
||||
|
||||
// RulesFunc is a factory that returns the decision rules for an agent.
|
||||
type RulesFunc func() []decision.Rule
|
||||
|
||||
var (
|
||||
registryMu sync.RWMutex
|
||||
registry = make(map[string]RulesFunc)
|
||||
)
|
||||
|
||||
// Register adds a rule factory for the given agent ID.
|
||||
// Intended to be called from init() in each agent package.
|
||||
// Panics if the same ID is registered twice (catches copy-paste errors early).
|
||||
func Register(id string, fn RulesFunc) {
|
||||
registryMu.Lock()
|
||||
defer registryMu.Unlock()
|
||||
|
||||
if _, exists := registry[id]; exists {
|
||||
panic("agents.Register: duplicate agent id: " + id)
|
||||
}
|
||||
registry[id] = fn
|
||||
}
|
||||
|
||||
// GetRules returns the rule factory for the given agent ID.
|
||||
// Returns nil if no rules are registered (the agent is command-only).
|
||||
func GetRules(id string) RulesFunc {
|
||||
registryMu.RLock()
|
||||
defer registryMu.RUnlock()
|
||||
return registry[id]
|
||||
}
|
||||
|
||||
// RegisteredIDs returns a sorted list of all registered agent IDs.
|
||||
// Useful for debugging and diagnostics.
|
||||
func RegisteredIDs() []string {
|
||||
registryMu.RLock()
|
||||
defer registryMu.RUnlock()
|
||||
|
||||
ids := make([]string, 0, len(registry))
|
||||
for id := range registry {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// resetRegistry clears all registrations (for testing only).
|
||||
func resetRegistry() {
|
||||
registryMu.Lock()
|
||||
defer registryMu.Unlock()
|
||||
registry = make(map[string]RulesFunc)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/enmanuel/agents/pkg/decision"
|
||||
)
|
||||
|
||||
func TestRegisterAndGetRules(t *testing.T) {
|
||||
resetRegistry()
|
||||
|
||||
called := false
|
||||
fn := func() []decision.Rule {
|
||||
called = true
|
||||
return []decision.Rule{{Name: "test-rule"}}
|
||||
}
|
||||
|
||||
Register("test-agent", fn)
|
||||
|
||||
got := GetRules("test-agent")
|
||||
if got == nil {
|
||||
t.Fatal("GetRules returned nil for registered agent")
|
||||
}
|
||||
|
||||
rules := got()
|
||||
if !called {
|
||||
t.Error("rule factory was not called")
|
||||
}
|
||||
if len(rules) != 1 || rules[0].Name != "test-rule" {
|
||||
t.Errorf("unexpected rules: %+v", rules)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRulesMissing(t *testing.T) {
|
||||
resetRegistry()
|
||||
|
||||
got := GetRules("nonexistent")
|
||||
if got != nil {
|
||||
t.Errorf("expected nil for unregistered agent, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterDuplicatePanics(t *testing.T) {
|
||||
resetRegistry()
|
||||
|
||||
fn := func() []decision.Rule { return nil }
|
||||
Register("dup-agent", fn)
|
||||
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
t.Fatal("expected panic on duplicate registration, got none")
|
||||
}
|
||||
msg, ok := r.(string)
|
||||
if !ok {
|
||||
t.Fatalf("expected string panic, got %T: %v", r, r)
|
||||
}
|
||||
if msg != "agents.Register: duplicate agent id: dup-agent" {
|
||||
t.Errorf("unexpected panic message: %s", msg)
|
||||
}
|
||||
}()
|
||||
|
||||
Register("dup-agent", fn)
|
||||
}
|
||||
|
||||
func TestRegisteredIDs(t *testing.T) {
|
||||
resetRegistry()
|
||||
|
||||
Register("charlie", func() []decision.Rule { return nil })
|
||||
Register("alpha", func() []decision.Rule { return nil })
|
||||
Register("bravo", func() []decision.Rule { return nil })
|
||||
|
||||
ids := RegisteredIDs()
|
||||
sort.Strings(ids)
|
||||
|
||||
expected := []string{"alpha", "bravo", "charlie"}
|
||||
if len(ids) != len(expected) {
|
||||
t.Fatalf("expected %d ids, got %d: %v", len(expected), len(ids), ids)
|
||||
}
|
||||
for i, id := range ids {
|
||||
if id != expected[i] {
|
||||
t.Errorf("id[%d] = %q, want %q", i, id, expected[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetRegistry(t *testing.T) {
|
||||
resetRegistry()
|
||||
|
||||
Register("temp", func() []decision.Rule { return nil })
|
||||
if GetRules("temp") == nil {
|
||||
t.Fatal("expected registered agent")
|
||||
}
|
||||
|
||||
resetRegistry()
|
||||
|
||||
if GetRules("temp") != nil {
|
||||
t.Error("expected nil after reset")
|
||||
}
|
||||
if len(RegisteredIDs()) != 0 {
|
||||
t.Error("expected empty registry after reset")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user