fix: resolve blackboard split-brain in multi-agent tools
Tools were bound to a static board at registration time while the system prompt injected data from a separate per-session board. Add BoardAware interface and SetBoard() methods so tools receive the correct session blackboard before each execution cycle.
This commit is contained in:
parent
c5145f1ade
commit
4def7b497e
4 changed files with 44 additions and 10 deletions
|
|
@ -174,14 +174,14 @@ func registerSharedTools(cfg *config.Config, msgBus *bus.MessageBus, registry *A
|
||||||
if len(registry.ListAgentIDs()) > 1 {
|
if len(registry.ListAgentIDs()) > 1 {
|
||||||
resolver := ®istryResolver{registry: registry}
|
resolver := ®istryResolver{registry: registry}
|
||||||
|
|
||||||
// Blackboard tool: per-agent instance sharing a common blackboard
|
// Blackboard tool: per-agent instance sharing a placeholder blackboard.
|
||||||
// The actual blackboard is created per session in getOrCreateBlackboard
|
// The actual per-session blackboard is wired via SetBoard in updateToolContexts
|
||||||
// For tool registration, we use a shared "global" blackboard.
|
// before each message processing cycle (fixing the split-brain bug).
|
||||||
sharedBoard := multiagent.NewBlackboard()
|
placeholderBoard := multiagent.NewBlackboard()
|
||||||
agent.Tools.Register(multiagent.NewBlackboardTool(sharedBoard, agentID))
|
agent.Tools.Register(multiagent.NewBlackboardTool(placeholderBoard, agentID))
|
||||||
|
|
||||||
// Handoff tool: delegate tasks to other agents
|
// 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
|
// List agents tool: discover available agents
|
||||||
agent.Tools.Register(multiagent.NewListAgentsTool(resolver))
|
agent.Tools.Register(multiagent.NewListAgentsTool(resolver))
|
||||||
|
|
@ -438,8 +438,8 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Update tool contexts
|
// 1. Update tool contexts (including per-session blackboard wiring)
|
||||||
al.updateToolContexts(agent, opts.Channel, opts.ChatID)
|
al.updateToolContexts(agent, opts.Channel, opts.ChatID, opts.SessionKey)
|
||||||
|
|
||||||
// 2. Build messages (skip history for heartbeat)
|
// 2. Build messages (skip history for heartbeat)
|
||||||
var history []providers.Message
|
var history []providers.Message
|
||||||
|
|
@ -742,8 +742,9 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
return finalContent, iteration, nil
|
return finalContent, iteration, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateToolContexts updates the context for tools that need channel/chatID info.
|
// updateToolContexts updates the context for tools that need channel/chatID info
|
||||||
func (al *AgentLoop) updateToolContexts(agent *AgentInstance, channel, chatID string) {
|
// 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
|
// Use ContextualTool interface instead of type assertions
|
||||||
if tool, ok := agent.Tools.Get("message"); ok {
|
if tool, ok := agent.Tools.Get("message"); ok {
|
||||||
if mt, ok := tool.(tools.ContextualTool); 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)
|
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.
|
// getOrCreateBlackboard returns the blackboard for a session, creating one if needed.
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@ type BlackboardEntry struct {
|
||||||
Timestamp time.Time `json:"timestamp"`
|
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.
|
// Blackboard is a thread-safe shared context pool for multi-agent collaboration.
|
||||||
// Agents read and write string key-value entries, each tagged with authorship
|
// Agents read and write string key-value entries, each tagged with authorship
|
||||||
// and scope metadata.
|
// and scope metadata.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// Name returns the tool name.
|
||||||
func (t *BlackboardTool) Name() string { return "blackboard" }
|
func (t *BlackboardTool) Name() string { return "blackboard" }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// SetContext updates the origin channel and chat ID for handoff routing.
|
||||||
func (t *HandoffTool) SetContext(channel, chatID string) {
|
func (t *HandoffTool) SetContext(channel, chatID string) {
|
||||||
t.originChannel = channel
|
t.originChannel = channel
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue