feat: add plan_model config for smarter LLM during interview/review phases
Allow users to specify a separate model for plan phases (interviewing/review) via plan_model and plan_model_fallbacks in defaults or per-agent config. During execution phases, the normal model is used as before. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2be7c34f71
commit
2759bee2e3
7 changed files with 435 additions and 3 deletions
|
|
@ -6,6 +6,8 @@
|
||||||
"provider": "zhipu",
|
"provider": "zhipu",
|
||||||
"model": "glm-4.7",
|
"model": "glm-4.7",
|
||||||
"model_fallbacks": ["openai/gpt-4o"],
|
"model_fallbacks": ["openai/gpt-4o"],
|
||||||
|
"plan_model": "",
|
||||||
|
"plan_model_fallbacks": [],
|
||||||
"max_tokens": 8192,
|
"max_tokens": 8192,
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
"max_tool_iterations": 20,
|
"max_tool_iterations": 20,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ type AgentInstance struct {
|
||||||
Subagents *config.SubagentsConfig
|
Subagents *config.SubagentsConfig
|
||||||
SkillsFilter []string
|
SkillsFilter []string
|
||||||
Candidates []providers.FallbackCandidate
|
Candidates []providers.FallbackCandidate
|
||||||
|
PlanModel string
|
||||||
|
PlanFallbacks []string
|
||||||
|
PlanCandidates []providers.FallbackCandidate
|
||||||
|
|
||||||
// Interview staleness tracking: consecutive turns where MEMORY.md was not updated.
|
// Interview staleness tracking: consecutive turns where MEMORY.md was not updated.
|
||||||
interviewStaleCount int
|
interviewStaleCount int
|
||||||
|
|
@ -108,6 +111,18 @@ func NewAgentInstance(
|
||||||
}
|
}
|
||||||
candidates := providers.ResolveCandidates(modelCfg, defaults.Provider)
|
candidates := providers.ResolveCandidates(modelCfg, defaults.Provider)
|
||||||
|
|
||||||
|
// Resolve plan model (for interviewing/review phases)
|
||||||
|
planModel := resolvePlanModel(agentCfg, defaults)
|
||||||
|
planFallbacks := resolvePlanFallbacks(agentCfg, defaults)
|
||||||
|
var planCandidates []providers.FallbackCandidate
|
||||||
|
if planModel != "" {
|
||||||
|
planModelCfg := providers.ModelConfig{
|
||||||
|
Primary: planModel,
|
||||||
|
Fallbacks: planFallbacks,
|
||||||
|
}
|
||||||
|
planCandidates = providers.ResolveCandidates(planModelCfg, defaults.Provider)
|
||||||
|
}
|
||||||
|
|
||||||
return &AgentInstance{
|
return &AgentInstance{
|
||||||
ID: agentID,
|
ID: agentID,
|
||||||
Name: agentName,
|
Name: agentName,
|
||||||
|
|
@ -126,6 +141,9 @@ func NewAgentInstance(
|
||||||
Subagents: subagents,
|
Subagents: subagents,
|
||||||
SkillsFilter: skillsFilter,
|
SkillsFilter: skillsFilter,
|
||||||
Candidates: candidates,
|
Candidates: candidates,
|
||||||
|
PlanModel: planModel,
|
||||||
|
PlanFallbacks: planFallbacks,
|
||||||
|
PlanCandidates: planCandidates,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,6 +176,22 @@ func resolveAgentFallbacks(agentCfg *config.AgentConfig, defaults *config.AgentD
|
||||||
return defaults.ModelFallbacks
|
return defaults.ModelFallbacks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolvePlanModel resolves the plan model for an agent (used during interviewing/review phases).
|
||||||
|
func resolvePlanModel(agentCfg *config.AgentConfig, defaults *config.AgentDefaults) string {
|
||||||
|
if agentCfg != nil && agentCfg.PlanModel != nil && strings.TrimSpace(agentCfg.PlanModel.Primary) != "" {
|
||||||
|
return strings.TrimSpace(agentCfg.PlanModel.Primary)
|
||||||
|
}
|
||||||
|
return defaults.PlanModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolvePlanFallbacks resolves the plan model fallbacks for an agent.
|
||||||
|
func resolvePlanFallbacks(agentCfg *config.AgentConfig, defaults *config.AgentDefaults) []string {
|
||||||
|
if agentCfg != nil && agentCfg.PlanModel != nil && agentCfg.PlanModel.Fallbacks != nil {
|
||||||
|
return agentCfg.PlanModel.Fallbacks
|
||||||
|
}
|
||||||
|
return defaults.PlanModelFallbacks
|
||||||
|
}
|
||||||
|
|
||||||
func expandHome(path string) string {
|
func expandHome(path string) string {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return path
|
return path
|
||||||
|
|
|
||||||
|
|
@ -93,3 +93,110 @@ func TestNewAgentInstance_DefaultsTemperatureWhenUnset(t *testing.T) {
|
||||||
t.Fatalf("Temperature = %f, want %f", agent.Temperature, 0.7)
|
t.Fatalf("Temperature = %f, want %f", agent.Temperature, 0.7)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewAgentInstance_PlanModel_SetFromDefaults(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "agent-instance-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: tmpDir,
|
||||||
|
Provider: "zhipu",
|
||||||
|
Model: "glm-4.7",
|
||||||
|
PlanModel: "anthropic/claude-sonnet-4-6",
|
||||||
|
PlanModelFallbacks: []string{"openai/gpt-4o"},
|
||||||
|
MaxTokens: 8192,
|
||||||
|
MaxToolIterations: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := &mockProvider{}
|
||||||
|
agent := NewAgentInstance(nil, &cfg.Agents.Defaults, cfg, provider)
|
||||||
|
|
||||||
|
if agent.PlanModel != "anthropic/claude-sonnet-4-6" {
|
||||||
|
t.Errorf("PlanModel = %q, want 'anthropic/claude-sonnet-4-6'", agent.PlanModel)
|
||||||
|
}
|
||||||
|
if len(agent.PlanFallbacks) != 1 || agent.PlanFallbacks[0] != "openai/gpt-4o" {
|
||||||
|
t.Errorf("PlanFallbacks = %v, want [openai/gpt-4o]", agent.PlanFallbacks)
|
||||||
|
}
|
||||||
|
if len(agent.PlanCandidates) == 0 {
|
||||||
|
t.Fatal("PlanCandidates should not be empty when plan_model is set")
|
||||||
|
}
|
||||||
|
if agent.PlanCandidates[0].Model != "claude-sonnet-4-6" {
|
||||||
|
t.Errorf("PlanCandidates[0].Model = %q, want 'claude-sonnet-4-6'", agent.PlanCandidates[0].Model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAgentInstance_PlanModel_NilWhenUnset(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "agent-instance-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: tmpDir,
|
||||||
|
Model: "test-model",
|
||||||
|
MaxTokens: 1234,
|
||||||
|
MaxToolIterations: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := &mockProvider{}
|
||||||
|
agent := NewAgentInstance(nil, &cfg.Agents.Defaults, cfg, provider)
|
||||||
|
|
||||||
|
if agent.PlanModel != "" {
|
||||||
|
t.Errorf("PlanModel = %q, want empty", agent.PlanModel)
|
||||||
|
}
|
||||||
|
if agent.PlanCandidates != nil {
|
||||||
|
t.Errorf("PlanCandidates = %v, want nil", agent.PlanCandidates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAgentInstance_PlanModel_AgentOverridesDefaults(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "agent-instance-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: tmpDir,
|
||||||
|
Provider: "zhipu",
|
||||||
|
Model: "glm-4.7",
|
||||||
|
PlanModel: "default-plan-model",
|
||||||
|
PlanModelFallbacks: []string{"default-fallback"},
|
||||||
|
MaxTokens: 8192,
|
||||||
|
MaxToolIterations: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
agentCfg := &config.AgentConfig{
|
||||||
|
ID: "custom",
|
||||||
|
PlanModel: &config.AgentModelConfig{
|
||||||
|
Primary: "agent-plan-model",
|
||||||
|
Fallbacks: []string{"agent-fallback"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := &mockProvider{}
|
||||||
|
agent := NewAgentInstance(agentCfg, &cfg.Agents.Defaults, cfg, provider)
|
||||||
|
|
||||||
|
if agent.PlanModel != "agent-plan-model" {
|
||||||
|
t.Errorf("PlanModel = %q, want 'agent-plan-model'", agent.PlanModel)
|
||||||
|
}
|
||||||
|
if len(agent.PlanFallbacks) != 1 || agent.PlanFallbacks[0] != "agent-fallback" {
|
||||||
|
t.Errorf("PlanFallbacks = %v, want [agent-fallback]", agent.PlanFallbacks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1623,8 +1623,18 @@ func (al *AgentLoop) runLLMIteration(
|
||||||
}
|
}
|
||||||
|
|
||||||
callLLM := func() (*providers.LLMResponse, error) {
|
callLLM := func() (*providers.LLMResponse, error) {
|
||||||
if len(agent.Candidates) > 1 && al.fallback != nil {
|
// Plan model switching: use plan model during interviewing/review phases
|
||||||
fbResult, fbErr := al.fallback.Execute(ctx, agent.Candidates,
|
candidates := agent.Candidates
|
||||||
|
primaryModel := agent.Model
|
||||||
|
if isPlanPreExecution(planSnapshot) && agent.PlanModel != "" {
|
||||||
|
candidates = agent.PlanCandidates
|
||||||
|
primaryModel = agent.PlanModel
|
||||||
|
logger.InfoCF("agent", "Using plan model",
|
||||||
|
map[string]any{"agent_id": agent.ID, "plan_model": agent.PlanModel})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(candidates) > 1 && al.fallback != nil {
|
||||||
|
fbResult, fbErr := al.fallback.Execute(ctx, candidates,
|
||||||
func(ctx context.Context, provider, model string) (*providers.LLMResponse, error) {
|
func(ctx context.Context, provider, model string) (*providers.LLMResponse, error) {
|
||||||
p := al.resolveProvider(provider, model, agent.Provider)
|
p := al.resolveProvider(provider, model, agent.Provider)
|
||||||
return doCall(ctx, p, model)
|
return doCall(ctx, p, model)
|
||||||
|
|
@ -1640,7 +1650,7 @@ func (al *AgentLoop) runLLMIteration(
|
||||||
}
|
}
|
||||||
return fbResult.Response, nil
|
return fbResult.Response, nil
|
||||||
}
|
}
|
||||||
return doCall(ctx, agent.Provider, agent.Model)
|
return doCall(ctx, agent.Provider, primaryModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry loop for context/token errors
|
// Retry loop for context/token errors
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -2348,3 +2349,158 @@ func TestConsumeStream_OnChunkWithRepetitionDetection(t *testing.T) {
|
||||||
}
|
}
|
||||||
_ = ctx
|
_ = ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// modelCapturingMockProvider records which model was passed to Chat.
|
||||||
|
type modelCapturingMockProvider struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
models []string
|
||||||
|
response string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *modelCapturingMockProvider) Chat(
|
||||||
|
ctx context.Context,
|
||||||
|
messages []providers.Message,
|
||||||
|
tools_ []providers.ToolDefinition,
|
||||||
|
model string,
|
||||||
|
opts map[string]any,
|
||||||
|
) (*providers.LLMResponse, error) {
|
||||||
|
m.mu.Lock()
|
||||||
|
m.models = append(m.models, model)
|
||||||
|
m.mu.Unlock()
|
||||||
|
return &providers.LLMResponse{
|
||||||
|
Content: m.response,
|
||||||
|
ToolCalls: []providers.ToolCall{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *modelCapturingMockProvider) GetDefaultModel() string {
|
||||||
|
return "mock-capture-model"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgentLoop_PlanModel_UsedDuringInterviewing(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "agent-test-planmodel-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: tmpDir,
|
||||||
|
Model: "normal-model",
|
||||||
|
PlanModel: "plan-model",
|
||||||
|
MaxTokens: 4096,
|
||||||
|
MaxToolIterations: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBus := bus.NewMessageBus()
|
||||||
|
provider := &modelCapturingMockProvider{response: "Plan interview response"}
|
||||||
|
al := NewAgentLoop(cfg, msgBus, provider)
|
||||||
|
|
||||||
|
defaultAgent := al.registry.GetDefaultAgent()
|
||||||
|
if defaultAgent == nil {
|
||||||
|
t.Fatal("No default agent found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write MEMORY.md with interviewing status to activate plan model
|
||||||
|
memoryDir := filepath.Join(tmpDir, "memory")
|
||||||
|
os.MkdirAll(memoryDir, 0o755)
|
||||||
|
memoryPath := filepath.Join(memoryDir, "MEMORY.md")
|
||||||
|
memoryContent := "# Active Plan\n\n> Task: Test plan model\n> Status: interviewing\n> Phase: 1\n"
|
||||||
|
if err := os.WriteFile(memoryPath, []byte(memoryContent), 0o644); err != nil {
|
||||||
|
t.Fatalf("Failed to write MEMORY.md: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = al.ProcessDirectWithChannel(
|
||||||
|
context.Background(),
|
||||||
|
"Hello, plan model test",
|
||||||
|
"test-plan-session",
|
||||||
|
"test",
|
||||||
|
"test-chat",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessDirectWithChannel failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.mu.Lock()
|
||||||
|
defer provider.mu.Unlock()
|
||||||
|
|
||||||
|
if len(provider.models) == 0 {
|
||||||
|
t.Fatal("Expected at least one Chat call")
|
||||||
|
}
|
||||||
|
// The first call should use the plan model since we're in interviewing state
|
||||||
|
if provider.models[0] != "plan-model" {
|
||||||
|
t.Errorf("Expected plan model 'plan-model' during interviewing, got %q", provider.models[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgentLoop_PlanModel_NotUsedDuringExecuting(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "agent-test-planmodel-exec-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: tmpDir,
|
||||||
|
Model: "normal-model",
|
||||||
|
PlanModel: "plan-model",
|
||||||
|
MaxTokens: 4096,
|
||||||
|
MaxToolIterations: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBus := bus.NewMessageBus()
|
||||||
|
provider := &modelCapturingMockProvider{response: "Executing response"}
|
||||||
|
al := NewAgentLoop(cfg, msgBus, provider)
|
||||||
|
|
||||||
|
defaultAgent := al.registry.GetDefaultAgent()
|
||||||
|
if defaultAgent == nil {
|
||||||
|
t.Fatal("No default agent found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write MEMORY.md with executing status - should use normal model
|
||||||
|
memoryDir := filepath.Join(tmpDir, "memory")
|
||||||
|
os.MkdirAll(memoryDir, 0o755)
|
||||||
|
memoryPath := filepath.Join(memoryDir, "MEMORY.md")
|
||||||
|
memoryContent := `# Active Plan
|
||||||
|
|
||||||
|
> Task: Test plan model
|
||||||
|
> Status: executing
|
||||||
|
> Phase: 1
|
||||||
|
|
||||||
|
## Phase 1: Build
|
||||||
|
- [ ] Run build
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(memoryPath, []byte(memoryContent), 0o644); err != nil {
|
||||||
|
t.Fatalf("Failed to write MEMORY.md: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = al.ProcessDirectWithChannel(
|
||||||
|
context.Background(),
|
||||||
|
"Hello, executing test",
|
||||||
|
"test-exec-session",
|
||||||
|
"test",
|
||||||
|
"test-chat",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessDirectWithChannel failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.mu.Lock()
|
||||||
|
defer provider.mu.Unlock()
|
||||||
|
|
||||||
|
if len(provider.models) == 0 {
|
||||||
|
t.Fatal("Expected at least one Chat call")
|
||||||
|
}
|
||||||
|
// During executing phase, should use normal model, not plan model
|
||||||
|
if provider.models[0] != "normal-model" {
|
||||||
|
t.Errorf("Expected normal model 'normal-model' during executing, got %q", provider.models[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,7 @@ type AgentConfig struct {
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Workspace string `json:"workspace,omitempty"`
|
Workspace string `json:"workspace,omitempty"`
|
||||||
Model *AgentModelConfig `json:"model,omitempty"`
|
Model *AgentModelConfig `json:"model,omitempty"`
|
||||||
|
PlanModel *AgentModelConfig `json:"plan_model,omitempty"`
|
||||||
Skills []string `json:"skills,omitempty"`
|
Skills []string `json:"skills,omitempty"`
|
||||||
Subagents *SubagentsConfig `json:"subagents,omitempty"`
|
Subagents *SubagentsConfig `json:"subagents,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
@ -175,6 +176,8 @@ type AgentDefaults struct {
|
||||||
ModelFallbacks []string `json:"model_fallbacks,omitempty"`
|
ModelFallbacks []string `json:"model_fallbacks,omitempty"`
|
||||||
ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"`
|
ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"`
|
||||||
ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"`
|
ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"`
|
||||||
|
PlanModel string `json:"plan_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_PLAN_MODEL"`
|
||||||
|
PlanModelFallbacks []string `json:"plan_model_fallbacks,omitempty"`
|
||||||
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
|
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
|
||||||
Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
|
Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
|
||||||
MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
|
MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
|
||||||
|
|
|
||||||
|
|
@ -392,3 +392,123 @@ func TestLoadConfig_OpenAIWebSearchCanBeDisabled(t *testing.T) {
|
||||||
t.Fatal("OpenAI codex web search should be false when disabled in config file")
|
t.Fatal("OpenAI codex web search should be false when disabled in config file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAgentDefaults_PlanModel_StringParse(t *testing.T) {
|
||||||
|
jsonData := `{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"workspace": "~/.picoclaw/workspace",
|
||||||
|
"model": "glm-4.7",
|
||||||
|
"plan_model": "anthropic/claude-sonnet-4-6",
|
||||||
|
"plan_model_fallbacks": ["openai/gpt-4o"],
|
||||||
|
"max_tokens": 8192,
|
||||||
|
"max_tool_iterations": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
if err := json.Unmarshal([]byte(jsonData), cfg); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Agents.Defaults.PlanModel != "anthropic/claude-sonnet-4-6" {
|
||||||
|
t.Errorf("PlanModel = %q, want 'anthropic/claude-sonnet-4-6'", cfg.Agents.Defaults.PlanModel)
|
||||||
|
}
|
||||||
|
if len(cfg.Agents.Defaults.PlanModelFallbacks) != 1 || cfg.Agents.Defaults.PlanModelFallbacks[0] != "openai/gpt-4o" {
|
||||||
|
t.Errorf("PlanModelFallbacks = %v, want [openai/gpt-4o]", cfg.Agents.Defaults.PlanModelFallbacks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgentConfig_PlanModel_ObjectParse(t *testing.T) {
|
||||||
|
jsonData := `{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"workspace": "~/.picoclaw/workspace",
|
||||||
|
"model": "glm-4.7",
|
||||||
|
"max_tokens": 8192,
|
||||||
|
"max_tool_iterations": 20
|
||||||
|
},
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": "main",
|
||||||
|
"plan_model": "anthropic/claude-sonnet-4-6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "advanced",
|
||||||
|
"plan_model": {
|
||||||
|
"primary": "anthropic/claude-opus-4",
|
||||||
|
"fallbacks": ["anthropic/claude-sonnet-4-6"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
if err := json.Unmarshal([]byte(jsonData), cfg); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.Agents.List) != 2 {
|
||||||
|
t.Fatalf("agents.list len = %d, want 2", len(cfg.Agents.List))
|
||||||
|
}
|
||||||
|
|
||||||
|
// String form
|
||||||
|
main := cfg.Agents.List[0]
|
||||||
|
if main.PlanModel == nil || main.PlanModel.Primary != "anthropic/claude-sonnet-4-6" {
|
||||||
|
t.Errorf("main.PlanModel = %+v, want primary 'anthropic/claude-sonnet-4-6'", main.PlanModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object form with fallbacks
|
||||||
|
adv := cfg.Agents.List[1]
|
||||||
|
if adv.PlanModel == nil || adv.PlanModel.Primary != "anthropic/claude-opus-4" {
|
||||||
|
t.Errorf("advanced.PlanModel = %+v, want primary 'anthropic/claude-opus-4'", adv.PlanModel)
|
||||||
|
}
|
||||||
|
if len(adv.PlanModel.Fallbacks) != 1 || adv.PlanModel.Fallbacks[0] != "anthropic/claude-sonnet-4-6" {
|
||||||
|
t.Errorf("advanced.PlanModel.Fallbacks = %v", adv.PlanModel.Fallbacks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgentConfig_PlanModel_OverridesDefaults(t *testing.T) {
|
||||||
|
jsonData := `{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"workspace": "~/.picoclaw/workspace",
|
||||||
|
"model": "glm-4.7",
|
||||||
|
"plan_model": "default-plan-model",
|
||||||
|
"plan_model_fallbacks": ["default-fallback"],
|
||||||
|
"max_tokens": 8192,
|
||||||
|
"max_tool_iterations": 20
|
||||||
|
},
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": "custom",
|
||||||
|
"plan_model": {
|
||||||
|
"primary": "custom-plan-model",
|
||||||
|
"fallbacks": ["custom-fallback"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
if err := json.Unmarshal([]byte(jsonData), cfg); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agent-level plan_model should override defaults
|
||||||
|
custom := cfg.Agents.List[0]
|
||||||
|
if custom.PlanModel == nil || custom.PlanModel.Primary != "custom-plan-model" {
|
||||||
|
t.Errorf("custom.PlanModel.Primary = %v, want 'custom-plan-model'", custom.PlanModel)
|
||||||
|
}
|
||||||
|
if len(custom.PlanModel.Fallbacks) != 1 || custom.PlanModel.Fallbacks[0] != "custom-fallback" {
|
||||||
|
t.Errorf("custom.PlanModel.Fallbacks = %v, want [custom-fallback]", custom.PlanModel.Fallbacks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults should still be intact
|
||||||
|
if cfg.Agents.Defaults.PlanModel != "default-plan-model" {
|
||||||
|
t.Errorf("defaults.PlanModel = %q, want 'default-plan-model'", cfg.Agents.Defaults.PlanModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue