Skip to main content

ralph/contracts/config/
agent.rs

1//! Agent runner defaults configuration.
2//!
3//! Responsibilities:
4//! - Define AgentConfig struct and merge behavior for runner defaults.
5//!
6//! Not handled here:
7//! - Runner-specific configuration (see `crate::contracts::runner`).
8//! - Actual runner invocation (see `crate::runner` module).
9
10use crate::contracts::config::{
11    GitRevertMode, NotificationConfig, PhaseOverrides, RunnerRetryConfig, ScanPromptVersion,
12    WebhookConfig,
13};
14use crate::contracts::model::{Model, ReasoningEffort};
15use crate::contracts::runner::{ClaudePermissionMode, Runner, RunnerCliConfigRoot};
16use schemars::JsonSchema;
17use serde::{Deserialize, Serialize};
18use std::path::PathBuf;
19
20/// Agent runner defaults (Claude, Codex, OpenCode, Gemini, or Cursor).
21#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
22#[serde(default, deny_unknown_fields)]
23pub struct AgentConfig {
24    /// Which harness to use by default.
25    pub runner: Option<Runner>,
26
27    /// Default model.
28    pub model: Option<Model>,
29
30    /// Default reasoning effort (only meaningful for Codex models).
31    pub reasoning_effort: Option<ReasoningEffort>,
32
33    /// Number of iterations to run for each task (default: 1).
34    #[schemars(range(min = 1))]
35    pub iterations: Option<u8>,
36
37    /// Reasoning effort override for follow-up iterations (iterations > 1).
38    /// Only meaningful for Codex models.
39    pub followup_reasoning_effort: Option<ReasoningEffort>,
40
41    /// Override the codex executable name/path (default is "codex" if None).
42    pub codex_bin: Option<String>,
43
44    /// Override the opencode executable name/path (default is "opencode" if None).
45    pub opencode_bin: Option<String>,
46
47    /// Override the gemini executable name/path (default is "gemini" if None).
48    pub gemini_bin: Option<String>,
49
50    /// Override the claude executable name/path (default is "claude" if None).
51    pub claude_bin: Option<String>,
52
53    /// Override the cursor agent executable name/path (default is "agent" if None).
54    ///
55    /// NOTE: Cursor's runner binary name is `agent` (not `cursor`).
56    pub cursor_bin: Option<String>,
57
58    /// Override the kimi executable name/path (default is "kimi" if None).
59    pub kimi_bin: Option<String>,
60
61    /// Override the pi executable name/path (default is "pi" if None).
62    pub pi_bin: Option<String>,
63
64    /// Claude permission mode for tool and edit approval.
65    /// AcceptEdits: auto-approves file edits only
66    /// BypassPermissions: skip all permission prompts (YOLO mode)
67    pub claude_permission_mode: Option<ClaudePermissionMode>,
68
69    /// Normalized runner CLI behavior overrides (output/approval/sandbox/etc).
70    ///
71    /// This is additive: existing runner-specific fields remain supported.
72    pub runner_cli: Option<RunnerCliConfigRoot>,
73
74    /// Per-phase overrides for runner, model, and reasoning effort.
75    ///
76    /// Allows specifying different settings for each phase (1, 2, 3).
77    /// Phase-specific values override the global agent settings.
78    pub phase_overrides: Option<PhaseOverrides>,
79
80    /// Additional instruction files to inject at the top of every prompt sent to runner CLIs.
81    ///
82    /// Paths may be absolute, `~/`-prefixed, or repo-root relative. Missing files are treated as
83    /// configuration errors. To include repo-local AGENTS.md, add `"AGENTS.md"` to this list.
84    pub instruction_files: Option<Vec<PathBuf>>,
85
86    /// Require RepoPrompt usage during planning (inject context_builder instructions).
87    pub repoprompt_plan_required: Option<bool>,
88
89    /// Inject RepoPrompt tooling reminder block into prompts.
90    pub repoprompt_tool_injection: Option<bool>,
91
92    /// CI gate command to run (default: "make ci").
93    pub ci_gate_command: Option<String>,
94
95    /// Enable or disable the CI gate entirely (default: true).
96    pub ci_gate_enabled: Option<bool>,
97
98    /// Controls automatic git revert behavior when runner or supervision errors occur.
99    pub git_revert_mode: Option<GitRevertMode>,
100
101    /// Enable automatic git commit and push after successful runs (default: true).
102    pub git_commit_push_enabled: Option<bool>,
103
104    /// Number of execution phases (1, 2, or 3).
105    /// 1 = single-pass, 2 = plan+implement, 3 = plan+implement+review.
106    #[schemars(range(min = 1, max = 3))]
107    pub phases: Option<u8>,
108
109    /// Desktop notification configuration for task completion.
110    pub notification: NotificationConfig,
111
112    /// Webhook configuration for HTTP task event notifications.
113    pub webhook: WebhookConfig,
114
115    /// Session timeout in hours for crash recovery (default: 24).
116    /// Sessions older than this threshold are considered stale and require
117    /// explicit user confirmation to resume.
118    #[schemars(range(min = 1))]
119    pub session_timeout_hours: Option<u64>,
120
121    /// Scan prompt version to use (v1 or v2, default: v2).
122    pub scan_prompt_version: Option<ScanPromptVersion>,
123
124    /// Runner invocation retry/backoff configuration.
125    pub runner_retry: RunnerRetryConfig,
126}
127
128impl AgentConfig {
129    pub fn merge_from(&mut self, other: Self) {
130        if other.runner.is_some() {
131            self.runner = other.runner;
132        }
133        if other.model.is_some() {
134            self.model = other.model;
135        }
136        if other.reasoning_effort.is_some() {
137            self.reasoning_effort = other.reasoning_effort;
138        }
139        if other.iterations.is_some() {
140            self.iterations = other.iterations;
141        }
142        if other.followup_reasoning_effort.is_some() {
143            self.followup_reasoning_effort = other.followup_reasoning_effort;
144        }
145        if other.codex_bin.is_some() {
146            self.codex_bin = other.codex_bin;
147        }
148        if other.opencode_bin.is_some() {
149            self.opencode_bin = other.opencode_bin;
150        }
151        if other.gemini_bin.is_some() {
152            self.gemini_bin = other.gemini_bin;
153        }
154        if other.claude_bin.is_some() {
155            self.claude_bin = other.claude_bin;
156        }
157        if other.cursor_bin.is_some() {
158            self.cursor_bin = other.cursor_bin;
159        }
160        if other.kimi_bin.is_some() {
161            self.kimi_bin = other.kimi_bin;
162        }
163        if other.pi_bin.is_some() {
164            self.pi_bin = other.pi_bin;
165        }
166        if other.phases.is_some() {
167            self.phases = other.phases;
168        }
169        if other.claude_permission_mode.is_some() {
170            self.claude_permission_mode = other.claude_permission_mode;
171        }
172        if let Some(other_runner_cli) = other.runner_cli {
173            match &mut self.runner_cli {
174                Some(existing) => existing.merge_from(other_runner_cli),
175                None => self.runner_cli = Some(other_runner_cli),
176            }
177        }
178        // Merge phase_overrides
179        if let Some(other_phase_overrides) = other.phase_overrides {
180            match &mut self.phase_overrides {
181                Some(existing) => existing.merge_from(other_phase_overrides),
182                None => self.phase_overrides = Some(other_phase_overrides),
183            }
184        }
185        if other.instruction_files.is_some() {
186            self.instruction_files = other.instruction_files;
187        }
188        if other.repoprompt_plan_required.is_some() {
189            self.repoprompt_plan_required = other.repoprompt_plan_required;
190        }
191        if other.repoprompt_tool_injection.is_some() {
192            self.repoprompt_tool_injection = other.repoprompt_tool_injection;
193        }
194        if other.ci_gate_command.is_some() {
195            self.ci_gate_command = other.ci_gate_command;
196        }
197        if other.ci_gate_enabled.is_some() {
198            self.ci_gate_enabled = other.ci_gate_enabled;
199        }
200        if other.git_revert_mode.is_some() {
201            self.git_revert_mode = other.git_revert_mode;
202        }
203        if other.git_commit_push_enabled.is_some() {
204            self.git_commit_push_enabled = other.git_commit_push_enabled;
205        }
206        self.notification.merge_from(other.notification);
207        self.webhook.merge_from(other.webhook);
208        if other.session_timeout_hours.is_some() {
209            self.session_timeout_hours = other.session_timeout_hours;
210        }
211        if other.scan_prompt_version.is_some() {
212            self.scan_prompt_version = other.scan_prompt_version;
213        }
214        self.runner_retry.merge_from(other.runner_retry);
215    }
216}