feat: update orchestrator for enhanced multi-bot management and room discovery
This commit is contained in:
@@ -126,7 +126,7 @@ matrix:
|
|||||||
homeserver: "https://matrix-af2f3d.organic-machine.com"
|
homeserver: "https://matrix-af2f3d.organic-machine.com"
|
||||||
user_id: "@asistente-2:matrix-af2f3d.organic-machine.com"
|
user_id: "@asistente-2:matrix-af2f3d.organic-machine.com"
|
||||||
access_token_env: MATRIX_TOKEN_ASISTENTE_2
|
access_token_env: MATRIX_TOKEN_ASISTENTE_2
|
||||||
device_id: "XUGTSZJYFQ"
|
device_id: "IVECMVQWNZ"
|
||||||
|
|
||||||
encryption:
|
encryption:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ matrix:
|
|||||||
homeserver: "https://matrix-af2f3d.organic-machine.com"
|
homeserver: "https://matrix-af2f3d.organic-machine.com"
|
||||||
user_id: "@assistant-bot:matrix-af2f3d.organic-machine.com"
|
user_id: "@assistant-bot:matrix-af2f3d.organic-machine.com"
|
||||||
access_token_env: MATRIX_TOKEN_ASSISTANT_BOT
|
access_token_env: MATRIX_TOKEN_ASSISTANT_BOT
|
||||||
device_id: "SMWMRKMHDH"
|
device_id: "WXAKFKILMR"
|
||||||
|
|
||||||
encryption:
|
encryption:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
|
|
||||||
"github.com/enmanuel/agents/internal/config"
|
"github.com/enmanuel/agents/internal/config"
|
||||||
@@ -197,6 +198,16 @@ func (a *Agent) SetInterceptor(fn matrix.InterceptFunc) {
|
|||||||
a.listener.SetInterceptor(fn)
|
a.listener.SetInterceptor(fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetMembershipNotify registers a callback for room membership changes.
|
||||||
|
func (a *Agent) SetMembershipNotify(fn matrix.MembershipNotifyFunc) {
|
||||||
|
a.listener.SetMembershipNotify(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RawMatrixClient returns the underlying *mautrix.Client for room scanning.
|
||||||
|
func (a *Agent) RawMatrixClient() *mautrix.Client {
|
||||||
|
return a.matrix.Raw()
|
||||||
|
}
|
||||||
|
|
||||||
// Run starts the agent sync loop. Blocks until ctx is cancelled.
|
// Run starts the agent sync loop. Blocks until ctx is cancelled.
|
||||||
func (a *Agent) Run(ctx context.Context) error {
|
func (a *Agent) Run(ctx context.Context) error {
|
||||||
if a.cryptoStore != nil {
|
if a.cryptoStore != nil {
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ special:
|
|||||||
|
|
||||||
llm:
|
llm:
|
||||||
primary:
|
primary:
|
||||||
provider: anthropic
|
provider: openai
|
||||||
model: claude-sonnet-4-6
|
model: gpt-4o
|
||||||
api_key_env: ANTHROPIC_API_KEY
|
api_key_env: OPENAI_API_KEY
|
||||||
max_tokens: 512
|
max_tokens: 512
|
||||||
temperature: 0.2
|
temperature: 0.2
|
||||||
|
|
||||||
@@ -16,8 +16,4 @@ orchestration:
|
|||||||
max_iterations: 3
|
max_iterations: 3
|
||||||
quality_threshold: 0.8
|
quality_threshold: 0.8
|
||||||
delegation_timeout: 30s
|
delegation_timeout: 30s
|
||||||
rooms:
|
rooms: [] # auto-detected: any room with ≥2 registered bots is managed automatically
|
||||||
- room_id: "${MATRIX_ROOM_SHARED}"
|
|
||||||
participants:
|
|
||||||
- assistant-bot
|
|
||||||
- asistente-2
|
|
||||||
|
|||||||
+25
-11
@@ -14,6 +14,9 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
@@ -70,13 +73,14 @@ func main() {
|
|||||||
// Non-fatal: orchestration is optional
|
// Non-fatal: orchestration is optional
|
||||||
logger.Warn("orchestrator not started", "err", err)
|
logger.Warn("orchestrator not started", "err", err)
|
||||||
} else {
|
} else {
|
||||||
logger.Info("orchestrator ready",
|
logger.Info("orchestrator initialized")
|
||||||
"managed_rooms", len(orch.cfg.Orchestration.Rooms),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Start normal agents ──
|
// ── Start normal agents ──
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
var scannerOnce sync.Once
|
||||||
|
var scanner *mautrix.Client
|
||||||
|
|
||||||
for _, path := range configPaths {
|
for _, path := range configPaths {
|
||||||
path := path
|
path := path
|
||||||
cfg, err := config.Load(path)
|
cfg, err := config.Load(path)
|
||||||
@@ -101,20 +105,22 @@ func main() {
|
|||||||
// Connect agent to bus for orchestration
|
// Connect agent to bus for orchestration
|
||||||
a.SetBus(agentBus)
|
a.SetBus(agentBus)
|
||||||
|
|
||||||
// If orchestrator is active, set interceptor so bots don't
|
// If orchestrator is active, wire interceptor and membership notify
|
||||||
// handle events directly in orchestrated rooms.
|
|
||||||
// The first bot's listener to receive the event will trigger orchestration.
|
|
||||||
if orch != nil {
|
if orch != nil {
|
||||||
a.SetInterceptor(orch.orchestrator.Intercept)
|
a.SetInterceptor(orch.orchestrator.Intercept)
|
||||||
}
|
a.SetMembershipNotify(orch.orchestrator.NotifyMembership)
|
||||||
|
|
||||||
// Register this agent as a participant in the orchestrator
|
|
||||||
if orch != nil {
|
|
||||||
orch.orchestrator.RegisterParticipant(orchestration.ParticipantInfo{
|
orch.orchestrator.RegisterParticipant(orchestration.ParticipantInfo{
|
||||||
ID: cfg.Agent.ID,
|
ID: cfg.Agent.ID,
|
||||||
Description: cfg.Agent.Description,
|
MatrixUserID: cfg.Matrix.UserID,
|
||||||
|
Description: cfg.Agent.Description,
|
||||||
Capabilities: cfg.Agent.Tags,
|
Capabilities: cfg.Agent.Tags,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Grab the first available Matrix client for room scanning
|
||||||
|
scannerOnce.Do(func() {
|
||||||
|
scanner = a.RawMatrixClient()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
@@ -127,6 +133,14 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Startup room scan (after all participants are registered) ──
|
||||||
|
if orch != nil && scanner != nil {
|
||||||
|
orch.orchestrator.SetScanner(scanner)
|
||||||
|
scanCtx, scanCancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
|
orch.orchestrator.ScanExistingRooms(scanCtx)
|
||||||
|
scanCancel()
|
||||||
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
logger.Info("all agents stopped")
|
logger.Info("all agents stopped")
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ type ContextMessage struct {
|
|||||||
// ParticipantInfo describes a bot available for routing.
|
// ParticipantInfo describes a bot available for routing.
|
||||||
type ParticipantInfo struct {
|
type ParticipantInfo struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
MatrixUserID string `json:"matrix_user_id"` // e.g. "@assistant-bot:server"
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Capabilities []string `json:"capabilities,omitempty"`
|
Capabilities []string `json:"capabilities,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,15 +23,20 @@ type EventHandler func(ctx context.Context, msgCtx decision.MessageContext, evt
|
|||||||
// delivered to the bot's normal handler (the orchestrator handles it instead).
|
// delivered to the bot's normal handler (the orchestrator handles it instead).
|
||||||
type InterceptFunc func(ctx context.Context, msgCtx decision.MessageContext) bool
|
type InterceptFunc func(ctx context.Context, msgCtx decision.MessageContext) bool
|
||||||
|
|
||||||
|
// MembershipNotifyFunc is called when a StateMember event fires.
|
||||||
|
// Used by the orchestrator to detect when bots join or leave rooms.
|
||||||
|
type MembershipNotifyFunc func(ctx context.Context, roomID, userID, membership string)
|
||||||
|
|
||||||
// Listener attaches to a mautrix syncer and dispatches events to an EventHandler.
|
// Listener attaches to a mautrix syncer and dispatches events to an EventHandler.
|
||||||
type Listener struct {
|
type Listener struct {
|
||||||
client *Client
|
client *Client
|
||||||
cfg config.MatrixCfg
|
cfg config.MatrixCfg
|
||||||
handler EventHandler
|
handler EventHandler
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
dmCache map[id.RoomID]bool
|
dmCache map[id.RoomID]bool
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
interceptor InterceptFunc // if set and returns true, event is forwarded to orchestrator
|
interceptor InterceptFunc // if set and returns true, event is forwarded to orchestrator
|
||||||
|
membershipNotify MembershipNotifyFunc // if set, called on all StateMember events
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewListener creates a Listener for the given client.
|
// NewListener creates a Listener for the given client.
|
||||||
@@ -52,6 +57,11 @@ func (l *Listener) SetInterceptor(fn InterceptFunc) {
|
|||||||
l.interceptor = fn
|
l.interceptor = fn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetMembershipNotify registers a callback for StateMember events.
|
||||||
|
func (l *Listener) SetMembershipNotify(fn MembershipNotifyFunc) {
|
||||||
|
l.membershipNotify = fn
|
||||||
|
}
|
||||||
|
|
||||||
// Run starts the Matrix sync loop. Blocks until ctx is cancelled.
|
// Run starts the Matrix sync loop. Blocks until ctx is cancelled.
|
||||||
func (l *Listener) Run(ctx context.Context) error {
|
func (l *Listener) Run(ctx context.Context) error {
|
||||||
syncer := l.client.raw.Syncer.(*mautrix.DefaultSyncer)
|
syncer := l.client.raw.Syncer.(*mautrix.DefaultSyncer)
|
||||||
@@ -59,10 +69,19 @@ func (l *Listener) Run(ctx context.Context) error {
|
|||||||
// Auto-join rooms when invited. Without this, the bot stays in "invited"
|
// Auto-join rooms when invited. Without this, the bot stays in "invited"
|
||||||
// state and never receives m.room.message events.
|
// state and never receives m.room.message events.
|
||||||
syncer.OnEventType(event.StateMember, func(ctx context.Context, evt *event.Event) {
|
syncer.OnEventType(event.StateMember, func(ctx context.Context, evt *event.Event) {
|
||||||
if evt.GetStateKey() != l.cfg.UserID {
|
stateKey := evt.GetStateKey()
|
||||||
|
membership := evt.Content.AsMember().Membership
|
||||||
|
|
||||||
|
// Notify orchestrator of all membership changes (for any user)
|
||||||
|
if l.membershipNotify != nil {
|
||||||
|
l.membershipNotify(ctx, evt.RoomID.String(), stateKey, string(membership))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-join: only process invites for ourselves
|
||||||
|
if stateKey != l.cfg.UserID {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if evt.Content.AsMember().Membership != event.MembershipInvite {
|
if membership != event.MembershipInvite {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l.logger.Info("received room invite, joining", "room", evt.RoomID, "inviter", evt.Sender)
|
l.logger.Info("received room invite, joining", "room", evt.RoomID, "inviter", evt.Sender)
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
"github.com/enmanuel/agents/internal/config"
|
"github.com/enmanuel/agents/internal/config"
|
||||||
"github.com/enmanuel/agents/pkg/decision"
|
"github.com/enmanuel/agents/pkg/decision"
|
||||||
coretypes "github.com/enmanuel/agents/pkg/llm"
|
coretypes "github.com/enmanuel/agents/pkg/llm"
|
||||||
@@ -20,16 +23,31 @@ import (
|
|||||||
shelllm "github.com/enmanuel/agents/shell/llm"
|
shelllm "github.com/enmanuel/agents/shell/llm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RoomScanner is a read-only view of Matrix rooms and members.
|
||||||
|
// Satisfied by *mautrix.Client.
|
||||||
|
type RoomScanner interface {
|
||||||
|
JoinedRooms(ctx context.Context) (*mautrix.RespJoinedRooms, error)
|
||||||
|
JoinedMembers(ctx context.Context, roomID id.RoomID) (*mautrix.RespJoinedMembers, error)
|
||||||
|
}
|
||||||
|
|
||||||
// Orchestrator coordinates multi-bot rooms. It has no Matrix identity —
|
// Orchestrator coordinates multi-bot rooms. It has no Matrix identity —
|
||||||
// it intercepts events before they reach bots and delegates via the bus.
|
// it intercepts events before they reach bots and delegates via the bus.
|
||||||
type Orchestrator struct {
|
type Orchestrator struct {
|
||||||
cfg *config.SpecialConfig
|
cfg *config.SpecialConfig
|
||||||
llm coretypes.CompleteFunc
|
llm coretypes.CompleteFunc
|
||||||
bus *bus.Bus
|
bus *bus.Bus
|
||||||
managedRooms map[string][]string // roomID → []botID
|
|
||||||
participants map[string]orchestration.ParticipantInfo // botID → info
|
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
|
||||||
|
// mu protects managedRooms, participants, and knownBotIDs.
|
||||||
|
mu sync.RWMutex
|
||||||
|
managedRooms map[string][]string // roomID → []botID
|
||||||
|
staticRooms map[string]struct{} // rooms from YAML (never auto-removed)
|
||||||
|
participants map[string]orchestration.ParticipantInfo // botID → info
|
||||||
|
knownBotIDs map[string]string // matrixUserID → botID
|
||||||
|
|
||||||
|
// Scanner for room/member discovery (set via SetScanner after startup)
|
||||||
|
scanner RoomScanner
|
||||||
|
|
||||||
// Prompts loaded from files
|
// Prompts loaded from files
|
||||||
routingPrompt string
|
routingPrompt string
|
||||||
qualityPrompt string
|
qualityPrompt string
|
||||||
@@ -49,8 +67,13 @@ func New(cfg *config.SpecialConfig, agentBus *bus.Bus, logger *slog.Logger) (*Or
|
|||||||
}
|
}
|
||||||
|
|
||||||
managed := make(map[string][]string)
|
managed := make(map[string][]string)
|
||||||
|
static := make(map[string]struct{})
|
||||||
for _, room := range cfg.Orchestration.Rooms {
|
for _, room := range cfg.Orchestration.Rooms {
|
||||||
|
if room.RoomID == "" {
|
||||||
|
continue // skip empty room IDs (unset env vars)
|
||||||
|
}
|
||||||
managed[room.RoomID] = room.Participants
|
managed[room.RoomID] = room.Participants
|
||||||
|
static[room.RoomID] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
o := &Orchestrator{
|
o := &Orchestrator{
|
||||||
@@ -58,7 +81,9 @@ func New(cfg *config.SpecialConfig, agentBus *bus.Bus, logger *slog.Logger) (*Or
|
|||||||
llm: llmFunc,
|
llm: llmFunc,
|
||||||
bus: agentBus,
|
bus: agentBus,
|
||||||
managedRooms: managed,
|
managedRooms: managed,
|
||||||
|
staticRooms: static,
|
||||||
participants: make(map[string]orchestration.ParticipantInfo),
|
participants: make(map[string]orchestration.ParticipantInfo),
|
||||||
|
knownBotIDs: make(map[string]string),
|
||||||
logger: logger,
|
logger: logger,
|
||||||
seen: make(map[string]bool),
|
seen: make(map[string]bool),
|
||||||
}
|
}
|
||||||
@@ -70,15 +95,115 @@ func New(cfg *config.SpecialConfig, agentBus *bus.Bus, logger *slog.Logger) (*Or
|
|||||||
return o, nil
|
return o, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetScanner injects a Matrix client for room/member discovery.
|
||||||
|
// Must be called before ScanExistingRooms.
|
||||||
|
func (o *Orchestrator) SetScanner(s RoomScanner) {
|
||||||
|
o.scanner = s
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterParticipant adds bot metadata used for LLM routing decisions.
|
// RegisterParticipant adds bot metadata used for LLM routing decisions.
|
||||||
func (o *Orchestrator) RegisterParticipant(info orchestration.ParticipantInfo) {
|
func (o *Orchestrator) RegisterParticipant(info orchestration.ParticipantInfo) {
|
||||||
|
o.mu.Lock()
|
||||||
o.participants[info.ID] = info
|
o.participants[info.ID] = info
|
||||||
o.logger.Debug("registered participant", "bot", info.ID, "desc", info.Description)
|
if info.MatrixUserID != "" {
|
||||||
|
o.knownBotIDs[info.MatrixUserID] = info.ID
|
||||||
|
}
|
||||||
|
o.mu.Unlock()
|
||||||
|
o.logger.Debug("registered participant", "bot", info.ID, "matrix_uid", info.MatrixUserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanExistingRooms discovers rooms where ≥2 registered bots are members.
|
||||||
|
// Called once at startup after all participants are registered.
|
||||||
|
func (o *Orchestrator) ScanExistingRooms(ctx context.Context) {
|
||||||
|
if o.scanner == nil {
|
||||||
|
o.logger.Warn("no scanner set, skipping room discovery")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := o.scanner.JoinedRooms(ctx)
|
||||||
|
if err != nil {
|
||||||
|
o.logger.Error("failed to list joined rooms for discovery", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, roomID := range resp.JoinedRooms {
|
||||||
|
o.evaluateRoom(ctx, roomID.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
o.mu.RLock()
|
||||||
|
count := len(o.managedRooms)
|
||||||
|
o.mu.RUnlock()
|
||||||
|
o.logger.Info("room discovery complete", "managed_rooms", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// evaluateRoom checks if a room has ≥2 registered bots and updates managedRooms.
|
||||||
|
func (o *Orchestrator) evaluateRoom(ctx context.Context, roomID string) {
|
||||||
|
if o.scanner == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
members, err := o.scanner.JoinedMembers(ctx, id.RoomID(roomID))
|
||||||
|
if err != nil {
|
||||||
|
o.logger.Warn("evaluateRoom: failed to fetch members", "room", roomID, "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect which registered bots are in this room
|
||||||
|
o.mu.RLock()
|
||||||
|
var presentBots []string
|
||||||
|
for matrixUID, botID := range o.knownBotIDs {
|
||||||
|
if _, ok := members.Joined[id.UserID(matrixUID)]; ok {
|
||||||
|
presentBots = append(presentBots, botID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, isStatic := o.staticRooms[roomID]
|
||||||
|
o.mu.RUnlock()
|
||||||
|
|
||||||
|
// Static rooms (from YAML) are never auto-managed
|
||||||
|
if isStatic {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
o.mu.Lock()
|
||||||
|
defer o.mu.Unlock()
|
||||||
|
|
||||||
|
if len(presentBots) >= 2 {
|
||||||
|
prev, already := o.managedRooms[roomID]
|
||||||
|
if !already {
|
||||||
|
o.managedRooms[roomID] = presentBots
|
||||||
|
o.logger.Info("auto-managing room", "room", roomID, "bots", presentBots)
|
||||||
|
} else if len(prev) != len(presentBots) {
|
||||||
|
o.managedRooms[roomID] = presentBots
|
||||||
|
o.logger.Info("updated room participants", "room", roomID, "bots", presentBots)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, was := o.managedRooms[roomID]; was {
|
||||||
|
delete(o.managedRooms, roomID)
|
||||||
|
o.logger.Info("stopped managing room", "room", roomID, "remaining_bots", len(presentBots))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyMembership is called by bot listeners when a room membership changes.
|
||||||
|
// It re-evaluates whether the room should be auto-managed.
|
||||||
|
func (o *Orchestrator) NotifyMembership(ctx context.Context, roomID, userID, membership string) {
|
||||||
|
o.mu.RLock()
|
||||||
|
_, isBot := o.knownBotIDs[userID]
|
||||||
|
o.mu.RUnlock()
|
||||||
|
if !isBot {
|
||||||
|
return // only care about bot membership changes
|
||||||
|
}
|
||||||
|
|
||||||
|
o.logger.Debug("bot membership change, re-evaluating room",
|
||||||
|
"room", roomID, "user", userID, "membership", membership)
|
||||||
|
go o.evaluateRoom(ctx, roomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldIntercept returns true if the room is managed by this orchestrator.
|
// ShouldIntercept returns true if the room is managed by this orchestrator.
|
||||||
func (o *Orchestrator) ShouldIntercept(roomID string) bool {
|
func (o *Orchestrator) ShouldIntercept(roomID string) bool {
|
||||||
|
o.mu.RLock()
|
||||||
_, ok := o.managedRooms[roomID]
|
_, ok := o.managedRooms[roomID]
|
||||||
|
o.mu.RUnlock()
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +216,14 @@ func (o *Orchestrator) Intercept(ctx context.Context, msgCtx decision.MessageCon
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore messages from known bots to prevent feedback loops.
|
||||||
|
o.mu.RLock()
|
||||||
|
_, senderIsBot := o.knownBotIDs[msgCtx.SenderID]
|
||||||
|
o.mu.RUnlock()
|
||||||
|
if senderIsBot {
|
||||||
|
return true // suppress but don't route — bot's own message
|
||||||
|
}
|
||||||
|
|
||||||
// Dedup: multiple bots receive the same event. Only route once.
|
// Dedup: multiple bots receive the same event. Only route once.
|
||||||
key := msgCtx.RoomID + ":" + msgCtx.SenderID + ":" + msgCtx.Content
|
key := msgCtx.RoomID + ":" + msgCtx.SenderID + ":" + msgCtx.Content
|
||||||
o.seenMu.Lock()
|
o.seenMu.Lock()
|
||||||
@@ -119,7 +252,11 @@ func (o *Orchestrator) Intercept(ctx context.Context, msgCtx decision.MessageCon
|
|||||||
// Route is the main entry point. Called when a human posts in a managed room.
|
// Route is the main entry point. Called when a human posts in a managed room.
|
||||||
// It decides which bot(s) should respond and dispatches tasks via the bus.
|
// It decides which bot(s) should respond and dispatches tasks via the bus.
|
||||||
func (o *Orchestrator) Route(ctx context.Context, msgCtx decision.MessageContext) error {
|
func (o *Orchestrator) Route(ctx context.Context, msgCtx decision.MessageContext) error {
|
||||||
|
o.mu.RLock()
|
||||||
participants, ok := o.managedRooms[msgCtx.RoomID]
|
participants, ok := o.managedRooms[msgCtx.RoomID]
|
||||||
|
participantsCopy := append([]string(nil), participants...)
|
||||||
|
o.mu.RUnlock()
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("room %s is not managed", msgCtx.RoomID)
|
return fmt.Errorf("room %s is not managed", msgCtx.RoomID)
|
||||||
}
|
}
|
||||||
@@ -127,14 +264,14 @@ func (o *Orchestrator) Route(ctx context.Context, msgCtx decision.MessageContext
|
|||||||
o.logger.Info("orchestrating message",
|
o.logger.Info("orchestrating message",
|
||||||
"room", msgCtx.RoomID,
|
"room", msgCtx.RoomID,
|
||||||
"sender", msgCtx.SenderID,
|
"sender", msgCtx.SenderID,
|
||||||
"participants", participants,
|
"participants", participantsCopy,
|
||||||
"content_preview", truncate(msgCtx.Content, 80),
|
"content_preview", truncate(msgCtx.Content, 80),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Optimization: single bot → dispatch directly without LLM
|
// Optimization: single bot → dispatch directly without LLM
|
||||||
if len(participants) == 1 {
|
if len(participantsCopy) == 1 {
|
||||||
o.logger.Debug("single participant, dispatching directly", "bot", participants[0])
|
o.logger.Debug("single participant, dispatching directly", "bot", participantsCopy[0])
|
||||||
_, err := o.dispatchAndWait(ctx, participants[0], msgCtx, 0, nil)
|
_, err := o.dispatchAndWait(ctx, participantsCopy[0], msgCtx, 0, nil)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,10 +288,10 @@ func (o *Orchestrator) Route(ctx context.Context, msgCtx decision.MessageContext
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
rd, routeErr := o.routeInitial(ctx, msgCtx.Content, participants)
|
rd, routeErr := o.routeInitial(ctx, msgCtx.Content, participantsCopy)
|
||||||
if routeErr != nil {
|
if routeErr != nil {
|
||||||
o.logger.Error("routing failed, falling back to first participant", "err", routeErr)
|
o.logger.Error("routing failed, falling back to first participant", "err", routeErr)
|
||||||
target = participants[0]
|
target = participantsCopy[0]
|
||||||
} else {
|
} else {
|
||||||
target = rd.TargetBotID
|
target = rd.TargetBotID
|
||||||
o.logger.Info("routed to bot",
|
o.logger.Info("routed to bot",
|
||||||
@@ -165,7 +302,7 @@ func (o *Orchestrator) Route(ctx context.Context, msgCtx decision.MessageContext
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
rd, routeErr := o.routeRefinement(ctx, msgCtx.Content, responses, participants, lastBot)
|
rd, routeErr := o.routeRefinement(ctx, msgCtx.Content, responses, participantsCopy, lastBot)
|
||||||
if routeErr != nil {
|
if routeErr != nil {
|
||||||
o.logger.Warn("refinement routing failed, stopping pipeline", "err", routeErr)
|
o.logger.Warn("refinement routing failed, stopping pipeline", "err", routeErr)
|
||||||
break
|
break
|
||||||
@@ -304,6 +441,8 @@ func (o *Orchestrator) loadPrompts() error {
|
|||||||
|
|
||||||
// buildParticipantsList formats participant info for LLM prompts.
|
// buildParticipantsList formats participant info for LLM prompts.
|
||||||
func (o *Orchestrator) buildParticipantsList(botIDs []string, exclude string) string {
|
func (o *Orchestrator) buildParticipantsList(botIDs []string, exclude string) string {
|
||||||
|
o.mu.RLock()
|
||||||
|
defer o.mu.RUnlock()
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for _, id := range botIDs {
|
for _, id := range botIDs {
|
||||||
if id == exclude {
|
if id == exclude {
|
||||||
|
|||||||
Reference in New Issue
Block a user