feat: add Capabilities field to AgentInfo for capability-based routing

Add Capabilities []string to the full agent chain (config → instance →
resolver → multiagent) as required by issue #294 spec:
"Define a standard Agent interface that includes Capabilities."

- AgentConfig: new capabilities JSON field
- AgentInstance: new Capabilities field, populated from config
- registryResolver: map Capabilities in GetAgentInfo and ListAgents
- AgentInfo: new Capabilities field
- FindAgentsByCapability: helper to query agents by capability tag
- 4 new tests covering match, multi-match, empty, and nil safety
This commit is contained in:
Edouard CLAUDE 2026-02-18 21:45:57 +04:00
parent ef4ee76480
commit e121c7ca04
5 changed files with 87 additions and 3 deletions

View file

@ -28,6 +28,7 @@ type AgentInstance struct {
Sessions *session.SessionManager Sessions *session.SessionManager
ContextBuilder *ContextBuilder ContextBuilder *ContextBuilder
Tools *tools.ToolRegistry Tools *tools.ToolRegistry
Capabilities []string
Subagents *config.SubagentsConfig Subagents *config.SubagentsConfig
SkillsFilter []string SkillsFilter []string
Candidates []providers.FallbackCandidate Candidates []providers.FallbackCandidate
@ -67,6 +68,7 @@ func NewAgentInstance(
agentSystemPrompt := "" agentSystemPrompt := ""
var subagents *config.SubagentsConfig var subagents *config.SubagentsConfig
var skillsFilter []string var skillsFilter []string
var capabilities []string
if agentCfg != nil { if agentCfg != nil {
agentID = routing.NormalizeAgentID(agentCfg.ID) agentID = routing.NormalizeAgentID(agentCfg.ID)
@ -75,6 +77,7 @@ func NewAgentInstance(
agentSystemPrompt = agentCfg.SystemPrompt agentSystemPrompt = agentCfg.SystemPrompt
subagents = agentCfg.Subagents subagents = agentCfg.Subagents
skillsFilter = agentCfg.Skills skillsFilter = agentCfg.Skills
capabilities = agentCfg.Capabilities
} }
maxIter := defaults.MaxToolIterations maxIter := defaults.MaxToolIterations
@ -103,6 +106,7 @@ func NewAgentInstance(
Sessions: sessionsManager, Sessions: sessionsManager,
ContextBuilder: contextBuilder, ContextBuilder: contextBuilder,
Tools: toolsRegistry, Tools: toolsRegistry,
Capabilities: capabilities,
Subagents: subagents, Subagents: subagents,
SkillsFilter: skillsFilter, SkillsFilter: skillsFilter,
Candidates: candidates, Candidates: candidates,

View file

@ -99,6 +99,7 @@ func (r *registryResolver) GetAgentInfo(agentID string) *multiagent.AgentInfo {
Provider: agent.Provider, Provider: agent.Provider,
Tools: agent.Tools, Tools: agent.Tools,
MaxIter: agent.MaxIterations, MaxIter: agent.MaxIterations,
Capabilities: agent.Capabilities,
} }
} }
@ -114,6 +115,7 @@ func (r *registryResolver) ListAgents() []multiagent.AgentInfo {
ID: agent.ID, ID: agent.ID,
Name: agent.Name, Name: agent.Name,
Role: agent.Role, Role: agent.Role,
Capabilities: agent.Capabilities,
}) })
} }
return agents return agents

View file

@ -109,6 +109,7 @@ type AgentConfig struct {
Workspace string `json:"workspace,omitempty"` Workspace string `json:"workspace,omitempty"`
Model *AgentModelConfig `json:"model,omitempty"` Model *AgentModelConfig `json:"model,omitempty"`
Skills []string `json:"skills,omitempty"` Skills []string `json:"skills,omitempty"`
Capabilities []string `json:"capabilities,omitempty"`
Subagents *SubagentsConfig `json:"subagents,omitempty"` Subagents *SubagentsConfig `json:"subagents,omitempty"`
} }

View file

@ -3,6 +3,7 @@ package multiagent
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"github.com/sipeed/picoclaw/pkg/providers" "github.com/sipeed/picoclaw/pkg/providers"
"github.com/sipeed/picoclaw/pkg/tools" "github.com/sipeed/picoclaw/pkg/tools"
@ -26,6 +27,18 @@ type AgentInfo struct {
Provider providers.LLMProvider Provider providers.LLMProvider
Tools *tools.ToolRegistry Tools *tools.ToolRegistry
MaxIter int MaxIter int
Capabilities []string // optional tags for capability-based routing (e.g. "coding", "research")
}
// FindAgentsByCapability returns agents that advertise the given capability.
func FindAgentsByCapability(resolver AgentResolver, capability string) []AgentInfo {
var matches []AgentInfo
for _, a := range resolver.ListAgents() {
if slices.Contains(a.Capabilities, capability) {
matches = append(matches, a)
}
}
return matches
} }
// HandoffRequest describes a delegation from one agent to another. // HandoffRequest describes a delegation from one agent to another.

View file

@ -224,6 +224,70 @@ func TestListAgentsTool_Empty(t *testing.T) {
} }
} }
func TestFindAgentsByCapability(t *testing.T) {
resolver := newMockResolver(
&AgentInfo{ID: "coder", Name: "Coder", Capabilities: []string{"coding", "review"}},
&AgentInfo{ID: "researcher", Name: "Researcher", Capabilities: []string{"research", "web_search"}},
&AgentInfo{ID: "generalist", Name: "Generalist"},
)
// Find coding agents
matches := FindAgentsByCapability(resolver, "coding")
if len(matches) != 1 || matches[0].ID != "coder" {
t.Errorf("FindAgentsByCapability(coding) = %v, want [coder]", matches)
}
// Find research agents
matches = FindAgentsByCapability(resolver, "research")
if len(matches) != 1 || matches[0].ID != "researcher" {
t.Errorf("FindAgentsByCapability(research) = %v, want [researcher]", matches)
}
// No match
matches = FindAgentsByCapability(resolver, "design")
if len(matches) != 0 {
t.Errorf("FindAgentsByCapability(design) = %v, want empty", matches)
}
}
func TestFindAgentsByCapability_Multiple(t *testing.T) {
resolver := newMockResolver(
&AgentInfo{ID: "a", Capabilities: []string{"coding"}},
&AgentInfo{ID: "b", Capabilities: []string{"coding", "review"}},
&AgentInfo{ID: "c", Capabilities: []string{"research"}},
)
matches := FindAgentsByCapability(resolver, "coding")
if len(matches) != 2 {
t.Errorf("expected 2 matches, got %d", len(matches))
}
}
func TestFindAgentsByCapability_Empty(t *testing.T) {
resolver := newMockResolver()
matches := FindAgentsByCapability(resolver, "anything")
if len(matches) != 0 {
t.Errorf("expected empty, got %v", matches)
}
}
func TestAgentInfo_Capabilities(t *testing.T) {
agent := &AgentInfo{
ID: "coder",
Name: "Code Agent",
Capabilities: []string{"coding", "review", "testing"},
}
if len(agent.Capabilities) != 3 {
t.Errorf("Capabilities len = %d, want 3", len(agent.Capabilities))
}
// Nil capabilities should not panic
agent2 := &AgentInfo{ID: "basic"}
if agent2.Capabilities != nil {
t.Error("expected nil Capabilities for unset agent")
}
}
func TestBuildHandoffSystemPrompt(t *testing.T) { func TestBuildHandoffSystemPrompt(t *testing.T) {
agent := &AgentInfo{ agent := &AgentInfo{
Name: "Code Agent", Name: "Code Agent",