refactor: fix 62 golangci-lint issues across multiagent and routing packages

Fixes identified by running golangci-lint v2.10.1 (PR #304 config) with
govet, staticcheck, errcheck, revive, gosec enabled:

- Replace interface{} with any (revive: use-any)
- Replace WriteString(fmt.Sprintf(...)) with fmt.Fprintf (staticcheck: QF1012)
- Add doc comments on all exported methods (revive: exported)
- Safe type assertions with ok-check pattern (errcheck/revive)
- Use strings.EqualFold instead of double ToLower (staticcheck: SA6005)
- Add default cases to switch statements (revive)
- Suppress gosec G117 false positive on SessionKey field
- Test improvements: range int, unused params

Zero issues remaining in pkg/multiagent, pkg/agent, pkg/routing.
All 47 tests pass.

Relates to #304, #294
This commit is contained in:
Leandro Barbosa 2026-02-18 12:15:49 -03:00
parent 38ff3b8aa5
commit ef4ee76480
10 changed files with 93 additions and 58 deletions

View file

@ -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.

View file

@ -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",
})

View file

@ -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())

View file

@ -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,
},

View file

@ -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 {

View file

@ -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)

View file

@ -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")
}

View file

@ -5,6 +5,7 @@ import (
"strings"
)
// Agent ID defaults and constraints.
const (
DefaultAgentID = "main"
DefaultMainKey = "main"

View file

@ -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 {

View file

@ -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)
}