Skip to main content

ralph/contracts/config/
phase.rs

1//! Phase override configuration for per-phase runner/model/reasoning settings.
2//!
3//! Responsibilities:
4//! - Define phase override structs and merge behavior.
5//!
6//! Not handled here:
7//! - Phase execution logic (see `crate::commands::run::phases` module).
8
9use crate::contracts::model::{Model, ReasoningEffort};
10use crate::contracts::runner::Runner;
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14/// Per-phase configuration overrides for runner, model, and reasoning effort.
15///
16/// All fields are optional to support leaf-wise merging:
17/// - `Some(value)` overrides the parent config
18/// - `None` means "inherit from parent"
19#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
20#[serde(default, deny_unknown_fields)]
21pub struct PhaseOverrideConfig {
22    /// Runner to use for this phase (overrides global agent.runner)
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub runner: Option<Runner>,
25
26    /// Model to use for this phase (overrides global agent.model)
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub model: Option<Model>,
29
30    /// Reasoning effort for this phase (overrides global agent.reasoning_effort)
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub reasoning_effort: Option<ReasoningEffort>,
33}
34
35impl PhaseOverrideConfig {
36    /// Leaf-wise merge: other.Some overrides self, other.None preserves self
37    pub fn merge_from(&mut self, other: Self) {
38        if other.runner.is_some() {
39            self.runner = other.runner;
40        }
41        if other.model.is_some() {
42            self.model = other.model;
43        }
44        if other.reasoning_effort.is_some() {
45            self.reasoning_effort = other.reasoning_effort;
46        }
47    }
48}
49
50/// Phase overrides container for Phase 1/2/3 execution.
51///
52/// Per-phase configuration for Phase 1/2/3 execution.
53///
54/// Invariants/assumptions:
55/// - Overrides are defined per phase only; there is no shared `defaults` layer inside
56///   `agent.phase_overrides`. Use global `agent.runner` / `agent.model` /
57///   `agent.reasoning_effort` for shared defaults.
58/// - Merging is leaf-wise: `Some(value)` overrides, `None` inherits.
59#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
60#[serde(default, deny_unknown_fields)]
61pub struct PhaseOverrides {
62    /// Phase 1 specific overrides
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub phase1: Option<PhaseOverrideConfig>,
65
66    /// Phase 2 specific overrides
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub phase2: Option<PhaseOverrideConfig>,
69
70    /// Phase 3 specific overrides
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub phase3: Option<PhaseOverrideConfig>,
73}
74
75impl PhaseOverrides {
76    /// Merge other into self following leaf-wise semantics:
77    /// Merge each specific phase override
78    pub fn merge_from(&mut self, other: Self) {
79        // Merge phase1
80        match (&mut self.phase1, other.phase1) {
81            (Some(existing), Some(new)) => existing.merge_from(new),
82            (None, Some(new)) => self.phase1 = Some(new),
83            _ => {}
84        }
85
86        // Merge phase2
87        match (&mut self.phase2, other.phase2) {
88            (Some(existing), Some(new)) => existing.merge_from(new),
89            (None, Some(new)) => self.phase2 = Some(new),
90            _ => {}
91        }
92
93        // Merge phase3
94        match (&mut self.phase3, other.phase3) {
95            (Some(existing), Some(new)) => existing.merge_from(new),
96            (None, Some(new)) => self.phase3 = Some(new),
97            _ => {}
98        }
99    }
100}