diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 189d47aa7..bb923136d 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -768,11 +768,16 @@ func (al *AgentLoop) updateToolContexts(agent *AgentInstance, channel, chatID st // getOrCreateBlackboard returns the blackboard for a session, creating one if needed. func (al *AgentLoop) getOrCreateBlackboard(sessionKey string) *multiagent.Blackboard { if v, ok := al.blackboards.Load(sessionKey); ok { - return v.(*multiagent.Blackboard) + if bb, ok := v.(*multiagent.Blackboard); ok { + return bb + } } bb := multiagent.NewBlackboard() actual, _ := al.blackboards.LoadOrStore(sessionKey, bb) - return actual.(*multiagent.Blackboard) + if result, ok := actual.(*multiagent.Blackboard); ok { + return result + } + return bb } // maybeSummarize triggers summarization if the session history exceeds thresholds. diff --git a/pkg/multiagent/blackboard_test.go b/pkg/multiagent/blackboard_test.go index 45c7374d6..050361ba9 100644 --- a/pkg/multiagent/blackboard_test.go +++ b/pkg/multiagent/blackboard_test.go @@ -118,20 +118,18 @@ func TestBlackboard_Size(t *testing.T) { } } -func TestBlackboard_ConcurrentAccess(t *testing.T) { +func TestBlackboard_ConcurrentAccess(_ *testing.T) { bb := NewBlackboard() var wg sync.WaitGroup - for i := 0; i < 100; i++ { - wg.Add(1) - go func(n int) { - defer wg.Done() + for range 100 { + wg.Go(func() { key := "key" bb.Set(key, "val", "agent") bb.Get(key) bb.List() bb.Snapshot() - }(i) + }) } wg.Wait() } @@ -160,7 +158,7 @@ func TestBlackboardTool_Write(t *testing.T) { bb := NewBlackboard() tool := NewBlackboardTool(bb, "test-agent") - result := tool.Execute(context.Background(), map[string]interface{}{ + result := tool.Execute(context.Background(), map[string]any{ "action": "write", "key": "task", "value": "implement feature", @@ -183,7 +181,7 @@ func TestBlackboardTool_Read(t *testing.T) { bb.Set("info", "hello", "other") tool := NewBlackboardTool(bb, "reader") - result := tool.Execute(context.Background(), map[string]interface{}{ + result := tool.Execute(context.Background(), map[string]any{ "action": "read", "key": "info", }) @@ -199,7 +197,7 @@ func TestBlackboardTool_ReadMissing(t *testing.T) { bb := NewBlackboard() tool := NewBlackboardTool(bb, "reader") - result := tool.Execute(context.Background(), map[string]interface{}{ + result := tool.Execute(context.Background(), map[string]any{ "action": "read", "key": "nope", }) @@ -217,7 +215,7 @@ func TestBlackboardTool_List(t *testing.T) { bb.Set("b", "2", "y") tool := NewBlackboardTool(bb, "lister") - result := tool.Execute(context.Background(), map[string]interface{}{ + result := tool.Execute(context.Background(), map[string]any{ "action": "list", }) if result.IsError { @@ -233,7 +231,7 @@ func TestBlackboardTool_Delete(t *testing.T) { bb.Set("tmp", "val", "x") tool := NewBlackboardTool(bb, "deleter") - result := tool.Execute(context.Background(), map[string]interface{}{ + result := tool.Execute(context.Background(), map[string]any{ "action": "delete", "key": "tmp", }) @@ -249,7 +247,7 @@ func TestBlackboardTool_InvalidAction(t *testing.T) { bb := NewBlackboard() tool := NewBlackboardTool(bb, "test") - result := tool.Execute(context.Background(), map[string]interface{}{ + result := tool.Execute(context.Background(), map[string]any{ "action": "invalid", }) if !result.IsError { @@ -262,7 +260,7 @@ func TestBlackboardTool_MissingKey(t *testing.T) { tool := NewBlackboardTool(bb, "test") // read without key - result := tool.Execute(context.Background(), map[string]interface{}{ + result := tool.Execute(context.Background(), map[string]any{ "action": "read", }) if !result.IsError { @@ -270,7 +268,7 @@ func TestBlackboardTool_MissingKey(t *testing.T) { } // write without key - result = tool.Execute(context.Background(), map[string]interface{}{ + result = tool.Execute(context.Background(), map[string]any{ "action": "write", "value": "test", }) @@ -279,7 +277,7 @@ func TestBlackboardTool_MissingKey(t *testing.T) { } // write without value - result = tool.Execute(context.Background(), map[string]interface{}{ + result = tool.Execute(context.Background(), map[string]any{ "action": "write", "key": "k", }) diff --git a/pkg/multiagent/blackboard_tool.go b/pkg/multiagent/blackboard_tool.go index 4478b6d05..3f4d9e46f 100644 --- a/pkg/multiagent/blackboard_tool.go +++ b/pkg/multiagent/blackboard_tool.go @@ -23,27 +23,30 @@ func NewBlackboardTool(board *Blackboard, agentID string) *BlackboardTool { } } +// Name returns the tool name. func (t *BlackboardTool) Name() string { return "blackboard" } +// Description returns a human-readable description of the tool. func (t *BlackboardTool) Description() string { return "Read, write, list, or delete entries in the shared context blackboard. " + "Use this to share information between agents in a multi-agent session." } -func (t *BlackboardTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +// Parameters returns the JSON Schema for the tool's input. +func (t *BlackboardTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "action": map[string]interface{}{ + "properties": map[string]any{ + "action": map[string]any{ "type": "string", "enum": []string{"read", "write", "list", "delete"}, "description": "The action to perform on the blackboard", }, - "key": map[string]interface{}{ + "key": map[string]any{ "type": "string", "description": "The key to read, write, or delete (not required for list)", }, - "value": map[string]interface{}{ + "value": map[string]any{ "type": "string", "description": "The value to write (only required for write action)", }, @@ -52,10 +55,20 @@ func (t *BlackboardTool) Parameters() map[string]interface{} { } } -func (t *BlackboardTool) Execute(_ context.Context, args map[string]interface{}) *tools.ToolResult { - action, _ := args["action"].(string) - key, _ := args["key"].(string) - value, _ := args["value"].(string) +// Execute runs the blackboard action specified in args. +func (t *BlackboardTool) Execute(_ context.Context, args map[string]any) *tools.ToolResult { + action, ok := args["action"].(string) + if !ok { + action = "" + } + key, ok := args["key"].(string) + if !ok { + key = "" + } + value, ok := args["value"].(string) + if !ok { + value = "" + } switch strings.ToLower(action) { case "read": @@ -85,11 +98,11 @@ func (t *BlackboardTool) Execute(_ context.Context, args map[string]interface{}) return tools.NewToolResult("Blackboard is empty") } var sb strings.Builder - sb.WriteString(fmt.Sprintf("Blackboard entries (%d):\n", len(keys))) + fmt.Fprintf(&sb, "Blackboard entries (%d):\n", len(keys)) for _, k := range keys { entry := t.board.GetEntry(k) if entry != nil { - sb.WriteString(fmt.Sprintf("- %s (by %s): %s\n", k, entry.Author, entry.Value)) + fmt.Fprintf(&sb, "- %s (by %s): %s\n", k, entry.Author, entry.Value) } } return tools.NewToolResult(sb.String()) diff --git a/pkg/multiagent/handoff.go b/pkg/multiagent/handoff.go index 406bfdf84..cab5a9489 100644 --- a/pkg/multiagent/handoff.go +++ b/pkg/multiagent/handoff.go @@ -81,7 +81,7 @@ func ExecuteHandoff(ctx context.Context, resolver AgentResolver, board *Blackboa Model: target.Model, Tools: target.Tools, MaxIterations: maxIter, - LLMOptions: map[string]interface{}{ + LLMOptions: map[string]any{ "max_tokens": 4096, "temperature": 0.7, }, diff --git a/pkg/multiagent/handoff_test.go b/pkg/multiagent/handoff_test.go index 0c08312b1..7b535ae29 100644 --- a/pkg/multiagent/handoff_test.go +++ b/pkg/multiagent/handoff_test.go @@ -15,7 +15,7 @@ type mockProvider struct { err error } -func (m *mockProvider) Chat(_ context.Context, _ []providers.Message, _ []providers.ToolDefinition, _ string, _ map[string]interface{}) (*providers.LLMResponse, error) { +func (m *mockProvider) Chat(_ context.Context, _ []providers.Message, _ []providers.ToolDefinition, _ string, _ map[string]any) (*providers.LLMResponse, error) { if m.err != nil { return nil, m.err } @@ -140,7 +140,7 @@ func TestHandoffTool_Execute(t *testing.T) { bb := NewBlackboard() tool := NewHandoffTool(resolver, bb, "main") - result := tool.Execute(context.Background(), map[string]interface{}{ + result := tool.Execute(context.Background(), map[string]any{ "agent_id": "coder", "task": "write code", }) @@ -159,7 +159,7 @@ func TestHandoffTool_MissingArgs(t *testing.T) { tool := NewHandoffTool(resolver, bb, "main") // Missing agent_id - result := tool.Execute(context.Background(), map[string]interface{}{ + result := tool.Execute(context.Background(), map[string]any{ "task": "do something", }) if !result.IsError { @@ -167,7 +167,7 @@ func TestHandoffTool_MissingArgs(t *testing.T) { } // Missing task - result = tool.Execute(context.Background(), map[string]interface{}{ + result = tool.Execute(context.Background(), map[string]any{ "agent_id": "coder", }) if !result.IsError { diff --git a/pkg/multiagent/handoff_tool.go b/pkg/multiagent/handoff_tool.go index 03c85ff15..9f31dff33 100644 --- a/pkg/multiagent/handoff_tool.go +++ b/pkg/multiagent/handoff_tool.go @@ -28,8 +28,10 @@ func NewHandoffTool(resolver AgentResolver, board *Blackboard, fromAgentID strin } } +// Name returns the tool name. func (t *HandoffTool) Name() string { return "handoff" } +// Description returns a dynamic description listing available target agents. func (t *HandoffTool) Description() string { agents := t.resolver.ListAgents() if len(agents) <= 1 { @@ -42,31 +44,32 @@ func (t *HandoffTool) Description() string { if a.ID == t.fromAgentID { continue } - sb.WriteString(fmt.Sprintf("- %s", a.ID)) + fmt.Fprintf(&sb, "- %s", a.ID) if a.Name != "" { - sb.WriteString(fmt.Sprintf(" (%s)", a.Name)) + fmt.Fprintf(&sb, " (%s)", a.Name) } if a.Role != "" { - sb.WriteString(fmt.Sprintf(": %s", a.Role)) + fmt.Fprintf(&sb, ": %s", a.Role) } sb.WriteString("\n") } return sb.String() } -func (t *HandoffTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +// Parameters returns the JSON Schema for the tool's input. +func (t *HandoffTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "agent_id": map[string]interface{}{ + "properties": map[string]any{ + "agent_id": map[string]any{ "type": "string", "description": "The ID of the target agent to hand off to", }, - "task": map[string]interface{}{ + "task": map[string]any{ "type": "string", "description": "The task description for the target agent", }, - "context": map[string]interface{}{ + "context": map[string]any{ "type": "object", "description": "Optional key-value context to share via blackboard before handoff", }, @@ -75,14 +78,22 @@ func (t *HandoffTool) Parameters() map[string]interface{} { } } +// SetContext updates the origin channel and chat ID for handoff routing. func (t *HandoffTool) SetContext(channel, chatID string) { t.originChannel = channel t.originChatID = chatID } -func (t *HandoffTool) Execute(ctx context.Context, args map[string]interface{}) *tools.ToolResult { - agentID, _ := args["agent_id"].(string) - task, _ := args["task"].(string) +// Execute delegates a task to the specified target agent. +func (t *HandoffTool) Execute(ctx context.Context, args map[string]any) *tools.ToolResult { + agentID, ok := args["agent_id"].(string) + if !ok { + agentID = "" + } + task, ok := args["task"].(string) + if !ok { + task = "" + } if agentID == "" { return tools.ErrorResult("agent_id is required") @@ -93,7 +104,7 @@ func (t *HandoffTool) Execute(ctx context.Context, args map[string]interface{}) // Parse optional context map var contextMap map[string]string - if ctxRaw, ok := args["context"].(map[string]interface{}); ok { + if ctxRaw, ok := args["context"].(map[string]any); ok { contextMap = make(map[string]string, len(ctxRaw)) for k, v := range ctxRaw { contextMap[k] = fmt.Sprintf("%v", v) diff --git a/pkg/multiagent/list_agents_tool.go b/pkg/multiagent/list_agents_tool.go index 23e2ad6f3..f46838848 100644 --- a/pkg/multiagent/list_agents_tool.go +++ b/pkg/multiagent/list_agents_tool.go @@ -18,34 +18,38 @@ func NewListAgentsTool(resolver AgentResolver) *ListAgentsTool { return &ListAgentsTool{resolver: resolver} } +// Name returns the tool name. func (t *ListAgentsTool) Name() string { return "list_agents" } +// Description returns a human-readable description of the tool. func (t *ListAgentsTool) Description() string { return "List all available agents with their IDs, names, and roles." } -func (t *ListAgentsTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +// Parameters returns the JSON Schema for the tool's input. +func (t *ListAgentsTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{}, + "properties": map[string]any{}, } } -func (t *ListAgentsTool) Execute(_ context.Context, _ map[string]interface{}) *tools.ToolResult { +// Execute lists all registered agents with their metadata. +func (t *ListAgentsTool) Execute(_ context.Context, _ map[string]any) *tools.ToolResult { agents := t.resolver.ListAgents() if len(agents) == 0 { return tools.NewToolResult("No agents registered.") } var sb strings.Builder - sb.WriteString(fmt.Sprintf("Available agents (%d):\n", len(agents))) + fmt.Fprintf(&sb, "Available agents (%d):\n", len(agents)) for _, a := range agents { - sb.WriteString(fmt.Sprintf("- ID: %s", a.ID)) + fmt.Fprintf(&sb, "- ID: %s", a.ID) if a.Name != "" { - sb.WriteString(fmt.Sprintf(", Name: %s", a.Name)) + fmt.Fprintf(&sb, ", Name: %s", a.Name) } if a.Role != "" { - sb.WriteString(fmt.Sprintf(", Role: %s", a.Role)) + fmt.Fprintf(&sb, ", Role: %s", a.Role) } sb.WriteString("\n") } diff --git a/pkg/routing/agent_id.go b/pkg/routing/agent_id.go index bcf2f0dc0..a6e6e881e 100644 --- a/pkg/routing/agent_id.go +++ b/pkg/routing/agent_id.go @@ -5,6 +5,7 @@ import ( "strings" ) +// Agent ID defaults and constraints. const ( DefaultAgentID = "main" DefaultMainKey = "main" diff --git a/pkg/routing/route.go b/pkg/routing/route.go index 9eb060c53..9b69bf366 100644 --- a/pkg/routing/route.go +++ b/pkg/routing/route.go @@ -21,8 +21,8 @@ type ResolvedRoute struct { AgentID string Channel string AccountID string - SessionKey string - MainSessionKey string + SessionKey string `json:"session_key"` //nolint:gosec // G117: not a secret, this is a session identifier + MainSessionKey string `json:"main_session_key"` //nolint:gosec // G117: not a secret, this is a session identifier MatchedBy string // "binding.peer", "binding.peer.parent", "binding.guild", "binding.team", "binding.account", "binding.channel", "default" } @@ -141,7 +141,7 @@ func matchesAccountID(matchAccountID, actual string) bool { if trimmed == "*" { return true } - return strings.ToLower(trimmed) == strings.ToLower(actual) + return strings.EqualFold(trimmed, actual) } func (r *RouteResolver) findPeerMatch(bindings []config.AgentBinding, peer *RoutePeer) *config.AgentBinding { diff --git a/pkg/routing/session_key.go b/pkg/routing/session_key.go index e12f0d1d8..cb2469a10 100644 --- a/pkg/routing/session_key.go +++ b/pkg/routing/session_key.go @@ -8,6 +8,7 @@ import ( // DMScope controls DM session isolation granularity. type DMScope string +// DM scope constants control session isolation granularity. const ( DMScopeMain DMScope = "main" DMScopePerPeer DMScope = "per-peer" @@ -86,6 +87,8 @@ func BuildAgentPeerSessionKey(params SessionKeyParams) string { if peerID != "" { return fmt.Sprintf("agent:%s:direct:%s", agentID, peerID) } + default: + // DMScopeMain or unrecognized: fall through to main session key } return BuildAgentMainSessionKey(agentID) }