feat: implement multi-bot orchestration system with LLM routing
Implementa el sistema de orquestación para salas Matrix con múltiples bots. El orquestador es un "special agent" sin identidad Matrix que coordina qué bot responde y cuándo, usando LLM (Claude) para routing y evaluación de calidad. Cambios principales: - pkg/orchestration/task.go: tipos puros (TaskEvent, BotResponse, QualityScore, RoutingDecision) - shell/orchestration/: runtime del orquestador (orchestrator.go, router.go, evaluator.go) - agents/specials/orchestrator/: config + prompts (routing, quality, refinement) - internal/config/: SpecialConfig, OrchestrationCfg, LoadSpecial() - shell/bus/bus.go: protocolo request-reply (SendAndWait, Reply) para delegación - shell/matrix/listener.go: InterceptFunc para interceptar eventos en salas orquestadas - agents/runtime.go: SetBus, listenBus, handleTaskEvent para recibir tareas del orquestador - cmd/launcher/main.go: creación de bus compartido, arranque del orquestador antes de bots Incluye deduplicación para evitar que múltiples listeners en la misma sala disparen el orquestador más de una vez por mensaje. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,9 @@ import (
|
||||
"github.com/enmanuel/agents/pkg/decision"
|
||||
coretypes "github.com/enmanuel/agents/pkg/llm"
|
||||
"github.com/enmanuel/agents/pkg/memory"
|
||||
"github.com/enmanuel/agents/pkg/orchestration"
|
||||
"github.com/enmanuel/agents/pkg/personality"
|
||||
"github.com/enmanuel/agents/shell/bus"
|
||||
"github.com/enmanuel/agents/shell/effects"
|
||||
shelllm "github.com/enmanuel/agents/shell/llm"
|
||||
"github.com/enmanuel/agents/shell/matrix"
|
||||
@@ -49,6 +51,9 @@ type Agent struct {
|
||||
memStore memory.Store // nil when memory is disabled
|
||||
windowSize int
|
||||
roomCtx *tools.RoomContext
|
||||
|
||||
// Bus — set via SetBus() when running under the unified launcher
|
||||
agentBus *bus.Bus
|
||||
}
|
||||
|
||||
// ClearWindow resets the conversation window for a room and deletes persisted
|
||||
@@ -181,6 +186,17 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, logger *slog.Logger) (*
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// SetBus attaches the agent to the inter-agent bus for orchestration.
|
||||
// Must be called before Run().
|
||||
func (a *Agent) SetBus(b *bus.Bus) {
|
||||
a.agentBus = b
|
||||
}
|
||||
|
||||
// SetInterceptor configures the listener to skip events in orchestrated rooms.
|
||||
func (a *Agent) SetInterceptor(fn matrix.InterceptFunc) {
|
||||
a.listener.SetInterceptor(fn)
|
||||
}
|
||||
|
||||
// Run starts the agent sync loop. Blocks until ctx is cancelled.
|
||||
func (a *Agent) Run(ctx context.Context) error {
|
||||
if a.cryptoStore != nil {
|
||||
@@ -194,9 +210,136 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
"name", a.cfg.Agent.Name,
|
||||
"tools", a.toolReg.Names(),
|
||||
)
|
||||
|
||||
// Start bus listener if connected to the orchestration bus
|
||||
if a.agentBus != nil {
|
||||
ch := a.agentBus.Subscribe(bus.AgentID(a.cfg.Agent.ID))
|
||||
go a.listenBus(ctx, ch)
|
||||
a.logger.Info("bus listener started")
|
||||
}
|
||||
|
||||
return a.listener.Run(ctx)
|
||||
}
|
||||
|
||||
// listenBus processes messages from the inter-agent bus.
|
||||
func (a *Agent) listenBus(ctx context.Context, ch <-chan bus.AgentMessage) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case msg, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if msg.Kind == bus.KindTask {
|
||||
a.handleTaskEvent(ctx, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleTaskEvent processes a task delegated by the orchestrator.
|
||||
// The bot generates a response and sends it both to Matrix and back via bus.
|
||||
func (a *Agent) handleTaskEvent(ctx context.Context, msg bus.AgentMessage) {
|
||||
taskJSON, ok := msg.Payload["task_json"]
|
||||
if !ok {
|
||||
a.logger.Error("task message missing task_json payload")
|
||||
return
|
||||
}
|
||||
|
||||
task, err := orchestration.UnmarshalTaskEvent(taskJSON)
|
||||
if err != nil {
|
||||
a.logger.Error("failed to unmarshal task event", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
a.logger.Info("handling orchestrated task",
|
||||
"task_id", task.TaskID,
|
||||
"room", task.TargetRoomID,
|
||||
"sender", task.OriginalSender,
|
||||
"iteration", task.Iteration,
|
||||
)
|
||||
|
||||
roomID := task.TargetRoomID
|
||||
|
||||
// Update room context for memory tools
|
||||
a.roomCtx.Set(roomID)
|
||||
|
||||
if a.cfg.Personality.Behavior.TypingIndicator {
|
||||
_ = a.matrix.SendTyping(ctx, roomID, true)
|
||||
defer a.matrix.SendTyping(ctx, roomID, false)
|
||||
}
|
||||
|
||||
// Build a synthetic MessageContext from the task
|
||||
msgCtx := decision.MessageContext{
|
||||
SenderID: task.OriginalSender,
|
||||
RoomID: roomID,
|
||||
Content: task.OriginalQuestion,
|
||||
IsDirectMsg: false,
|
||||
IsMention: true, // treat orchestrated tasks like mentions
|
||||
}
|
||||
|
||||
// If there are previous responses, prepend context
|
||||
if len(task.PreviousResponses) > 0 {
|
||||
var context string
|
||||
for _, pr := range task.PreviousResponses {
|
||||
context += fmt.Sprintf("[Previous response from %s]: %s\n\n", pr.BotID, pr.Text)
|
||||
}
|
||||
msgCtx.Content = context + "Original question: " + task.OriginalQuestion +
|
||||
"\n\nPlease provide an improved or complementary answer."
|
||||
}
|
||||
|
||||
// Load memory and run LLM
|
||||
a.ensureWindowLoaded(ctx, roomID)
|
||||
a.appendToWindow(roomID, coretypes.Message{
|
||||
Role: coretypes.RoleUser, Content: msgCtx.Content,
|
||||
})
|
||||
|
||||
reply, err := a.runLLM(ctx, msgCtx)
|
||||
|
||||
// Build the result to send back via bus
|
||||
result := orchestration.TaskResult{
|
||||
TaskID: task.TaskID,
|
||||
BotID: a.cfg.Agent.ID,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
a.logger.Error("LLM error during orchestrated task", "err", err)
|
||||
result.Error = err.Error()
|
||||
reply = "Sorry, I encountered an error."
|
||||
} else {
|
||||
result.Text = reply
|
||||
// Persist assistant reply
|
||||
a.appendToWindow(roomID, coretypes.Message{
|
||||
Role: coretypes.RoleAssistant, Content: reply,
|
||||
})
|
||||
a.persistMessage(ctx, roomID, coretypes.RoleAssistant, reply)
|
||||
}
|
||||
|
||||
// Send reply to Matrix room
|
||||
if sendErr := a.matrix.SendText(ctx, roomID, reply); sendErr != nil {
|
||||
a.logger.Error("failed to send orchestrated reply to Matrix", "err", sendErr)
|
||||
}
|
||||
|
||||
// Send result back to orchestrator via bus
|
||||
resultJSON, marshalErr := orchestration.MarshalTaskResult(result)
|
||||
if marshalErr != nil {
|
||||
a.logger.Error("failed to marshal task result", "err", marshalErr)
|
||||
return
|
||||
}
|
||||
|
||||
replyMsg := bus.AgentMessage{
|
||||
From: bus.AgentID(a.cfg.Agent.ID),
|
||||
To: msg.From,
|
||||
Kind: bus.KindTaskResult,
|
||||
Payload: map[string]string{"result_json": resultJSON},
|
||||
}
|
||||
|
||||
if busErr := a.agentBus.Reply(task.TaskID, replyMsg); busErr != nil {
|
||||
a.logger.Error("failed to send task result via bus", "err", busErr)
|
||||
}
|
||||
}
|
||||
|
||||
// handleEvent is called by the matrix Listener for each filtered incoming event.
|
||||
func (a *Agent) handleEvent(ctx context.Context, msgCtx decision.MessageContext, evt *event.Event) {
|
||||
a.logger.Debug("handling event",
|
||||
|
||||
Reference in New Issue
Block a user