feat: enable dynamic swarm event relaying to original channels (Telegram/Discord)

This commit is contained in:
Danieldd28 2026-02-11 02:26:40 +07:00
parent e4700cd201
commit acac61ac2f
5 changed files with 55 additions and 22 deletions

View file

@ -84,9 +84,9 @@ func (al *AgentLoop) Run(ctx context.Context) error {
go func() { go func() {
for msg := range al.swarm.Outbound { for msg := range al.swarm.Outbound {
al.bus.PublishOutbound(bus.OutboundMessage{ al.bus.PublishOutbound(bus.OutboundMessage{
Channel: "cli", // Default to CLI for now, ideally dynamic Channel: msg.Channel,
ChatID: "direct", ChatID: msg.ChatID,
Content: msg, Content: msg.Content,
}) })
} }
}() }()
@ -104,7 +104,7 @@ func (al *AgentLoop) Run(ctx context.Context) error {
// Intercept /swarm commands // Intercept /swarm commands
if al.swarm != nil && len(msg.Content) > 6 && msg.Content[:7] == "/swarm " { if al.swarm != nil && len(msg.Content) > 6 && msg.Content[:7] == "/swarm " {
response := al.swarm.HandleCommand(ctx, msg.Content) response := al.swarm.HandleCommand(ctx, msg.Channel, msg.ChatID, msg.Content)
al.bus.PublishOutbound(bus.OutboundMessage{ al.bus.PublishOutbound(bus.OutboundMessage{
Channel: msg.Channel, Channel: msg.Channel,
ChatID: msg.ChatID, ChatID: msg.ChatID,
@ -141,7 +141,7 @@ func (al *AgentLoop) Stop() {
func (al *AgentLoop) ProcessDirect(ctx context.Context, content, sessionKey string) (string, error) { func (al *AgentLoop) ProcessDirect(ctx context.Context, content, sessionKey string) (string, error) {
// Intercept /swarm commands // Intercept /swarm commands
if al.swarm != nil && len(content) > 6 && content[:7] == "/swarm " { if al.swarm != nil && len(content) > 6 && content[:7] == "/swarm " {
return al.swarm.HandleCommand(ctx, content), nil return al.swarm.HandleCommand(ctx, "cli", "direct", content), nil
} }
msg := bus.InboundMessage{ msg := bus.InboundMessage{

View file

@ -36,10 +36,12 @@ const (
// --- Core Structs --- // --- Core Structs ---
type Swarm struct { type Swarm struct {
ID SwarmID `json:"id"` ID SwarmID `json:"id"`
Goal string `json:"goal"` Goal string `json:"goal"`
Status SwarmStatus `json:"status"` Status SwarmStatus `json:"status"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
OriginChannel string `json:"origin_channel"`
OriginChatID string `json:"origin_chat_id"`
} }
type Node struct { type Node struct {
@ -126,3 +128,10 @@ type FactResult struct {
Content string `json:"content"` Content string `json:"content"`
Score float32 `json:"score"` Score float32 `json:"score"`
} }
// --- Relay Types ---
type RelayMessage struct {
Channel string
ChatID string
Content string
}

View file

@ -24,7 +24,7 @@ func NewSQLiteStore(dbPath string) (*SQLiteStore, error) {
} }
func (s *SQLiteStore) init() error { func (s *SQLiteStore) init() error {
if _, err := s.db.Exec(`CREATE TABLE IF NOT EXISTS swarms (id TEXT PRIMARY KEY, goal TEXT, status TEXT, created_at DATETIME);`); err != nil { if _, err := s.db.Exec(`CREATE TABLE IF NOT EXISTS swarms (id TEXT PRIMARY KEY, goal TEXT, status TEXT, created_at DATETIME, origin_channel TEXT, origin_chat_id TEXT);`); err != nil {
return err return err
} }
if _, err := s.db.Exec(`CREATE TABLE IF NOT EXISTS nodes (id TEXT PRIMARY KEY, swarm_id TEXT, parent_id TEXT, role JSON, task TEXT, status TEXT, output TEXT, stats JSON);`); err != nil { if _, err := s.db.Exec(`CREATE TABLE IF NOT EXISTS nodes (id TEXT PRIMARY KEY, swarm_id TEXT, parent_id TEXT, role JSON, task TEXT, status TEXT, output TEXT, stats JSON);`); err != nil {
@ -37,14 +37,14 @@ func (s *SQLiteStore) init() error {
} }
func (s *SQLiteStore) CreateSwarm(ctx context.Context, sw *core.Swarm) error { func (s *SQLiteStore) CreateSwarm(ctx context.Context, sw *core.Swarm) error {
_, err := s.db.ExecContext(ctx, "INSERT INTO swarms (id, goal, status, created_at) VALUES (?, ?, ?, ?)", sw.ID, sw.Goal, sw.Status, sw.CreatedAt) _, err := s.db.ExecContext(ctx, "INSERT INTO swarms (id, goal, status, created_at, origin_channel, origin_chat_id) VALUES (?, ?, ?, ?, ?, ?)", sw.ID, sw.Goal, sw.Status, sw.CreatedAt, sw.OriginChannel, sw.OriginChatID)
return err return err
} }
func (s *SQLiteStore) GetSwarm(ctx context.Context, id core.SwarmID) (*core.Swarm, error) { func (s *SQLiteStore) GetSwarm(ctx context.Context, id core.SwarmID) (*core.Swarm, error) {
row := s.db.QueryRowContext(ctx, "SELECT id, goal, status, created_at FROM swarms WHERE id=?", id) row := s.db.QueryRowContext(ctx, "SELECT id, goal, status, created_at, origin_channel, origin_chat_id FROM swarms WHERE id=?", id)
var sw core.Swarm var sw core.Swarm
if err := row.Scan(&sw.ID, &sw.Goal, &sw.Status, &sw.CreatedAt); err != nil { if err := row.Scan(&sw.ID, &sw.Goal, &sw.Status, &sw.CreatedAt, &sw.OriginChannel, &sw.OriginChatID); err != nil {
return nil, err return nil, err
} }
return &sw, nil return &sw, nil
@ -56,7 +56,7 @@ func (s *SQLiteStore) UpdateSwarm(ctx context.Context, sw *core.Swarm) error {
} }
func (s *SQLiteStore) ListSwarms(ctx context.Context, status core.SwarmStatus) ([]*core.Swarm, error) { func (s *SQLiteStore) ListSwarms(ctx context.Context, status core.SwarmStatus) ([]*core.Swarm, error) {
rows, err := s.db.QueryContext(ctx, "SELECT id, goal, status, created_at FROM swarms WHERE status=?", status) rows, err := s.db.QueryContext(ctx, "SELECT id, goal, status, created_at, origin_channel, origin_chat_id FROM swarms WHERE status=?", status)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -64,7 +64,7 @@ func (s *SQLiteStore) ListSwarms(ctx context.Context, status core.SwarmStatus) (
var list []*core.Swarm var list []*core.Swarm
for rows.Next() { for rows.Next() {
var sw core.Swarm var sw core.Swarm
if err := rows.Scan(&sw.ID, &sw.Goal, &sw.Status, &sw.CreatedAt); err != nil { if err := rows.Scan(&sw.ID, &sw.Goal, &sw.Status, &sw.CreatedAt, &sw.OriginChannel, &sw.OriginChatID); err != nil {
return nil, err return nil, err
} }
list = append(list, &sw) list = append(list, &sw)

View file

@ -39,9 +39,12 @@ func NewOrchestrator(store core.SwarmStore, bus core.EventBus, llm core.LLMClien
func (o *Orchestrator) SetSharedMemory(m core.SharedMemory) { o.memory = m } func (o *Orchestrator) SetSharedMemory(m core.SharedMemory) { o.memory = m }
func (o *Orchestrator) SpawnSwarm(ctx context.Context, goal string) (core.SwarmID, error) { func (o *Orchestrator) SpawnSwarm(ctx context.Context, goal, channel, chatID string) (core.SwarmID, error) {
id := core.SwarmID(uuid.New().String()) id := core.SwarmID(uuid.New().String())
if err := o.store.CreateSwarm(ctx, &core.Swarm{ID: id, Goal: goal, Status: core.SwarmStatusActive, CreatedAt: time.Now()}); err != nil { if err := o.store.CreateSwarm(ctx, &core.Swarm{
ID: id, Goal: goal, Status: core.SwarmStatusActive, CreatedAt: time.Now(),
OriginChannel: channel, OriginChatID: chatID,
}); err != nil {
return "", fmt.Errorf("failed to create swarm in store: %w", err) return "", fmt.Errorf("failed to create swarm in store: %w", err)
} }

View file

@ -19,7 +19,7 @@ type Service struct {
Orchestrator *runtime.Orchestrator Orchestrator *runtime.Orchestrator
Store core.SwarmStore Store core.SwarmStore
Bus core.EventBus Bus core.EventBus
Outbound chan string Outbound chan core.RelayMessage
} }
func NewService(dbPath string, provider providers.LLMProvider, registry *tools.ToolRegistry, cfg config.SwarmConfig, model string) (*Service, error) { func NewService(dbPath string, provider providers.LLMProvider, registry *tools.ToolRegistry, cfg config.SwarmConfig, model string) (*Service, error) {
@ -35,7 +35,7 @@ func NewService(dbPath string, provider providers.LLMProvider, registry *tools.T
orch := runtime.NewOrchestrator(store, eventBus, adapter, registry, cfg, model) orch := runtime.NewOrchestrator(store, eventBus, adapter, registry, cfg, model)
orch.SetSharedMemory(sharedMem) orch.SetSharedMemory(sharedMem)
s := &Service{Orchestrator: orch, Store: store, Bus: eventBus, Outbound: make(chan string, 100)} s := &Service{Orchestrator: orch, Store: store, Bus: eventBus, Outbound: make(chan core.RelayMessage, 100)}
s.listen() s.listen()
return s, nil return s, nil
} }
@ -49,21 +49,42 @@ func (s *Service) listen() {
case core.EventNodeFailed: msg = fmt.Sprintf("❌ [%s] Failed: %v", e.NodeID[:4], e.Payload["error"]) case core.EventNodeFailed: msg = fmt.Sprintf("❌ [%s] Failed: %v", e.NodeID[:4], e.Payload["error"])
} }
if msg != "" { if msg != "" {
select { case s.Outbound <- msg: default: } // Get swarm origin
sw, err := s.Store.GetSwarm(context.Background(), e.SwarmID)
if err == nil && sw.OriginChannel != "" {
select {
case s.Outbound <- core.RelayMessage{
Channel: sw.OriginChannel,
ChatID: sw.OriginChatID,
Content: msg,
}:
default:
}
} else {
// Fallback to CLI
select {
case s.Outbound <- core.RelayMessage{
Channel: "cli",
ChatID: "direct",
Content: msg,
}:
default:
}
}
} }
}); err != nil { }); err != nil {
fmt.Printf("Warning: Swarm event subscription failed: %v\n", err) fmt.Printf("Warning: Swarm event subscription failed: %v\n", err)
} }
} }
func (s *Service) HandleCommand(ctx context.Context, input string) string { func (s *Service) HandleCommand(ctx context.Context, channel, chatID, input string) string {
args := strings.Fields(input) args := strings.Fields(input)
if len(args) < 2 { return "Usage: /swarm <spawn|list|stop|status|viz> [goal/id]" } if len(args) < 2 { return "Usage: /swarm <spawn|list|stop|status|viz> [goal/id]" }
switch args[1] { switch args[1] {
case "spawn": case "spawn":
goal := strings.Join(args[2:], " ") goal := strings.Join(args[2:], " ")
id, err := s.Orchestrator.SpawnSwarm(ctx, goal) id, err := s.Orchestrator.SpawnSwarm(ctx, goal, channel, chatID)
if err != nil { if err != nil {
return fmt.Sprintf("❌ Failed to spawn swarm: %v", err) return fmt.Sprintf("❌ Failed to spawn swarm: %v", err)
} }