From 2a6872ac84c2eeee1c10022a9db967aa230f2a2c Mon Sep 17 00:00:00 2001 From: Zezae Oh Date: Sat, 11 Jul 2026 13:54:18 +0900 Subject: [PATCH] fix: honor model override for sync subagents (#1423) --- internal/tools/subagent_spawn.go | 3 +- internal/tools/subagent_spawn_test.go | 48 +++++++++++++++++++++++++++ internal/tools/subagent_spawn_tool.go | 3 +- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 internal/tools/subagent_spawn_test.go diff --git a/internal/tools/subagent_spawn.go b/internal/tools/subagent_spawn.go index 0f44430b..f8497fed 100644 --- a/internal/tools/subagent_spawn.go +++ b/internal/tools/subagent_spawn.go @@ -127,7 +127,7 @@ func (sm *SubagentManager) RunSync( ctx context.Context, parentID string, depth int, - task, label string, + task, label, modelOverride string, channel, chatID string, ) (string, int, error) { cfg := sm.effectiveConfig(ctx) @@ -160,6 +160,7 @@ func (sm *SubagentManager) RunSync( Label: label, Status: "running", Depth: depth + 1, + Model: modelOverride, OriginChannel: channel, OriginChatID: chatID, OriginLocalKey: ToolLocalKeyFromCtx(ctx), diff --git a/internal/tools/subagent_spawn_test.go b/internal/tools/subagent_spawn_test.go new file mode 100644 index 00000000..5df6cffe --- /dev/null +++ b/internal/tools/subagent_spawn_test.go @@ -0,0 +1,48 @@ +package tools + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/nextlevelbuilder/goclaw/internal/providers" + "github.com/nextlevelbuilder/goclaw/internal/store" +) + +type recordingSubagentProvider struct { + model string +} + +func (p *recordingSubagentProvider) Name() string { return "recording" } +func (p *recordingSubagentProvider) DefaultModel() string { return "provider-default" } +func (p *recordingSubagentProvider) Chat(_ context.Context, req providers.ChatRequest) (*providers.ChatResponse, error) { + p.model = req.Model + return &providers.ChatResponse{Content: "done", FinishReason: "stop"}, nil +} +func (p *recordingSubagentProvider) ChatStream(ctx context.Context, req providers.ChatRequest, _ func(providers.StreamChunk)) (*providers.ChatResponse, error) { + return p.Chat(ctx, req) +} + +func TestRunSyncHonorsPerTaskModelOverride(t *testing.T) { + provider := &recordingSubagentProvider{} + manager := NewSubagentManager(provider, nil, "manager-default", nil, NewRegistry, SubagentConfig{ + MaxConcurrent: 4, + MaxSpawnDepth: 3, + MaxChildrenPerAgent: 8, + Model: "configured-model", + }) + + ctx := store.WithTenantID(context.Background(), uuid.New()) + ctx = WithParentModel(ctx, "parent-model") + + result, _, err := manager.RunSync(ctx, "parent", 0, "test task", "test", "requested-model", "test", "chat") + if err != nil { + t.Fatalf("RunSync() error = %v", err) + } + if result != "done" { + t.Fatalf("RunSync() result = %q, want %q", result, "done") + } + if provider.model != "requested-model" { + t.Fatalf("provider model = %q, want per-task override %q", provider.model, "requested-model") + } +} diff --git a/internal/tools/subagent_spawn_tool.go b/internal/tools/subagent_spawn_tool.go index c2bac9db..48f57332 100644 --- a/internal/tools/subagent_spawn_tool.go +++ b/internal/tools/subagent_spawn_tool.go @@ -153,6 +153,7 @@ After all spawn tool calls in this turn are complete, briefly tell the user what // executeSubagentSync runs a sync self-clone. func (t *SpawnTool) executeSubagentSync(ctx context.Context, args map[string]any, task string) *Result { label, _ := args["label"].(string) + modelOverride, _ := args["model"].(string) if label == "" { label = truncate(task, 50) } @@ -165,7 +166,7 @@ func (t *SpawnTool) executeSubagentSync(ctx context.Context, args map[string]any parentID = t.parentID } - result, iterations, err := t.subagentMgr.RunSync(ctx, parentID, t.depth, task, label, + result, iterations, err := t.subagentMgr.RunSync(ctx, parentID, t.depth, task, label, modelOverride, channel, chatID) if err != nil { return ErrorResult(fmt.Sprintf("Subagent '%s' failed: %v", label, err))