feat: add model parameter to spawn and subagent tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mingmxren 2026-03-06 12:56:57 +08:00
parent 73575c7d7d
commit 1b96f79577
3 changed files with 109 additions and 4 deletions

View file

@ -44,6 +44,10 @@ func (t *SpawnTool) Parameters() map[string]any {
"type": "string", "type": "string",
"description": "Optional target agent ID to delegate the task to", "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"}, "required": []string{"task"},
} }
@ -71,6 +75,7 @@ func (t *SpawnTool) execute(ctx context.Context, args map[string]any, cb AsyncCa
label, _ := args["label"].(string) label, _ := args["label"].(string)
agentID, _ := args["agent_id"].(string) agentID, _ := args["agent_id"].(string)
model, _ := args["model"].(string)
// Check allowlist if targeting a specific agent // Check allowlist if targeting a specific agent
if agentID != "" && t.allowlistCheck != nil { 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 // 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 { if err != nil {
return ErrorResult(fmt.Sprintf("failed to spawn subagent: %v", err)) return ErrorResult(fmt.Sprintf("failed to spawn subagent: %v", err))
} }

View file

@ -120,7 +120,7 @@ func (sm *SubagentManager) RegisterTool(tool Tool) {
func (sm *SubagentManager) Spawn( func (sm *SubagentManager) Spawn(
ctx context.Context, ctx context.Context,
task, label, agentID, originChannel, originChatID string, task, label, agentID, model, originChannel, originChatID string,
callback AsyncCallback, callback AsyncCallback,
) (string, error) { ) (string, error) {
sm.mu.Lock() sm.mu.Lock()
@ -133,6 +133,7 @@ func (sm *SubagentManager) Spawn(
ID: taskID, ID: taskID,
Task: task, Task: task,
Label: label, Label: label,
Model: model,
AgentID: agentID, AgentID: agentID,
OriginChannel: originChannel, OriginChannel: originChannel,
OriginChatID: originChatID, OriginChatID: originChatID,
@ -181,6 +182,19 @@ After completing the task, provide a clear summary of what was done.`
default: 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 // Run tool loop with access to tools
sm.mu.RLock() sm.mu.RLock()
tools := sm.tools tools := sm.tools
@ -204,7 +218,7 @@ After completing the task, provide a clear summary of what was done.`
loopResult, err := RunToolLoop(ctx, ToolLoopConfig{ loopResult, err := RunToolLoop(ctx, ToolLoopConfig{
Provider: sm.provider, Provider: sm.provider,
Model: sm.defaultModel, Model: resolvedModel,
Tools: tools, Tools: tools,
MaxIterations: maxIter, MaxIterations: maxIter,
LLMOptions: llmOptions, LLMOptions: llmOptions,
@ -319,6 +333,10 @@ func (t *SubagentTool) Parameters() map[string]any {
"type": "string", "type": "string",
"description": "Optional short label for the task (for display)", "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"}, "required": []string{"task"},
} }
@ -331,11 +349,18 @@ func (t *SubagentTool) Execute(ctx context.Context, args map[string]any) *ToolRe
} }
label, _ := args["label"].(string) label, _ := args["label"].(string)
model, _ := args["model"].(string)
if t.manager == nil { if t.manager == nil {
return ErrorResult("Subagent manager not configured").WithError(fmt.Errorf("manager is 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 // Build messages for subagent
messages := []providers.Message{ messages := []providers.Message{
{ {
@ -383,7 +408,7 @@ func (t *SubagentTool) Execute(ctx context.Context, args map[string]any) *ToolRe
loopResult, err := RunToolLoop(ctx, ToolLoopConfig{ loopResult, err := RunToolLoop(ctx, ToolLoopConfig{
Provider: sm.provider, Provider: sm.provider,
Model: sm.defaultModel, Model: resolvedModel,
Tools: tools, Tools: tools,
MaxIterations: maxIter, MaxIterations: maxIter,
LLMOptions: llmOptions, LLMOptions: llmOptions,

View file

@ -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 // TestSubagentTool_ForUserTruncation verifies long content is truncated for user
func TestSubagentTool_ForUserTruncation(t *testing.T) { func TestSubagentTool_ForUserTruncation(t *testing.T) {
// Create a mock provider that returns very long content // Create a mock provider that returns very long content