feat(teams): add model tags system for capability-aware agent routing
- Added ModelTag* constants (vision, code, fast, long-context, reasoning) to subagent.go - Added Tags []string to config.ModelConfig (json:"tags,omitempty") - Piped tags from config through FallbackCandidate and into SubagentManager.allowedModels - Changed ResolveCandidatesWithLookup lookup signature to return (string, []string, bool) to carry tags - Added ModelCapabilityHint() that generates rich per-model routing guidance for the LLM - Dynamically injected capability hints into TeamTool and SpawnSubAgentTool descriptions - Fixed fallback_test.go and subagent test files to match updated signatures
This commit is contained in:
parent
429ce66a7d
commit
efb9dc27ad
10 changed files with 172 additions and 43 deletions
|
|
@ -92,7 +92,7 @@ func NewAgentInstance(
|
||||||
Primary: model,
|
Primary: model,
|
||||||
Fallbacks: fallbacks,
|
Fallbacks: fallbacks,
|
||||||
}
|
}
|
||||||
resolveFromModelList := func(raw string) (string, bool) {
|
resolveFromModelList := func(raw string) (string, []string, bool) {
|
||||||
ensureProtocol := func(model string) string {
|
ensureProtocol := func(model string) string {
|
||||||
model = strings.TrimSpace(model)
|
model = strings.TrimSpace(model)
|
||||||
if model == "" {
|
if model == "" {
|
||||||
|
|
@ -106,12 +106,12 @@ func NewAgentInstance(
|
||||||
|
|
||||||
raw = strings.TrimSpace(raw)
|
raw = strings.TrimSpace(raw)
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
return "", false
|
return "", nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg != nil {
|
if cfg != nil {
|
||||||
if mc, err := cfg.GetModelConfig(raw); err == nil && mc != nil && strings.TrimSpace(mc.Model) != "" {
|
if mc, err := cfg.GetModelConfig(raw); err == nil && mc != nil && strings.TrimSpace(mc.Model) != "" {
|
||||||
return ensureProtocol(mc.Model), true
|
return ensureProtocol(mc.Model), mc.Tags, true
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range cfg.ModelList {
|
for i := range cfg.ModelList {
|
||||||
|
|
@ -120,16 +120,16 @@ func NewAgentInstance(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if fullModel == raw {
|
if fullModel == raw {
|
||||||
return ensureProtocol(fullModel), true
|
return ensureProtocol(fullModel), cfg.ModelList[i].Tags, true
|
||||||
}
|
}
|
||||||
_, modelID := providers.ExtractProtocol(fullModel)
|
_, modelID := providers.ExtractProtocol(fullModel)
|
||||||
if modelID == raw {
|
if modelID == raw {
|
||||||
return ensureProtocol(fullModel), true
|
return ensureProtocol(fullModel), cfg.ModelList[i].Tags, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", false
|
return "", nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates := providers.ResolveCandidatesWithLookup(modelCfg, defaults.Provider, resolveFromModelList)
|
candidates := providers.ResolveCandidatesWithLookup(modelCfg, defaults.Provider, resolveFromModelList)
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ func registerSharedTools(
|
||||||
agent.Tools.Register(tools.NewInstallSkillTool(registryMgr, agent.Workspace))
|
agent.Tools.Register(tools.NewInstallSkillTool(registryMgr, agent.Workspace))
|
||||||
|
|
||||||
// Spawn tool with allowlist checker
|
// Spawn tool with allowlist checker
|
||||||
subagentManager := tools.NewSubagentManager(provider, agent.Model, agent.Workspace, msgBus)
|
subagentManager := tools.NewSubagentManager(provider, agent.Model, agent.Candidates, agent.Workspace, msgBus)
|
||||||
subagentManager.SetLLMOptions(agent.MaxTokens, agent.Temperature)
|
subagentManager.SetLLMOptions(agent.MaxTokens, agent.Temperature)
|
||||||
spawnTool := tools.NewSpawnTool(subagentManager)
|
spawnTool := tools.NewSpawnTool(subagentManager)
|
||||||
currentAgentID := agentID
|
currentAgentID := agentID
|
||||||
|
|
|
||||||
|
|
@ -457,8 +457,9 @@ type OpenAIProviderConfig struct {
|
||||||
// Default protocol is "openai" if no prefix is specified.
|
// Default protocol is "openai" if no prefix is specified.
|
||||||
type ModelConfig struct {
|
type ModelConfig struct {
|
||||||
// Required fields
|
// Required fields
|
||||||
ModelName string `json:"model_name"` // User-facing alias for the model
|
ModelName string `json:"model_name"` // User-facing alias for the model
|
||||||
Model string `json:"model"` // Protocol/model-identifier (e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4.6")
|
Model string `json:"model"` // Protocol/model-identifier (e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4.6")
|
||||||
|
Tags []string `json:"tags,omitempty"` // Model capability labels like 'vision'
|
||||||
|
|
||||||
// HTTP-based providers
|
// HTTP-based providers
|
||||||
APIBase string `json:"api_base,omitempty"` // API endpoint URL
|
APIBase string `json:"api_base,omitempty"` // API endpoint URL
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ type FallbackChain struct {
|
||||||
type FallbackCandidate struct {
|
type FallbackCandidate struct {
|
||||||
Provider string
|
Provider string
|
||||||
Model string
|
Model string
|
||||||
|
Tags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// FallbackResult contains the successful response and metadata about all attempts.
|
// FallbackResult contains the successful response and metadata about all attempts.
|
||||||
|
|
@ -49,16 +50,19 @@ func ResolveCandidates(cfg ModelConfig, defaultProvider string) []FallbackCandid
|
||||||
func ResolveCandidatesWithLookup(
|
func ResolveCandidatesWithLookup(
|
||||||
cfg ModelConfig,
|
cfg ModelConfig,
|
||||||
defaultProvider string,
|
defaultProvider string,
|
||||||
lookup func(raw string) (resolved string, ok bool),
|
lookup func(raw string) (resolved string, tags []string, ok bool),
|
||||||
) []FallbackCandidate {
|
) []FallbackCandidate {
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
var candidates []FallbackCandidate
|
var candidates []FallbackCandidate
|
||||||
|
|
||||||
addCandidate := func(raw string) {
|
addCandidate := func(raw string) {
|
||||||
candidateRaw := strings.TrimSpace(raw)
|
candidateRaw := strings.TrimSpace(raw)
|
||||||
|
var modelTags []string
|
||||||
|
|
||||||
if lookup != nil {
|
if lookup != nil {
|
||||||
if resolved, ok := lookup(candidateRaw); ok {
|
if resolved, tags, ok := lookup(candidateRaw); ok {
|
||||||
candidateRaw = resolved
|
candidateRaw = resolved
|
||||||
|
modelTags = tags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,6 +78,7 @@ func ResolveCandidatesWithLookup(
|
||||||
candidates = append(candidates, FallbackCandidate{
|
candidates = append(candidates, FallbackCandidate{
|
||||||
Provider: ref.Provider,
|
Provider: ref.Provider,
|
||||||
Model: ref.Model,
|
Model: ref.Model,
|
||||||
|
Tags: modelTags,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -459,11 +459,11 @@ func TestResolveCandidatesWithLookup_AliasResolvesToNestedModel(t *testing.T) {
|
||||||
Fallbacks: nil,
|
Fallbacks: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
lookup := func(raw string) (string, bool) {
|
lookup := func(raw string) (string, []string, bool) {
|
||||||
if raw == "step-3.5-flash" {
|
if raw == "step-3.5-flash" {
|
||||||
return "openrouter/stepfun/step-3.5-flash:free", true
|
return "openrouter/stepfun/step-3.5-flash:free", nil, true
|
||||||
}
|
}
|
||||||
return "", false
|
return "", nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates := ResolveCandidatesWithLookup(cfg, "", lookup)
|
candidates := ResolveCandidatesWithLookup(cfg, "", lookup)
|
||||||
|
|
@ -484,11 +484,11 @@ func TestResolveCandidatesWithLookup_DeduplicateAfterLookup(t *testing.T) {
|
||||||
Fallbacks: []string{"openrouter/stepfun/step-3.5-flash:free"},
|
Fallbacks: []string{"openrouter/stepfun/step-3.5-flash:free"},
|
||||||
}
|
}
|
||||||
|
|
||||||
lookup := func(raw string) (string, bool) {
|
lookup := func(raw string) (string, []string, bool) {
|
||||||
if raw == "step-3.5-flash" {
|
if raw == "step-3.5-flash" {
|
||||||
return "openrouter/stepfun/step-3.5-flash:free", true
|
return "openrouter/stepfun/step-3.5-flash:free", nil, true
|
||||||
}
|
}
|
||||||
return "", false
|
return "", nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates := ResolveCandidatesWithLookup(cfg, "", lookup)
|
candidates := ResolveCandidatesWithLookup(cfg, "", lookup)
|
||||||
|
|
@ -503,11 +503,11 @@ func TestResolveCandidatesWithLookup_AliasWithoutProtocolUsesDefaultProvider(t *
|
||||||
Fallbacks: nil,
|
Fallbacks: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
lookup := func(raw string) (string, bool) {
|
lookup := func(raw string) (string, []string, bool) {
|
||||||
if raw == "glm-5" {
|
if raw == "glm-5" {
|
||||||
return "glm-5", true
|
return "glm-5", nil, true
|
||||||
}
|
}
|
||||||
return "", false
|
return "", nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates := ResolveCandidatesWithLookup(cfg, "openai", lookup)
|
candidates := ResolveCandidatesWithLookup(cfg, "openai", lookup)
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,13 @@ func (t *SpawnSubAgentTool) Name() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *SpawnSubAgentTool) Description() string {
|
func (t *SpawnSubAgentTool) Description() string {
|
||||||
return "Directly delegate a specific task to a new, isolated sub-agent. You (the main agent) should autonomously determine the appropriate expert role and specific task based on the user's high-level request. It will execute independently and return the final result."
|
base := "Directly delegate a specific task to a new, isolated sub-agent. You (the main agent) should autonomously determine the appropriate expert role and specific task based on the user's high-level request. It will execute independently and return the final result."
|
||||||
|
if t.manager != nil {
|
||||||
|
if hint := t.manager.ModelCapabilityHint(); hint != "" {
|
||||||
|
return base + "\n\n" + hint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *SpawnSubAgentTool) Parameters() map[string]any {
|
func (t *SpawnSubAgentTool) Parameters() map[string]any {
|
||||||
|
|
@ -89,7 +95,11 @@ func (t *SpawnSubAgentTool) Execute(ctx context.Context, args map[string]any) *T
|
||||||
|
|
||||||
// 2.1 Model Override (Heterogeneous Agents)
|
// 2.1 Model Override (Heterogeneous Agents)
|
||||||
if modelParam, ok := args["model"].(string); ok && strings.TrimSpace(modelParam) != "" {
|
if modelParam, ok := args["model"].(string); ok && strings.TrimSpace(modelParam) != "" {
|
||||||
config.Model = strings.TrimSpace(modelParam)
|
requestedModel := strings.TrimSpace(modelParam)
|
||||||
|
if !t.manager.IsModelAllowed(requestedModel) {
|
||||||
|
return ErrorResult(fmt.Sprintf("requested model '%s' is not in the allowed fallback candidates list for this agent workspace", requestedModel)).WithError(fmt.Errorf("model %s not allowed", requestedModel))
|
||||||
|
}
|
||||||
|
config.Model = requestedModel
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: For MVP, we pass the current ToolRegistry unmodified.
|
// Note: For MVP, we pass the current ToolRegistry unmodified.
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
func TestSpawnTool_Execute_EmptyTask(t *testing.T) {
|
func TestSpawnTool_Execute_EmptyTask(t *testing.T) {
|
||||||
provider := &MockLLMProvider{}
|
provider := &MockLLMProvider{}
|
||||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil)
|
manager := NewSubagentManager(provider, "test-model", nil, "/tmp/test", nil)
|
||||||
tool := NewSpawnTool(manager)
|
tool := NewSpawnTool(manager)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
@ -42,7 +42,7 @@ func TestSpawnTool_Execute_EmptyTask(t *testing.T) {
|
||||||
|
|
||||||
func TestSpawnTool_Execute_ValidTask(t *testing.T) {
|
func TestSpawnTool_Execute_ValidTask(t *testing.T) {
|
||||||
provider := &MockLLMProvider{}
|
provider := &MockLLMProvider{}
|
||||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil)
|
manager := NewSubagentManager(provider, "test-model", nil, "/tmp/test", nil)
|
||||||
tool := NewSpawnTool(manager)
|
tool := NewSpawnTool(manager)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package tools
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -10,6 +11,26 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/providers"
|
"github.com/sipeed/picoclaw/pkg/providers"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ModelTag constants define the recognized capability labels for models in config.json.
|
||||||
|
// These are set via `"tags": ["vision", "code"]` under each model in the model list.
|
||||||
|
const (
|
||||||
|
ModelTagVision = "vision" // Supports image/screenshot input (multimodal)
|
||||||
|
ModelTagCode = "code" // Specialized for code generation and analysis
|
||||||
|
ModelTagFast = "fast" // Low-latency model, suited for lightweight tasks
|
||||||
|
ModelTagLongContext = "long-context" // Supports very long context windows (>100k tokens)
|
||||||
|
ModelTagReasoning = "reasoning" // Strong logical/math reasoning (e.g., o1, deepseek-r1)
|
||||||
|
)
|
||||||
|
|
||||||
|
// modelTagDescriptions provides LLM-readable explanations of each known tag,
|
||||||
|
// injected at runtime into the tool description to guide model selection.
|
||||||
|
var modelTagDescriptions = map[string]string{
|
||||||
|
ModelTagVision: "can analyze images and screenshots",
|
||||||
|
ModelTagCode: "specialized in code generation and debugging",
|
||||||
|
ModelTagFast: "fast and lightweight, ideal for simple or high-frequency tasks",
|
||||||
|
ModelTagLongContext: "handles very long inputs (>100k tokens)",
|
||||||
|
ModelTagReasoning: "excels at logical reasoning, math, and multi-step planning",
|
||||||
|
}
|
||||||
|
|
||||||
type SubagentTask struct {
|
type SubagentTask struct {
|
||||||
ID string
|
ID string
|
||||||
Task string
|
Task string
|
||||||
|
|
@ -27,6 +48,7 @@ type SubagentManager struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
provider providers.LLMProvider
|
provider providers.LLMProvider
|
||||||
defaultModel string
|
defaultModel string
|
||||||
|
allowedModels []providers.FallbackCandidate
|
||||||
bus *bus.MessageBus
|
bus *bus.MessageBus
|
||||||
workspace string
|
workspace string
|
||||||
tools *ToolRegistry
|
tools *ToolRegistry
|
||||||
|
|
@ -40,13 +62,16 @@ type SubagentManager struct {
|
||||||
|
|
||||||
func NewSubagentManager(
|
func NewSubagentManager(
|
||||||
provider providers.LLMProvider,
|
provider providers.LLMProvider,
|
||||||
defaultModel, workspace string,
|
defaultModel string,
|
||||||
|
candidates []providers.FallbackCandidate,
|
||||||
|
workspace string,
|
||||||
bus *bus.MessageBus,
|
bus *bus.MessageBus,
|
||||||
) *SubagentManager {
|
) *SubagentManager {
|
||||||
return &SubagentManager{
|
return &SubagentManager{
|
||||||
tasks: make(map[string]*SubagentTask),
|
tasks: make(map[string]*SubagentTask),
|
||||||
provider: provider,
|
provider: provider,
|
||||||
defaultModel: defaultModel,
|
defaultModel: defaultModel,
|
||||||
|
allowedModels: candidates,
|
||||||
bus: bus,
|
bus: bus,
|
||||||
workspace: workspace,
|
workspace: workspace,
|
||||||
tools: NewToolRegistry(),
|
tools: NewToolRegistry(),
|
||||||
|
|
@ -55,6 +80,52 @@ func NewSubagentManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsModelAllowed checks if a specific requested model exists in the permitted candidates list.
|
||||||
|
func (sm *SubagentManager) IsModelAllowed(model string) bool {
|
||||||
|
// If the user requested the default model directly, that's automatically allowed
|
||||||
|
if model == sm.defaultModel {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, check against the resolved candidates (primary + fallbacks + explicitly configured)
|
||||||
|
for _, cand := range sm.allowedModels {
|
||||||
|
if cand.Model == model {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelCapabilityHint generates a human-readable summary of allowed models and their tags.
|
||||||
|
// This is injected into the coordinator's tool descriptions so the LLM can make better routing decisions.
|
||||||
|
func (sm *SubagentManager) ModelCapabilityHint() string {
|
||||||
|
if len(sm.allowedModels) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var modelLines []string
|
||||||
|
for _, cand := range sm.allowedModels {
|
||||||
|
if len(cand.Tags) == 0 {
|
||||||
|
modelLines = append(modelLines, fmt.Sprintf(" - %s (general purpose)", cand.Model))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var descs []string
|
||||||
|
for _, tag := range cand.Tags {
|
||||||
|
if desc, known := modelTagDescriptions[tag]; known {
|
||||||
|
descs = append(descs, fmt.Sprintf("%s (%s)", tag, desc))
|
||||||
|
} else {
|
||||||
|
descs = append(descs, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modelLines = append(modelLines, fmt.Sprintf(" - %s [%s]", cand.Model, strings.Join(descs, ", ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
hint := "When selecting a 'model' for sub-agents, use ONLY these configured models:\n"
|
||||||
|
hint += strings.Join(modelLines, "\n")
|
||||||
|
hint += "\nIf a task requires vision/image analysis, you MUST select a model with the 'vision' tag. If no suitable model is available, omit the 'model' field to use the default."
|
||||||
|
return hint
|
||||||
|
}
|
||||||
|
|
||||||
// SetLLMOptions sets max tokens and temperature for subagent LLM calls.
|
// SetLLMOptions sets max tokens and temperature for subagent LLM calls.
|
||||||
func (sm *SubagentManager) SetLLMOptions(maxTokens int, temperature float64) {
|
func (sm *SubagentManager) SetLLMOptions(maxTokens int, temperature float64) {
|
||||||
sm.mu.Lock()
|
sm.mu.Lock()
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ func (m *MockLLMProvider) GetContextWindow() int {
|
||||||
|
|
||||||
func TestSubagentManager_SetLLMOptions_AppliesToRunToolLoop(t *testing.T) {
|
func TestSubagentManager_SetLLMOptions_AppliesToRunToolLoop(t *testing.T) {
|
||||||
provider := &MockLLMProvider{}
|
provider := &MockLLMProvider{}
|
||||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil)
|
manager := NewSubagentManager(provider, "test-model", nil, "/tmp/test", nil)
|
||||||
manager.SetLLMOptions(2048, 0.6)
|
manager.SetLLMOptions(2048, 0.6)
|
||||||
tool := NewSubagentTool(manager)
|
tool := NewSubagentTool(manager)
|
||||||
tool.SetContext("cli", "direct")
|
tool.SetContext("cli", "direct")
|
||||||
|
|
@ -74,7 +74,7 @@ func TestSubagentManager_SetLLMOptions_AppliesToRunToolLoop(t *testing.T) {
|
||||||
// TestSubagentTool_Name verifies tool name
|
// TestSubagentTool_Name verifies tool name
|
||||||
func TestSubagentTool_Name(t *testing.T) {
|
func TestSubagentTool_Name(t *testing.T) {
|
||||||
provider := &MockLLMProvider{}
|
provider := &MockLLMProvider{}
|
||||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil)
|
manager := NewSubagentManager(provider, "test-model", nil, "/tmp/test", nil)
|
||||||
tool := NewSubagentTool(manager)
|
tool := NewSubagentTool(manager)
|
||||||
|
|
||||||
if tool.Name() != "subagent" {
|
if tool.Name() != "subagent" {
|
||||||
|
|
@ -85,7 +85,7 @@ func TestSubagentTool_Name(t *testing.T) {
|
||||||
// TestSubagentTool_Description verifies tool description
|
// TestSubagentTool_Description verifies tool description
|
||||||
func TestSubagentTool_Description(t *testing.T) {
|
func TestSubagentTool_Description(t *testing.T) {
|
||||||
provider := &MockLLMProvider{}
|
provider := &MockLLMProvider{}
|
||||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil)
|
manager := NewSubagentManager(provider, "test-model", nil, "/tmp/test", nil)
|
||||||
tool := NewSubagentTool(manager)
|
tool := NewSubagentTool(manager)
|
||||||
|
|
||||||
desc := tool.Description()
|
desc := tool.Description()
|
||||||
|
|
@ -100,7 +100,7 @@ func TestSubagentTool_Description(t *testing.T) {
|
||||||
// TestSubagentTool_Parameters verifies tool parameters schema
|
// TestSubagentTool_Parameters verifies tool parameters schema
|
||||||
func TestSubagentTool_Parameters(t *testing.T) {
|
func TestSubagentTool_Parameters(t *testing.T) {
|
||||||
provider := &MockLLMProvider{}
|
provider := &MockLLMProvider{}
|
||||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil)
|
manager := NewSubagentManager(provider, "test-model", nil, "/tmp/test", nil)
|
||||||
tool := NewSubagentTool(manager)
|
tool := NewSubagentTool(manager)
|
||||||
|
|
||||||
params := tool.Parameters()
|
params := tool.Parameters()
|
||||||
|
|
@ -150,7 +150,7 @@ func TestSubagentTool_Parameters(t *testing.T) {
|
||||||
// TestSubagentTool_SetContext verifies context setting
|
// TestSubagentTool_SetContext verifies context setting
|
||||||
func TestSubagentTool_SetContext(t *testing.T) {
|
func TestSubagentTool_SetContext(t *testing.T) {
|
||||||
provider := &MockLLMProvider{}
|
provider := &MockLLMProvider{}
|
||||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil)
|
manager := NewSubagentManager(provider, "test-model", nil, "/tmp/test", nil)
|
||||||
tool := NewSubagentTool(manager)
|
tool := NewSubagentTool(manager)
|
||||||
|
|
||||||
tool.SetContext("test-channel", "test-chat")
|
tool.SetContext("test-channel", "test-chat")
|
||||||
|
|
@ -164,7 +164,7 @@ func TestSubagentTool_SetContext(t *testing.T) {
|
||||||
func TestSubagentTool_Execute_Success(t *testing.T) {
|
func TestSubagentTool_Execute_Success(t *testing.T) {
|
||||||
provider := &MockLLMProvider{}
|
provider := &MockLLMProvider{}
|
||||||
msgBus := bus.NewMessageBus()
|
msgBus := bus.NewMessageBus()
|
||||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus)
|
manager := NewSubagentManager(provider, "test-model", nil, "/tmp/test", msgBus)
|
||||||
tool := NewSubagentTool(manager)
|
tool := NewSubagentTool(manager)
|
||||||
tool.SetContext("telegram", "chat-123")
|
tool.SetContext("telegram", "chat-123")
|
||||||
|
|
||||||
|
|
@ -220,7 +220,7 @@ func TestSubagentTool_Execute_Success(t *testing.T) {
|
||||||
func TestSubagentTool_Execute_NoLabel(t *testing.T) {
|
func TestSubagentTool_Execute_NoLabel(t *testing.T) {
|
||||||
provider := &MockLLMProvider{}
|
provider := &MockLLMProvider{}
|
||||||
msgBus := bus.NewMessageBus()
|
msgBus := bus.NewMessageBus()
|
||||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus)
|
manager := NewSubagentManager(provider, "test-model", nil, "/tmp/test", msgBus)
|
||||||
tool := NewSubagentTool(manager)
|
tool := NewSubagentTool(manager)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
@ -243,7 +243,7 @@ func TestSubagentTool_Execute_NoLabel(t *testing.T) {
|
||||||
// TestSubagentTool_Execute_MissingTask tests error handling for missing task
|
// TestSubagentTool_Execute_MissingTask tests error handling for missing task
|
||||||
func TestSubagentTool_Execute_MissingTask(t *testing.T) {
|
func TestSubagentTool_Execute_MissingTask(t *testing.T) {
|
||||||
provider := &MockLLMProvider{}
|
provider := &MockLLMProvider{}
|
||||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil)
|
manager := NewSubagentManager(provider, "test-model", nil, "/tmp/test", nil)
|
||||||
tool := NewSubagentTool(manager)
|
tool := NewSubagentTool(manager)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
@ -294,7 +294,7 @@ func TestSubagentTool_Execute_NilManager(t *testing.T) {
|
||||||
func TestSubagentTool_Execute_ContextPassing(t *testing.T) {
|
func TestSubagentTool_Execute_ContextPassing(t *testing.T) {
|
||||||
provider := &MockLLMProvider{}
|
provider := &MockLLMProvider{}
|
||||||
msgBus := bus.NewMessageBus()
|
msgBus := bus.NewMessageBus()
|
||||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus)
|
manager := NewSubagentManager(provider, "test-model", nil, "/tmp/test", msgBus)
|
||||||
tool := NewSubagentTool(manager)
|
tool := NewSubagentTool(manager)
|
||||||
|
|
||||||
// Set context
|
// Set context
|
||||||
|
|
@ -323,7 +323,7 @@ func TestSubagentTool_ForUserTruncation(t *testing.T) {
|
||||||
// Create a mock provider that returns very long content
|
// Create a mock provider that returns very long content
|
||||||
provider := &MockLLMProvider{}
|
provider := &MockLLMProvider{}
|
||||||
msgBus := bus.NewMessageBus()
|
msgBus := bus.NewMessageBus()
|
||||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus)
|
manager := NewSubagentManager(provider, "test-model", nil, "/tmp/test", msgBus)
|
||||||
tool := NewSubagentTool(manager)
|
tool := NewSubagentTool(manager)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,13 @@ func (t *TeamTool) Name() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TeamTool) Description() string {
|
func (t *TeamTool) Description() string {
|
||||||
return "Compose and execute a team of distinct sub-agents. You (the main agent) should autonomously analyze the user's request, determine the necessary specialized roles, break down the work into sub-tasks, and assign them. Execute sequentially (passing output from one to the next) or concurrently in parallel."
|
base := "Compose and execute a team of distinct sub-agents. You (the main agent) should autonomously analyze the user's request, determine the necessary specialized roles, break down the work into sub-tasks, and assign them. Execute sequentially (passing output from one to the next) or concurrently in parallel."
|
||||||
|
if t.manager != nil {
|
||||||
|
if hint := t.manager.ModelCapabilityHint(); hint != "" {
|
||||||
|
return base + "\n\n" + hint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TeamTool) Parameters() map[string]any {
|
func (t *TeamTool) Parameters() map[string]any {
|
||||||
|
|
@ -210,14 +216,17 @@ func upgradeRegistryForConcurrency(original *ToolRegistry) *ToolRegistry {
|
||||||
|
|
||||||
// buildWorkerConfig creates a ToolLoopConfig for a specific team member,
|
// buildWorkerConfig creates a ToolLoopConfig for a specific team member,
|
||||||
// potentially overriding the model based on the member's definition.
|
// potentially overriding the model based on the member's definition.
|
||||||
func buildWorkerConfig(baseConfig ToolLoopConfig, registry *ToolRegistry, m TeamMember) ToolLoopConfig {
|
func buildWorkerConfig(baseConfig ToolLoopConfig, registry *ToolRegistry, m TeamMember, manager *SubagentManager) (ToolLoopConfig, error) {
|
||||||
cfg := baseConfig
|
cfg := baseConfig
|
||||||
cfg.Tools = registry
|
cfg.Tools = registry
|
||||||
// Heterogeneous Agents: Override model if this team member requested a specific one
|
// Heterogeneous Agents: Override model if this team member requested a specific one
|
||||||
if m.Model != "" {
|
if m.Model != "" {
|
||||||
|
if !manager.IsModelAllowed(m.Model) {
|
||||||
|
return cfg, fmt.Errorf("requested model '%s' is not in the allowed fallback candidates list for this agent workspace", m.Model)
|
||||||
|
}
|
||||||
cfg.Model = m.Model
|
cfg.Model = m.Model
|
||||||
}
|
}
|
||||||
return cfg
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TeamTool) executeSequential(ctx context.Context, baseConfig ToolLoopConfig, members []TeamMember) *ToolResult {
|
func (t *TeamTool) executeSequential(ctx context.Context, baseConfig ToolLoopConfig, members []TeamMember) *ToolResult {
|
||||||
|
|
@ -238,7 +247,13 @@ func (t *TeamTool) executeSequential(ctx context.Context, baseConfig ToolLoopCon
|
||||||
{Role: "user", Content: actualTask},
|
{Role: "user", Content: actualTask},
|
||||||
}
|
}
|
||||||
|
|
||||||
workerConfig := buildWorkerConfig(baseConfig, baseConfig.Tools, m)
|
workerConfig, err := buildWorkerConfig(baseConfig, baseConfig.Tools, m, t.manager)
|
||||||
|
if err != nil {
|
||||||
|
errStr := fmt.Sprintf("Phase %d (Role: %s) configuration failed: %v", i+1, m.Role, err)
|
||||||
|
finalOutput.WriteString(errStr + "\n")
|
||||||
|
return ErrorResult(errStr).WithError(err)
|
||||||
|
}
|
||||||
|
|
||||||
loopResult, err := RunToolLoop(ctx, workerConfig, messages, t.originChannel, t.originChatID)
|
loopResult, err := RunToolLoop(ctx, workerConfig, messages, t.originChannel, t.originChatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStr := fmt.Sprintf("Phase %d (Role: %s) failed: %v", i+1, m.Role, err)
|
errStr := fmt.Sprintf("Phase %d (Role: %s) failed: %v", i+1, m.Role, err)
|
||||||
|
|
@ -278,7 +293,12 @@ func (t *TeamTool) executeParallel(ctx context.Context, baseConfig ToolLoopConfi
|
||||||
{Role: "user", Content: member.Task},
|
{Role: "user", Content: member.Task},
|
||||||
}
|
}
|
||||||
|
|
||||||
workerConfig := buildWorkerConfig(baseConfig, baseConfig.Tools, member)
|
workerConfig, err := buildWorkerConfig(baseConfig, baseConfig.Tools, member, t.manager)
|
||||||
|
if err != nil {
|
||||||
|
resultsChan <- workResult{index: index, role: member.Role, err: err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
loopResult, err := RunToolLoop(ctx, workerConfig, messages, t.originChannel, t.originChatID)
|
loopResult, err := RunToolLoop(ctx, workerConfig, messages, t.originChannel, t.originChatID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -344,7 +364,13 @@ func (t *TeamTool) executeEvaluatorOptimizer(ctx context.Context, baseConfig Too
|
||||||
finalOutput.WriteString(fmt.Sprintf("## Attempt %d\n", attempt))
|
finalOutput.WriteString(fmt.Sprintf("## Attempt %d\n", attempt))
|
||||||
|
|
||||||
// 2. Trigger Worker (resumes from its exact previous state!)
|
// 2. Trigger Worker (resumes from its exact previous state!)
|
||||||
workerConfig := buildWorkerConfig(baseConfig, baseConfig.Tools, worker)
|
workerConfig, err := buildWorkerConfig(baseConfig, baseConfig.Tools, worker, t.manager)
|
||||||
|
if err != nil {
|
||||||
|
errStr := fmt.Sprintf("Worker configuration failed on attempt %d: %v", attempt, err)
|
||||||
|
finalOutput.WriteString(errStr + "\n")
|
||||||
|
return ErrorResult(errStr).WithError(err)
|
||||||
|
}
|
||||||
|
|
||||||
workerResult, err := RunToolLoop(ctx, workerConfig, workerMessages, t.originChannel, t.originChatID)
|
workerResult, err := RunToolLoop(ctx, workerConfig, workerMessages, t.originChannel, t.originChatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStr := fmt.Sprintf("Worker failed on attempt %d: %v", attempt, err)
|
errStr := fmt.Sprintf("Worker failed on attempt %d: %v", attempt, err)
|
||||||
|
|
@ -365,7 +391,13 @@ func (t *TeamTool) executeEvaluatorOptimizer(ctx context.Context, baseConfig Too
|
||||||
{Role: "user", Content: evalContext},
|
{Role: "user", Content: evalContext},
|
||||||
}
|
}
|
||||||
|
|
||||||
evalConfig := buildWorkerConfig(baseConfig, baseConfig.Tools, evaluator)
|
evalConfig, err := buildWorkerConfig(baseConfig, baseConfig.Tools, evaluator, t.manager)
|
||||||
|
if err != nil {
|
||||||
|
errStr := fmt.Sprintf("Evaluator configuration failed on attempt %d: %v", attempt, err)
|
||||||
|
finalOutput.WriteString(errStr + "\n")
|
||||||
|
return ErrorResult(errStr).WithError(err)
|
||||||
|
}
|
||||||
|
|
||||||
evalResult, err := RunToolLoop(ctx, evalConfig, evalMessages, t.originChannel, t.originChatID)
|
evalResult, err := RunToolLoop(ctx, evalConfig, evalMessages, t.originChannel, t.originChatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStr := fmt.Sprintf("Evaluator failed on attempt %d: %v", attempt, err)
|
errStr := fmt.Sprintf("Evaluator failed on attempt %d: %v", attempt, err)
|
||||||
|
|
@ -514,7 +546,17 @@ func (t *TeamTool) executeDAG(ctx context.Context, baseConfig ToolLoopConfig, me
|
||||||
{Role: "user", Content: actualTask},
|
{Role: "user", Content: actualTask},
|
||||||
}
|
}
|
||||||
|
|
||||||
workerConfig := buildWorkerConfig(baseConfig, baseConfig.Tools, m)
|
workerConfig, err := buildWorkerConfig(baseConfig, baseConfig.Tools, m, t.manager)
|
||||||
|
if err != nil {
|
||||||
|
masterErrMu.Lock()
|
||||||
|
if masterErr == nil {
|
||||||
|
masterErr = err
|
||||||
|
}
|
||||||
|
masterErrMu.Unlock()
|
||||||
|
resultChan <- nodeResult{id: id, err: err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
loopResult, err := RunToolLoop(ctx, workerConfig, messages, t.originChannel, t.originChatID)
|
loopResult, err := RunToolLoop(ctx, workerConfig, messages, t.originChannel, t.originChatID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue