From 2e2b2178a3d9e14f21470ea2c457945b5f71fe85 Mon Sep 17 00:00:00 2001 From: mingmxren Date: Fri, 6 Mar 2026 12:49:14 +0800 Subject: [PATCH] feat: add ResolveModel to SubagentManager with 3-level fallback Co-Authored-By: Claude Opus 4.6 --- pkg/tools/subagent.go | 42 ++++++++++++++++++++-- pkg/tools/subagent_tool_test.go | 62 +++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/pkg/tools/subagent.go b/pkg/tools/subagent.go index 429340047..4cd23af2a 100644 --- a/pkg/tools/subagent.go +++ b/pkg/tools/subagent.go @@ -14,6 +14,7 @@ type SubagentTask struct { ID string Task string Label string + Model string // model_name override for this task AgentID string OriginChannel string OriginChatID string @@ -26,8 +27,10 @@ type SubagentManager struct { tasks map[string]*SubagentTask mu sync.RWMutex provider providers.LLMProvider - defaultModel string - bus *bus.MessageBus + defaultModel string + subagentDefaultModel string // from SubagentsConfig.Model + modelValidator func(string) bool // validates model_name exists + bus *bus.MessageBus workspace string tools *ToolRegistry maxIterations int @@ -65,6 +68,41 @@ func (sm *SubagentManager) SetLLMOptions(maxTokens int, temperature float64) { sm.hasTemperature = true } +// SetSubagentDefaultModel sets the default model for subagents from config. +func (sm *SubagentManager) SetSubagentDefaultModel(model string) { + sm.mu.Lock() + defer sm.mu.Unlock() + sm.subagentDefaultModel = model +} + +// SetModelValidator sets the function used to validate model names. +func (sm *SubagentManager) SetModelValidator(validator func(string) bool) { + sm.mu.Lock() + defer sm.mu.Unlock() + sm.modelValidator = validator +} + +// ResolveModel resolves the model to use for a subagent task. +// Priority: requested > subagentDefaultModel > defaultModel (parent). +func (sm *SubagentManager) ResolveModel(requested string) (string, error) { + sm.mu.RLock() + validator := sm.modelValidator + subDefault := sm.subagentDefaultModel + parentModel := sm.defaultModel + sm.mu.RUnlock() + + if requested != "" { + if validator != nil && !validator(requested) { + return "", fmt.Errorf("model %q not found in model_list", requested) + } + return requested, nil + } + if subDefault != "" { + return subDefault, nil + } + return parentModel, nil +} + // SetTools sets the tool registry for subagent execution. // If not set, subagent will have access to the provided tools. func (sm *SubagentManager) SetTools(tools *ToolRegistry) { diff --git a/pkg/tools/subagent_tool_test.go b/pkg/tools/subagent_tool_test.go index a1450410a..8ae30b4f1 100644 --- a/pkg/tools/subagent_tool_test.go +++ b/pkg/tools/subagent_tool_test.go @@ -300,6 +300,68 @@ func TestSubagentTool_Execute_ContextPassing(t *testing.T) { // but execution success indicates context was handled properly } +func TestSubagentManager_ResolveModel(t *testing.T) { + provider := &MockLLMProvider{} + alwaysValid := func(name string) bool { return true } + + t.Run("uses requested model when valid", func(t *testing.T) { + manager := NewSubagentManager(provider, "parent-model", "/tmp/test", nil) + manager.SetModelValidator(alwaysValid) + model, err := manager.ResolveModel("requested-model") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if model != "requested-model" { + t.Errorf("got %q, want %q", model, "requested-model") + } + }) + + t.Run("returns error for invalid requested model", func(t *testing.T) { + manager := NewSubagentManager(provider, "parent-model", "/tmp/test", nil) + manager.SetModelValidator(func(name string) bool { return name != "bad-model" }) + _, err := manager.ResolveModel("bad-model") + if err == nil { + t.Fatal("expected error for invalid model") + } + }) + + t.Run("falls back to subagent default model", func(t *testing.T) { + manager := NewSubagentManager(provider, "parent-model", "/tmp/test", nil) + manager.SetSubagentDefaultModel("subagent-default") + manager.SetModelValidator(alwaysValid) + model, err := manager.ResolveModel("") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if model != "subagent-default" { + t.Errorf("got %q, want %q", model, "subagent-default") + } + }) + + t.Run("falls back to parent model", func(t *testing.T) { + manager := NewSubagentManager(provider, "parent-model", "/tmp/test", nil) + manager.SetModelValidator(alwaysValid) + model, err := manager.ResolveModel("") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if model != "parent-model" { + t.Errorf("got %q, want %q", model, "parent-model") + } + }) + + t.Run("works without validator set (no validation)", func(t *testing.T) { + manager := NewSubagentManager(provider, "parent-model", "/tmp/test", nil) + model, err := manager.ResolveModel("any-model") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if model != "any-model" { + t.Errorf("got %q, want %q", model, "any-model") + } + }) +} + // TestSubagentTool_ForUserTruncation verifies long content is truncated for user func TestSubagentTool_ForUserTruncation(t *testing.T) { // Create a mock provider that returns very long content