diff --git a/pkg/tools/spawn.go b/pkg/tools/spawn.go index be40ffda2..48ec6a222 100644 --- a/pkg/tools/spawn.go +++ b/pkg/tools/spawn.go @@ -44,6 +44,10 @@ func (t *SpawnTool) Parameters() map[string]any { "type": "string", "description": "Optional target agent ID to delegate the task to", }, + "model": map[string]any{ + "type": "string", + "description": "Optional model_name from model_list to use for the subagent", + }, }, "required": []string{"task"}, } @@ -71,6 +75,7 @@ func (t *SpawnTool) execute(ctx context.Context, args map[string]any, cb AsyncCa label, _ := args["label"].(string) agentID, _ := args["agent_id"].(string) + model, _ := args["model"].(string) // Check allowlist if targeting a specific agent if agentID != "" && t.allowlistCheck != nil { @@ -96,7 +101,7 @@ func (t *SpawnTool) execute(ctx context.Context, args map[string]any, cb AsyncCa } // Pass callback to manager for async completion notification - result, err := t.manager.Spawn(ctx, task, label, agentID, channel, chatID, cb) + result, err := t.manager.Spawn(ctx, task, label, agentID, model, channel, chatID, cb) if err != nil { return ErrorResult(fmt.Sprintf("failed to spawn subagent: %v", err)) } diff --git a/pkg/tools/subagent.go b/pkg/tools/subagent.go index 10cf8b6c8..2bc2b4049 100644 --- a/pkg/tools/subagent.go +++ b/pkg/tools/subagent.go @@ -120,7 +120,7 @@ func (sm *SubagentManager) RegisterTool(tool Tool) { func (sm *SubagentManager) Spawn( ctx context.Context, - task, label, agentID, originChannel, originChatID string, + task, label, agentID, model, originChannel, originChatID string, callback AsyncCallback, ) (string, error) { sm.mu.Lock() @@ -133,6 +133,7 @@ func (sm *SubagentManager) Spawn( ID: taskID, Task: task, Label: label, + Model: model, AgentID: agentID, OriginChannel: originChannel, OriginChatID: originChatID, @@ -181,6 +182,19 @@ After completing the task, provide a clear summary of what was done.` default: } + // Resolve the model for this task + resolvedModel, err := sm.ResolveModel(task.Model) + if err != nil { + sm.mu.Lock() + task.Status = "failed" + task.Result = err.Error() + sm.mu.Unlock() + if callback != nil { + callback(ctx, ErrorResult(err.Error())) + } + return + } + // Run tool loop with access to tools sm.mu.RLock() tools := sm.tools @@ -204,7 +218,7 @@ After completing the task, provide a clear summary of what was done.` loopResult, err := RunToolLoop(ctx, ToolLoopConfig{ Provider: sm.provider, - Model: sm.defaultModel, + Model: resolvedModel, Tools: tools, MaxIterations: maxIter, LLMOptions: llmOptions, @@ -319,6 +333,10 @@ func (t *SubagentTool) Parameters() map[string]any { "type": "string", "description": "Optional short label for the task (for display)", }, + "model": map[string]any{ + "type": "string", + "description": "Optional model_name from model_list to use for the subagent", + }, }, "required": []string{"task"}, } @@ -331,11 +349,18 @@ func (t *SubagentTool) Execute(ctx context.Context, args map[string]any) *ToolRe } label, _ := args["label"].(string) + model, _ := args["model"].(string) if t.manager == nil { return ErrorResult("Subagent manager not configured").WithError(fmt.Errorf("manager is nil")) } + // Resolve the model before proceeding + resolvedModel, err := t.manager.ResolveModel(model) + if err != nil { + return ErrorResult(fmt.Sprintf("invalid model: %v", err)).WithError(err) + } + // Build messages for subagent messages := []providers.Message{ { @@ -383,7 +408,7 @@ func (t *SubagentTool) Execute(ctx context.Context, args map[string]any) *ToolRe loopResult, err := RunToolLoop(ctx, ToolLoopConfig{ Provider: sm.provider, - Model: sm.defaultModel, + Model: resolvedModel, Tools: tools, MaxIterations: maxIter, LLMOptions: llmOptions, diff --git a/pkg/tools/subagent_tool_test.go b/pkg/tools/subagent_tool_test.go index 8ae30b4f1..776857dfc 100644 --- a/pkg/tools/subagent_tool_test.go +++ b/pkg/tools/subagent_tool_test.go @@ -362,6 +362,81 @@ func TestSubagentManager_ResolveModel(t *testing.T) { }) } +func TestSubagentTool_Execute_WithModel(t *testing.T) { + provider := &MockLLMProvider{} + msgBus := bus.NewMessageBus() + manager := NewSubagentManager(provider, "parent-model", "/tmp/test", msgBus) + manager.SetModelValidator(func(name string) bool { + return name == "custom-model" || name == "parent-model" + }) + tool := NewSubagentTool(manager) + + ctx := WithToolContext(context.Background(), "cli", "direct") + args := map[string]any{ + "task": "Test with custom model", + "model": "custom-model", + } + + result := tool.Execute(ctx, args) + if result.IsError { + t.Errorf("Expected success, got error: %s", result.ForLLM) + } +} + +func TestSubagentTool_Execute_WithInvalidModel(t *testing.T) { + provider := &MockLLMProvider{} + msgBus := bus.NewMessageBus() + manager := NewSubagentManager(provider, "parent-model", "/tmp/test", msgBus) + manager.SetModelValidator(func(name string) bool { + return name == "parent-model" + }) + tool := NewSubagentTool(manager) + + ctx := WithToolContext(context.Background(), "cli", "direct") + args := map[string]any{ + "task": "Test with bad model", + "model": "nonexistent-model", + } + + result := tool.Execute(ctx, args) + if !result.IsError { + t.Error("Expected error for invalid model") + } + if !strings.Contains(result.ForLLM, "not found in model_list") { + t.Errorf("Error should mention model_list, got: %s", result.ForLLM) + } +} + +func TestSpawnTool_Parameters_IncludesModel(t *testing.T) { + provider := &MockLLMProvider{} + manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil) + tool := NewSpawnTool(manager) + params := tool.Parameters() + props := params["properties"].(map[string]any) + model, ok := props["model"].(map[string]any) + if !ok { + t.Fatal("model parameter should exist in spawn tool") + } + if model["type"] != "string" { + t.Errorf("model type should be 'string', got: %v", model["type"]) + } +} + +func TestSubagentTool_Parameters_IncludesModel(t *testing.T) { + provider := &MockLLMProvider{} + manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil) + tool := NewSubagentTool(manager) + params := tool.Parameters() + props := params["properties"].(map[string]any) + model, ok := props["model"].(map[string]any) + if !ok { + t.Fatal("model parameter should exist in subagent tool") + } + if model["type"] != "string" { + t.Errorf("model type should be 'string', got: %v", model["type"]) + } +} + // TestSubagentTool_ForUserTruncation verifies long content is truncated for user func TestSubagentTool_ForUserTruncation(t *testing.T) { // Create a mock provider that returns very long content