From b8b06257ff97d0e284957d79a8cf7bc0f9e5637c Mon Sep 17 00:00:00 2001 From: cexll Date: Tue, 13 Jan 2026 22:26:58 +0800 Subject: [PATCH] feat(codeagent-wrapper): add reasoning effort config for codex backend - Add --reasoning-effort CLI flag for codex model thinking intensity - Support reasoning config in ~/.codeagent/models.json per agent - CLI flag takes precedence over config file - Only effective for codex backend Closes #117 Generated with SWE-Agent.ai Co-Authored-By: SWE-Agent.ai --- codeagent-wrapper/agent_config.go | 25 ++--- codeagent-wrapper/agent_config_test.go | 18 ++-- codeagent-wrapper/config.go | 64 +++++++++--- codeagent-wrapper/executor.go | 26 +++-- codeagent-wrapper/main.go | 13 +-- codeagent-wrapper/main_test.go | 139 +++++++++++++++++++++++++ 6 files changed, 236 insertions(+), 49 deletions(-) diff --git a/codeagent-wrapper/agent_config.go b/codeagent-wrapper/agent_config.go index 3981581..b5d8cfb 100644 --- a/codeagent-wrapper/agent_config.go +++ b/codeagent-wrapper/agent_config.go @@ -13,6 +13,7 @@ type AgentModelConfig struct { PromptFile string `json:"prompt_file,omitempty"` Description string `json:"description,omitempty"` Yolo bool `json:"yolo,omitempty"` + Reasoning string `json:"reasoning,omitempty"` } type ModelsConfig struct { @@ -25,15 +26,15 @@ var defaultModelsConfig = ModelsConfig{ DefaultBackend: "opencode", DefaultModel: "opencode/grok-code", Agents: map[string]AgentModelConfig{ - "sisyphus": {Backend: "claude", Model: "claude-sonnet-4-20250514", PromptFile: "~/.claude/skills/omo/references/sisyphus.md", Description: "Primary orchestrator"}, - "oracle": {Backend: "claude", Model: "claude-sonnet-4-20250514", PromptFile: "~/.claude/skills/omo/references/oracle.md", Description: "Technical advisor"}, - "librarian": {Backend: "claude", Model: "claude-sonnet-4-5-20250514", PromptFile: "~/.claude/skills/omo/references/librarian.md", Description: "Researcher"}, - "explore": {Backend: "opencode", Model: "opencode/grok-code", PromptFile: "~/.claude/skills/omo/references/explore.md", Description: "Code search"}, - "develop": {Backend: "codex", Model: "", PromptFile: "~/.claude/skills/omo/references/develop.md", Description: "Code development"}, - "frontend-ui-ux-engineer": {Backend: "gemini", Model: "gemini-3-pro-preview", PromptFile: "~/.claude/skills/omo/references/frontend-ui-ux-engineer.md", Description: "Frontend engineer"}, - "document-writer": {Backend: "gemini", Model: "gemini-3-flash-preview", PromptFile: "~/.claude/skills/omo/references/document-writer.md", Description: "Documentation"}, - }, -} + "sisyphus": {Backend: "claude", Model: "claude-sonnet-4-20250514", PromptFile: "~/.claude/skills/omo/references/sisyphus.md", Description: "Primary orchestrator"}, + "oracle": {Backend: "claude", Model: "claude-sonnet-4-20250514", PromptFile: "~/.claude/skills/omo/references/oracle.md", Description: "Technical advisor"}, + "librarian": {Backend: "claude", Model: "claude-sonnet-4-5-20250514", PromptFile: "~/.claude/skills/omo/references/librarian.md", Description: "Researcher"}, + "explore": {Backend: "opencode", Model: "opencode/grok-code", PromptFile: "~/.claude/skills/omo/references/explore.md", Description: "Code search"}, + "develop": {Backend: "codex", Model: "", PromptFile: "~/.claude/skills/omo/references/develop.md", Description: "Code development"}, + "frontend-ui-ux-engineer": {Backend: "gemini", Model: "", PromptFile: "~/.claude/skills/omo/references/frontend-ui-ux-engineer.md", Description: "Frontend engineer"}, + "document-writer": {Backend: "gemini", Model: "", PromptFile: "~/.claude/skills/omo/references/document-writer.md", Description: "Documentation"}, + }, + } func loadModelsConfig() *ModelsConfig { home, err := os.UserHomeDir() @@ -70,10 +71,10 @@ func loadModelsConfig() *ModelsConfig { return &cfg } -func resolveAgentConfig(agentName string) (backend, model, promptFile string, yolo bool) { +func resolveAgentConfig(agentName string) (backend, model, promptFile, reasoning string, yolo bool) { cfg := loadModelsConfig() if agent, ok := cfg.Agents[agentName]; ok { - return agent.Backend, agent.Model, agent.PromptFile, agent.Yolo + return agent.Backend, agent.Model, agent.PromptFile, agent.Reasoning, agent.Yolo } - return cfg.DefaultBackend, cfg.DefaultModel, "", false + return cfg.DefaultBackend, cfg.DefaultModel, "", "", false } diff --git a/codeagent-wrapper/agent_config_test.go b/codeagent-wrapper/agent_config_test.go index 14ef65c..0060571 100644 --- a/codeagent-wrapper/agent_config_test.go +++ b/codeagent-wrapper/agent_config_test.go @@ -19,17 +19,17 @@ func TestResolveAgentConfig_Defaults(t *testing.T) { wantModel string wantPromptFile string }{ - {"sisyphus", "claude", "claude-sonnet-4-20250514", "~/.claude/skills/omo/references/sisyphus.md"}, - {"oracle", "claude", "claude-sonnet-4-20250514", "~/.claude/skills/omo/references/oracle.md"}, - {"librarian", "claude", "claude-sonnet-4-5-20250514", "~/.claude/skills/omo/references/librarian.md"}, - {"explore", "opencode", "opencode/grok-code", "~/.claude/skills/omo/references/explore.md"}, - {"frontend-ui-ux-engineer", "gemini", "gemini-3-pro-preview", "~/.claude/skills/omo/references/frontend-ui-ux-engineer.md"}, - {"document-writer", "gemini", "gemini-3-flash-preview", "~/.claude/skills/omo/references/document-writer.md"}, - } + {"sisyphus", "claude", "claude-sonnet-4-20250514", "~/.claude/skills/omo/references/sisyphus.md"}, + {"oracle", "claude", "claude-sonnet-4-20250514", "~/.claude/skills/omo/references/oracle.md"}, + {"librarian", "claude", "claude-sonnet-4-5-20250514", "~/.claude/skills/omo/references/librarian.md"}, + {"explore", "opencode", "opencode/grok-code", "~/.claude/skills/omo/references/explore.md"}, + {"frontend-ui-ux-engineer", "gemini", "", "~/.claude/skills/omo/references/frontend-ui-ux-engineer.md"}, + {"document-writer", "gemini", "", "~/.claude/skills/omo/references/document-writer.md"}, + } for _, tt := range tests { t.Run(tt.agent, func(t *testing.T) { - backend, model, promptFile, _ := resolveAgentConfig(tt.agent) + backend, model, promptFile, _, _ := resolveAgentConfig(tt.agent) if backend != tt.wantBackend { t.Errorf("backend = %q, want %q", backend, tt.wantBackend) } @@ -48,7 +48,7 @@ func TestResolveAgentConfig_UnknownAgent(t *testing.T) { t.Setenv("HOME", home) t.Setenv("USERPROFILE", home) - backend, model, promptFile, _ := resolveAgentConfig("unknown-agent") + backend, model, promptFile, _, _ := resolveAgentConfig("unknown-agent") if backend != "opencode" { t.Errorf("unknown agent backend = %q, want %q", backend, "opencode") } diff --git a/codeagent-wrapper/config.go b/codeagent-wrapper/config.go index c34d29a..6cab9b6 100644 --- a/codeagent-wrapper/config.go +++ b/codeagent-wrapper/config.go @@ -16,6 +16,7 @@ type Config struct { SessionID string WorkDir string Model string + ReasoningEffort string ExplicitStdin bool Timeout int Backend string @@ -35,18 +36,19 @@ type ParallelConfig struct { // TaskSpec describes an individual task entry in the parallel config type TaskSpec struct { - ID string `json:"id"` - Task string `json:"task"` - WorkDir string `json:"workdir,omitempty"` - Dependencies []string `json:"dependencies,omitempty"` - SessionID string `json:"session_id,omitempty"` - Backend string `json:"backend,omitempty"` - Model string `json:"model,omitempty"` - Agent string `json:"agent,omitempty"` - PromptFile string `json:"prompt_file,omitempty"` - Mode string `json:"-"` - UseStdin bool `json:"-"` - Context context.Context `json:"-"` + ID string `json:"id"` + Task string `json:"task"` + WorkDir string `json:"workdir,omitempty"` + Dependencies []string `json:"dependencies,omitempty"` + SessionID string `json:"session_id,omitempty"` + Backend string `json:"backend,omitempty"` + Model string `json:"model,omitempty"` + ReasoningEffort string `json:"reasoning_effort,omitempty"` + Agent string `json:"agent,omitempty"` + PromptFile string `json:"prompt_file,omitempty"` + Mode string `json:"-"` + UseStdin bool `json:"-"` + Context context.Context `json:"-"` } // TaskResult captures the execution outcome of a task @@ -190,6 +192,8 @@ func parseParallelConfig(data []byte) (*ParallelConfig, error) { task.Backend = value case "model": task.Model = value + case "reasoning_effort": + task.ReasoningEffort = value case "agent": agentSpecified = true task.Agent = value @@ -214,13 +218,16 @@ func parseParallelConfig(data []byte) (*ParallelConfig, error) { if err := validateAgentName(task.Agent); err != nil { return nil, fmt.Errorf("task block #%d invalid agent name: %w", taskIndex, err) } - backend, model, promptFile, _ := resolveAgentConfig(task.Agent) + backend, model, promptFile, reasoning, _ := resolveAgentConfig(task.Agent) if task.Backend == "" { task.Backend = backend } if task.Model == "" { task.Model = model } + if task.ReasoningEffort == "" { + task.ReasoningEffort = reasoning + } task.PromptFile = promptFile } @@ -257,6 +264,7 @@ func parseArgs() (*Config, error) { backendName := defaultBackendName model := "" + reasoningEffort := "" agentName := "" promptFile := "" promptFileExplicit := false @@ -277,12 +285,15 @@ func parseArgs() (*Config, error) { if err := validateAgentName(value); err != nil { return nil, fmt.Errorf("--agent flag invalid value: %w", err) } - resolvedBackend, resolvedModel, resolvedPromptFile, resolvedYolo := resolveAgentConfig(value) + resolvedBackend, resolvedModel, resolvedPromptFile, resolvedReasoning, resolvedYolo := resolveAgentConfig(value) backendName = resolvedBackend model = resolvedModel if !promptFileExplicit { promptFile = resolvedPromptFile } + if reasoningEffort == "" { + reasoningEffort = resolvedReasoning + } yolo = resolvedYolo agentName = value i++ @@ -295,12 +306,15 @@ func parseArgs() (*Config, error) { if err := validateAgentName(value); err != nil { return nil, fmt.Errorf("--agent flag invalid value: %w", err) } - resolvedBackend, resolvedModel, resolvedPromptFile, resolvedYolo := resolveAgentConfig(value) + resolvedBackend, resolvedModel, resolvedPromptFile, resolvedReasoning, resolvedYolo := resolveAgentConfig(value) backendName = resolvedBackend model = resolvedModel if !promptFileExplicit { promptFile = resolvedPromptFile } + if reasoningEffort == "" { + reasoningEffort = resolvedReasoning + } yolo = resolvedYolo agentName = value continue @@ -355,6 +369,24 @@ func parseArgs() (*Config, error) { } model = value continue + case arg == "--reasoning-effort": + if i+1 >= len(args) { + return nil, fmt.Errorf("--reasoning-effort flag requires a value") + } + value := strings.TrimSpace(args[i+1]) + if value == "" { + return nil, fmt.Errorf("--reasoning-effort flag requires a value") + } + reasoningEffort = value + i++ + continue + case strings.HasPrefix(arg, "--reasoning-effort="): + value := strings.TrimSpace(strings.TrimPrefix(arg, "--reasoning-effort=")) + if value == "" { + return nil, fmt.Errorf("--reasoning-effort flag requires a value") + } + reasoningEffort = value + continue case strings.HasPrefix(arg, "--skip-permissions="): skipPermissions = parseBoolFlag(strings.TrimPrefix(arg, "--skip-permissions="), skipPermissions) continue @@ -370,7 +402,7 @@ func parseArgs() (*Config, error) { } args = filtered - cfg := &Config{WorkDir: defaultWorkdir, Backend: backendName, Agent: agentName, PromptFile: promptFile, PromptFileExplicit: promptFileExplicit, SkipPermissions: skipPermissions, Yolo: yolo, Model: strings.TrimSpace(model)} + cfg := &Config{WorkDir: defaultWorkdir, Backend: backendName, Agent: agentName, PromptFile: promptFile, PromptFileExplicit: promptFileExplicit, SkipPermissions: skipPermissions, Yolo: yolo, Model: strings.TrimSpace(model), ReasoningEffort: strings.TrimSpace(reasoningEffort)} cfg.MaxParallelWorkers = resolveMaxParallelWorkers() if args[0] == "resume" { diff --git a/codeagent-wrapper/executor.go b/codeagent-wrapper/executor.go index 7cf751b..1cd3860 100644 --- a/codeagent-wrapper/executor.go +++ b/codeagent-wrapper/executor.go @@ -764,6 +764,10 @@ func buildCodexArgs(cfg *Config, targetArg string) []string { args = append(args, "--model", model) } + if reasoningEffort := strings.TrimSpace(cfg.ReasoningEffort); reasoningEffort != "" { + args = append(args, "--reasoning-effort", reasoningEffort) + } + args = append(args, "--skip-git-repo-check") if isResume { @@ -804,12 +808,13 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe logger := injectedLogger cfg := &Config{ - Mode: taskSpec.Mode, - Task: taskSpec.Task, - SessionID: taskSpec.SessionID, - WorkDir: taskSpec.WorkDir, - Model: taskSpec.Model, - Backend: defaultBackendName, + Mode: taskSpec.Mode, + Task: taskSpec.Task, + SessionID: taskSpec.SessionID, + WorkDir: taskSpec.WorkDir, + Model: taskSpec.Model, + ReasoningEffort: taskSpec.ReasoningEffort, + Backend: defaultBackendName, } commandName := codexCommand @@ -846,6 +851,12 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe } } + // Load gemini env from ~/.gemini/.env if exists + var geminiEnv map[string]string + if cfg.Backend == "gemini" { + geminiEnv = loadGeminiEnv() + } + useStdin := taskSpec.UseStdin targetArg := taskSpec.Task if useStdin { @@ -948,6 +959,9 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe if cfg.Backend == "claude" && len(claudeEnv) > 0 { cmd.SetEnv(claudeEnv) } + if cfg.Backend == "gemini" && len(geminiEnv) > 0 { + cmd.SetEnv(geminiEnv) + } // For backends that don't support -C flag (claude, gemini), set working directory via cmd.Dir // Codex passes workdir via -C flag, so we skip setting Dir for it to avoid conflicts diff --git a/codeagent-wrapper/main.go b/codeagent-wrapper/main.go index 749e464..7926f2d 100644 --- a/codeagent-wrapper/main.go +++ b/codeagent-wrapper/main.go @@ -434,12 +434,13 @@ func run() (exitCode int) { logInfo(fmt.Sprintf("%s running...", cfg.Backend)) taskSpec := TaskSpec{ - Task: taskText, - WorkDir: cfg.WorkDir, - Mode: cfg.Mode, - SessionID: cfg.SessionID, - Model: cfg.Model, - UseStdin: useStdin, + Task: taskText, + WorkDir: cfg.WorkDir, + Mode: cfg.Mode, + SessionID: cfg.SessionID, + Model: cfg.Model, + ReasoningEffort: cfg.ReasoningEffort, + UseStdin: useStdin, } result := runTaskFn(taskSpec, false, cfg.Timeout) diff --git a/codeagent-wrapper/main_test.go b/codeagent-wrapper/main_test.go index 908cdf2..e59e5e9 100644 --- a/codeagent-wrapper/main_test.go +++ b/codeagent-wrapper/main_test.go @@ -1290,6 +1290,65 @@ func TestBackendParseArgs_ModelFlag(t *testing.T) { } } +func TestBackendParseArgs_ReasoningEffortFlag(t *testing.T) { + tests := []struct { + name string + args []string + want string + wantErr bool + }{ + { + name: "reasoning-effort flag", + args: []string{"codeagent-wrapper", "--reasoning-effort", "low", "task"}, + want: "low", + }, + { + name: "reasoning-effort equals syntax", + args: []string{"codeagent-wrapper", "--reasoning-effort=medium", "task"}, + want: "medium", + }, + { + name: "reasoning-effort trimmed", + args: []string{"codeagent-wrapper", "--reasoning-effort", " high ", "task"}, + want: "high", + }, + { + name: "reasoning-effort with resume mode", + args: []string{"codeagent-wrapper", "--reasoning-effort", "low", "resume", "sid", "task"}, + want: "low", + }, + { + name: "missing reasoning-effort value", + args: []string{"codeagent-wrapper", "--reasoning-effort"}, + wantErr: true, + }, + { + name: "reasoning-effort equals missing value", + args: []string{"codeagent-wrapper", "--reasoning-effort=", "task"}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Args = tt.args + cfg, err := parseArgs() + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.ReasoningEffort != tt.want { + t.Fatalf("ReasoningEffort = %q, want %q", cfg.ReasoningEffort, tt.want) + } + }) + } +} + func TestBackendParseArgs_PromptFileFlag(t *testing.T) { tests := []struct { name string @@ -1829,6 +1888,28 @@ func TestRun_PromptFilePrefixesTask(t *testing.T) { }) } +func TestRun_PassesReasoningEffortToTaskSpec(t *testing.T) { + defer resetTestHooks() + cleanupLogsFn = func() (CleanupStats, error) { return CleanupStats{}, nil } + + stdinReader = strings.NewReader("") + isTerminalFn = func() bool { return true } + + var got TaskSpec + runTaskFn = func(task TaskSpec, silent bool, timeout int) TaskResult { + got = task + return TaskResult{ExitCode: 0, Message: "ok"} + } + + os.Args = []string{"codeagent-wrapper", "--reasoning-effort", "high", "task"} + if code := run(); code != 0 { + t.Fatalf("run exit = %d, want 0", code) + } + if got.ReasoningEffort != "high" { + t.Fatalf("ReasoningEffort = %q, want %q", got.ReasoningEffort, "high") + } +} + func TestRunBuildCodexArgs_NewMode(t *testing.T) { const key = "CODEX_BYPASS_SANDBOX" t.Setenv(key, "false") @@ -1852,6 +1933,64 @@ func TestRunBuildCodexArgs_NewMode(t *testing.T) { } } +func TestRunBuildCodexArgs_NewMode_WithReasoningEffort(t *testing.T) { + const key = "CODEX_BYPASS_SANDBOX" + t.Setenv(key, "false") + + cfg := &Config{Mode: "new", WorkDir: "/test/dir", ReasoningEffort: "high"} + args := buildCodexArgs(cfg, "my task") + expected := []string{ + "e", + "--reasoning-effort", "high", + "--skip-git-repo-check", + "-C", "/test/dir", + "--json", + "my task", + } + if len(args) != len(expected) { + t.Fatalf("len mismatch") + } + for i := range args { + if args[i] != expected[i] { + t.Fatalf("args[%d]=%s, want %s", i, args[i], expected[i]) + } + } +} + +func TestRunCodexTaskWithContext_CodexReasoningEffort(t *testing.T) { + defer resetTestHooks() + t.Setenv("CODEX_BYPASS_SANDBOX", "false") + + var gotArgs []string + origRunner := newCommandRunner + newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner { + gotArgs = append([]string(nil), args...) + return newFakeCmd(fakeCmdConfig{ + PID: 123, + StdoutPlan: []fakeStdoutEvent{ + {Data: "{\"type\":\"result\",\"session_id\":\"sid\",\"result\":\"ok\"}\n"}, + }, + }) + } + t.Cleanup(func() { newCommandRunner = origRunner }) + + res := runCodexTaskWithContext(context.Background(), TaskSpec{Task: "hi", Mode: "new", WorkDir: defaultWorkdir, ReasoningEffort: "high"}, nil, nil, false, true, 5) + if res.ExitCode != 0 || res.Message != "ok" { + t.Fatalf("unexpected result: %+v", res) + } + + found := false + for i := 0; i+1 < len(gotArgs); i++ { + if gotArgs[i] == "--reasoning-effort" && gotArgs[i+1] == "high" { + found = true + break + } + } + if !found { + t.Fatalf("expected --reasoning-effort high in args, got %v", gotArgs) + } +} + func TestRunBuildCodexArgs_ResumeMode(t *testing.T) { const key = "CODEX_BYPASS_SANDBOX" t.Setenv(key, "false")