Files
agents_and_robots/shell/bus/bus.go
T
egutierrez 5697b92ab8 feat: integrar structured logging en todos los componentes del shell
Se propaga *slog.Logger a todos los componentes impuros del shell:
- shell/bus/ — logs de subscribe, send, reply, timeout, unsubscribe
- shell/effects/ — duración y resultado de cada action ejecutada
- shell/llm/ (anthropic, openai, factory) — request/response con tokens, duración, fallback
- shell/memory/sqlite — open, save, recall, close con detalles
- shell/ssh/ — inicio, fin, errores y duración de comandos SSH
- tools/registry — registro, ejecución y errores de herramientas

Se usa el paquete shell/logger para field names consistentes (FieldDurationMS, FieldTokensUsed, etc.).
Cada componente recibe el logger por inyección de dependencias, sin globals.
Las firmas de New/FromConfig se actualizan para aceptar *slog.Logger.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:53:31 +00:00

144 lines
3.7 KiB
Go

// Package bus provides in-process agent-to-agent message passing.
package bus
import (
"context"
"fmt"
"log/slog"
"sync"
"time"
)
// Well-known message kinds used by the orchestrator.
const (
KindTask = "task" // orchestrator → bot: handle this question
KindTaskResult = "task_result" // bot → orchestrator: here is my answer
)
// AgentID identifies an agent.
type AgentID string
// AgentMessage is a message between agents.
type AgentMessage struct {
From AgentID
To AgentID
Kind string
Payload map[string]string
}
// Bus manages channels for inter-agent communication.
type Bus struct {
mu sync.RWMutex
channels map[AgentID]chan AgentMessage
replyMu sync.Mutex
replyChs map[string]chan AgentMessage // taskID → one-shot reply channel
logger *slog.Logger
}
// New creates a new Bus.
func New(logger *slog.Logger) *Bus {
return &Bus{
channels: make(map[AgentID]chan AgentMessage),
replyChs: make(map[string]chan AgentMessage),
logger: logger.With("component", "bus"),
}
}
// Subscribe registers an agent and returns its receive channel.
func (b *Bus) Subscribe(id AgentID) <-chan AgentMessage {
b.mu.Lock()
defer b.mu.Unlock()
ch := make(chan AgentMessage, 64)
b.channels[id] = ch
b.logger.Info("bus_subscribe", "agent", id)
return ch
}
// Send delivers a message to an agent's channel.
func (b *Bus) Send(msg AgentMessage) error {
b.mu.RLock()
ch, ok := b.channels[msg.To]
b.mu.RUnlock()
if !ok {
b.logger.Warn("bus_not_found", "to", msg.To, "from", msg.From, "kind", msg.Kind)
return fmt.Errorf("agent %q not registered on bus", msg.To)
}
select {
case ch <- msg:
b.logger.Debug("bus_send", "from", msg.From, "to", msg.To, "kind", msg.Kind)
return nil
default:
b.logger.Warn("bus_queue_full", "to", msg.To, "from", msg.From, "kind", msg.Kind)
return fmt.Errorf("agent %q message queue full", msg.To)
}
}
// SendAndWait sends a task message and blocks until a reply with the matching
// taskID arrives or the context expires. The caller must ensure the reply is
// routed via Reply().
func (b *Bus) SendAndWait(ctx context.Context, msg AgentMessage, taskID string, timeout time.Duration) (AgentMessage, error) {
ch := make(chan AgentMessage, 1)
b.replyMu.Lock()
b.replyChs[taskID] = ch
b.replyMu.Unlock()
defer func() {
b.replyMu.Lock()
delete(b.replyChs, taskID)
b.replyMu.Unlock()
}()
if err := b.Send(msg); err != nil {
return AgentMessage{}, err
}
b.logger.Debug("bus_send_and_wait", "task", taskID, "to", msg.To, "timeout", timeout)
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case reply := <-ch:
return reply, nil
case <-timer.C:
b.logger.Warn("bus_timeout", "task", taskID, "to", msg.To, "timeout", timeout)
return AgentMessage{}, fmt.Errorf("task %s: delegation timeout after %s", taskID, timeout)
case <-ctx.Done():
return AgentMessage{}, ctx.Err()
}
}
// Reply routes a task_result message to the waiting SendAndWait caller.
// If no one is waiting for this taskID, it falls back to regular Send.
func (b *Bus) Reply(taskID string, msg AgentMessage) error {
b.replyMu.Lock()
ch, ok := b.replyChs[taskID]
b.replyMu.Unlock()
if ok {
select {
case ch <- msg:
b.logger.Debug("bus_reply", "task", taskID, "from", msg.From)
return nil
default:
b.logger.Warn("bus_reply_full", "task", taskID)
return fmt.Errorf("reply channel full for task %s", taskID)
}
}
// Fallback: deliver via regular channel
return b.Send(msg)
}
// Unsubscribe removes an agent from the bus.
func (b *Bus) Unsubscribe(id AgentID) {
b.mu.Lock()
defer b.mu.Unlock()
if ch, ok := b.channels[id]; ok {
close(ch)
delete(b.channels, id)
b.logger.Info("bus_unsubscribe", "agent", id)
}
}