diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 12ccb0745..cc31e06a8 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -174,14 +174,14 @@ func registerSharedTools(cfg *config.Config, msgBus *bus.MessageBus, registry *A if len(registry.ListAgentIDs()) > 1 { resolver := ®istryResolver{registry: registry} - // Blackboard tool: per-agent instance sharing a common blackboard - // The actual blackboard is created per session in getOrCreateBlackboard - // For tool registration, we use a shared "global" blackboard. - sharedBoard := multiagent.NewBlackboard() - agent.Tools.Register(multiagent.NewBlackboardTool(sharedBoard, agentID)) + // Blackboard tool: per-agent instance sharing a placeholder blackboard. + // The actual per-session blackboard is wired via SetBoard in updateToolContexts + // before each message processing cycle (fixing the split-brain bug). + placeholderBoard := multiagent.NewBlackboard() + agent.Tools.Register(multiagent.NewBlackboardTool(placeholderBoard, agentID)) // Handoff tool: delegate tasks to other agents - agent.Tools.Register(multiagent.NewHandoffTool(resolver, sharedBoard, agentID)) + agent.Tools.Register(multiagent.NewHandoffTool(resolver, placeholderBoard, agentID)) // List agents tool: discover available agents agent.Tools.Register(multiagent.NewListAgentsTool(resolver)) @@ -438,8 +438,8 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt } } - // 1. Update tool contexts - al.updateToolContexts(agent, opts.Channel, opts.ChatID) + // 1. Update tool contexts (including per-session blackboard wiring) + al.updateToolContexts(agent, opts.Channel, opts.ChatID, opts.SessionKey) // 2. Build messages (skip history for heartbeat) var history []providers.Message @@ -742,8 +742,9 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, return finalContent, iteration, nil } -// updateToolContexts updates the context for tools that need channel/chatID info. -func (al *AgentLoop) updateToolContexts(agent *AgentInstance, channel, chatID string) { +// updateToolContexts updates the context for tools that need channel/chatID info +// and wires the per-session blackboard to board-aware tools. +func (al *AgentLoop) updateToolContexts(agent *AgentInstance, channel, chatID string, sessionKey string) { // Use ContextualTool interface instead of type assertions if tool, ok := agent.Tools.Get("message"); ok { if mt, ok := tool.(tools.ContextualTool); ok { @@ -765,6 +766,20 @@ func (al *AgentLoop) updateToolContexts(agent *AgentInstance, channel, chatID st ht.SetContext(channel, chatID) } } + + // Wire the per-session blackboard to board-aware tools (fixes split-brain bug). + // This ensures BlackboardTool and HandoffTool operate on the same board that + // gets injected into the system prompt via getOrCreateBlackboard. + if sessionKey != "" { + bb := al.getOrCreateBlackboard(sessionKey) + for _, toolName := range []string{"blackboard", "handoff"} { + if tool, ok := agent.Tools.Get(toolName); ok { + if ba, ok := tool.(multiagent.BoardAware); ok { + ba.SetBoard(bb) + } + } + } + } } // getOrCreateBlackboard returns the blackboard for a session, creating one if needed. diff --git a/pkg/multiagent/blackboard.go b/pkg/multiagent/blackboard.go index 7f35eaa24..35300933b 100644 --- a/pkg/multiagent/blackboard.go +++ b/pkg/multiagent/blackboard.go @@ -16,6 +16,13 @@ type BlackboardEntry struct { Timestamp time.Time `json:"timestamp"` } +// BoardAware is implemented by tools that need the session blackboard injected +// before each execution. This fixes the split-brain bug where tools were bound +// to a static board at registration time instead of the per-session board. +type BoardAware interface { + SetBoard(board *Blackboard) +} + // Blackboard is a thread-safe shared context pool for multi-agent collaboration. // Agents read and write string key-value entries, each tagged with authorship // and scope metadata. diff --git a/pkg/multiagent/blackboard_tool.go b/pkg/multiagent/blackboard_tool.go index 3f4d9e46f..6c57c2764 100644 --- a/pkg/multiagent/blackboard_tool.go +++ b/pkg/multiagent/blackboard_tool.go @@ -23,6 +23,12 @@ func NewBlackboardTool(board *Blackboard, agentID string) *BlackboardTool { } } +// SetBoard replaces the blackboard reference, allowing the tool to be wired +// to the correct per-session board before each execution. +func (t *BlackboardTool) SetBoard(board *Blackboard) { + t.board = board +} + // Name returns the tool name. func (t *BlackboardTool) Name() string { return "blackboard" } diff --git a/pkg/multiagent/handoff_tool.go b/pkg/multiagent/handoff_tool.go index c97c8bbf4..75276f3d2 100644 --- a/pkg/multiagent/handoff_tool.go +++ b/pkg/multiagent/handoff_tool.go @@ -85,6 +85,12 @@ func (t *HandoffTool) Parameters() map[string]any { } } +// SetBoard replaces the blackboard reference, allowing the tool to be wired +// to the correct per-session board before each execution. +func (t *HandoffTool) SetBoard(board *Blackboard) { + t.board = board +} + // SetContext updates the origin channel and chat ID for handoff routing. func (t *HandoffTool) SetContext(channel, chatID string) { t.originChannel = channel