Skip to main content

made_core/value_objects/ceremony/
execution_profile.rs

1use serde::{Deserialize, Serialize};
2
3use super::{ExecutionProfileFallbackPolicy, ExecutionProfileInheritance};
4use crate::error::DomainError;
5
6const MAX_PROFILE_TEXT: usize = 256;
7
8/// Host-owned execution selection recorded with a claimed step.
9///
10/// The ceremony definition remains provider-neutral. The host supplies the
11/// requested selection and records the actual selection it used, including
12/// an explicit fallback, agent incarnation and any checkpoint handoff.
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct ExecutionProfile {
15    requested_model: String,
16    requested_reasoning_effort: String,
17    required_capabilities: Vec<String>,
18    fallback_policy: ExecutionProfileFallbackPolicy,
19    fallback_model: Option<String>,
20    fallback_reasoning_effort: Option<String>,
21    actual_model: String,
22    actual_reasoning_effort: String,
23    actual_capabilities: Vec<String>,
24    host_agent_id: String,
25    host_agent_incarnation: String,
26    inherited_from: Option<ExecutionProfileInheritance>,
27    checkpoint_id: Option<String>,
28    handoff_from: Option<String>,
29}
30
31impl ExecutionProfile {
32    pub fn from_json(value: serde_json::Value) -> Result<Self, DomainError> {
33        let profile: Self =
34            serde_json::from_value(value).map_err(|_| DomainError::InvariantViolated {
35                reason: "invalid execution profile payload",
36            })?;
37        Self::new(
38            profile.requested_model,
39            profile.requested_reasoning_effort,
40            profile.required_capabilities,
41            profile.fallback_policy,
42            profile.fallback_model,
43            profile.fallback_reasoning_effort,
44            profile.actual_model,
45            profile.actual_reasoning_effort,
46            profile.actual_capabilities,
47            profile.host_agent_id,
48            profile.host_agent_incarnation,
49            profile.inherited_from,
50            profile.checkpoint_id,
51            profile.handoff_from,
52        )
53    }
54
55    #[allow(clippy::too_many_arguments)]
56    pub fn new(
57        requested_model: impl Into<String>,
58        requested_reasoning_effort: impl Into<String>,
59        required_capabilities: Vec<String>,
60        fallback_policy: ExecutionProfileFallbackPolicy,
61        fallback_model: Option<String>,
62        fallback_reasoning_effort: Option<String>,
63        actual_model: impl Into<String>,
64        actual_reasoning_effort: impl Into<String>,
65        actual_capabilities: Vec<String>,
66        host_agent_id: impl Into<String>,
67        host_agent_incarnation: impl Into<String>,
68        inherited_from: Option<ExecutionProfileInheritance>,
69        checkpoint_id: Option<String>,
70        handoff_from: Option<String>,
71    ) -> Result<Self, DomainError> {
72        let profile = Self {
73            requested_model: clean(requested_model.into(), "execution_profile.requested_model")?,
74            requested_reasoning_effort: clean(
75                requested_reasoning_effort.into(),
76                "execution_profile.requested_reasoning_effort",
77            )?,
78            required_capabilities: clean_list(
79                required_capabilities,
80                "execution_profile.required_capabilities",
81            )?,
82            fallback_policy,
83            fallback_model: fallback_model
84                .map(|value| clean(value, "execution_profile.fallback_model"))
85                .transpose()?,
86            fallback_reasoning_effort: fallback_reasoning_effort
87                .map(|value| clean(value, "execution_profile.fallback_reasoning_effort"))
88                .transpose()?,
89            actual_model: clean(actual_model.into(), "execution_profile.actual_model")?,
90            actual_reasoning_effort: clean(
91                actual_reasoning_effort.into(),
92                "execution_profile.actual_reasoning_effort",
93            )?,
94            actual_capabilities: clean_list(
95                actual_capabilities,
96                "execution_profile.actual_capabilities",
97            )?,
98            host_agent_id: clean(host_agent_id.into(), "execution_profile.host_agent_id")?,
99            host_agent_incarnation: clean(
100                host_agent_incarnation.into(),
101                "execution_profile.host_agent_incarnation",
102            )?,
103            inherited_from,
104            checkpoint_id: checkpoint_id
105                .map(|value| clean(value, "execution_profile.checkpoint_id"))
106                .transpose()?,
107            handoff_from: handoff_from
108                .map(|value| clean(value, "execution_profile.handoff_from"))
109                .transpose()?,
110        };
111        profile.validate_resolution()?;
112        Ok(profile)
113    }
114
115    /// Validate the host's requested-versus-actual selection.
116    pub fn validate_resolution(&self) -> Result<(), DomainError> {
117        if self
118            .required_capabilities
119            .iter()
120            .any(|required| !self.actual_capabilities.contains(required))
121        {
122            return Err(DomainError::InvariantViolated {
123                reason:
124                    "actual execution profile capabilities do not satisfy required capabilities",
125            });
126        }
127        match self.fallback_policy {
128            ExecutionProfileFallbackPolicy::Reject
129                if self.actual_model != self.requested_model
130                    || self.actual_reasoning_effort != self.requested_reasoning_effort =>
131            {
132                Err(DomainError::InvariantViolated {
133                    reason:
134                        "requested execution profile is unsupported and fallback_policy is reject",
135                })
136            }
137            ExecutionProfileFallbackPolicy::Fallback => {
138                if self.actual_model != self.requested_model
139                    && self
140                        .fallback_model
141                        .as_deref()
142                        .is_some_and(|fallback| fallback != self.actual_model)
143                {
144                    return Err(DomainError::InvariantViolated {
145                        reason: "declared fallback model does not match actual model",
146                    });
147                }
148                if self.actual_reasoning_effort != self.requested_reasoning_effort
149                    && self
150                        .fallback_reasoning_effort
151                        .as_deref()
152                        .is_some_and(|fallback| fallback != self.actual_reasoning_effort)
153                {
154                    return Err(DomainError::InvariantViolated {
155                        reason: "declared fallback reasoning effort does not match actual effort",
156                    });
157                }
158                if self.actual_model != self.requested_model
159                    && self.fallback_model.as_deref() != Some(self.actual_model.as_str())
160                {
161                    return Err(DomainError::InvariantViolated {
162                        reason: "actual model is not the declared execution profile fallback",
163                    });
164                }
165                if self.actual_reasoning_effort != self.requested_reasoning_effort
166                    && self.fallback_reasoning_effort.as_deref()
167                        != Some(self.actual_reasoning_effort.as_str())
168                {
169                    return Err(DomainError::InvariantViolated {
170                        reason:
171                            "actual reasoning effort is not the declared execution profile fallback",
172                    });
173                }
174                Ok(())
175            }
176            ExecutionProfileFallbackPolicy::Reject => Ok(()),
177        }
178    }
179
180    #[must_use]
181    pub fn requested_model(&self) -> &str {
182        &self.requested_model
183    }
184    #[must_use]
185    pub fn requested_reasoning_effort(&self) -> &str {
186        &self.requested_reasoning_effort
187    }
188    #[must_use]
189    pub fn required_capabilities(&self) -> &[String] {
190        &self.required_capabilities
191    }
192    #[must_use]
193    pub const fn fallback_policy(&self) -> ExecutionProfileFallbackPolicy {
194        self.fallback_policy
195    }
196    #[must_use]
197    pub fn actual_model(&self) -> &str {
198        &self.actual_model
199    }
200    #[must_use]
201    pub fn actual_reasoning_effort(&self) -> &str {
202        &self.actual_reasoning_effort
203    }
204    #[must_use]
205    pub fn actual_capabilities(&self) -> &[String] {
206        &self.actual_capabilities
207    }
208    #[must_use]
209    pub fn host_agent_id(&self) -> &str {
210        &self.host_agent_id
211    }
212    #[must_use]
213    pub fn host_agent_incarnation(&self) -> &str {
214        &self.host_agent_incarnation
215    }
216    #[must_use]
217    pub fn inherited_from(&self) -> Option<ExecutionProfileInheritance> {
218        self.inherited_from
219    }
220    #[must_use]
221    pub fn checkpoint_id(&self) -> Option<&str> {
222        self.checkpoint_id.as_deref()
223    }
224    #[must_use]
225    pub fn handoff_from(&self) -> Option<&str> {
226        self.handoff_from.as_deref()
227    }
228}
229
230fn clean(value: impl Into<String>, field: &'static str) -> Result<String, DomainError> {
231    let value = value.into().trim().to_owned();
232    if value.is_empty() {
233        return Err(DomainError::EmptyField { field });
234    }
235    if value.len() > MAX_PROFILE_TEXT {
236        return Err(DomainError::FieldTooLong {
237            field,
238            actual: value.len(),
239            max: MAX_PROFILE_TEXT,
240        });
241    }
242    if value.chars().any(char::is_control) {
243        return Err(DomainError::InvalidCharacters { field });
244    }
245    Ok(value)
246}
247
248fn clean_list(values: Vec<String>, field: &'static str) -> Result<Vec<String>, DomainError> {
249    values
250        .into_iter()
251        .map(|value| clean(value, field))
252        .collect()
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    fn profile(
260        policy: ExecutionProfileFallbackPolicy,
261        actual_model: &str,
262    ) -> Result<ExecutionProfile, DomainError> {
263        ExecutionProfile::new(
264            "strong-model",
265            "high",
266            vec!["reasoning".into()],
267            policy,
268            Some("balanced-model".into()),
269            Some("medium".into()),
270            actual_model,
271            if actual_model == "strong-model" {
272                "high"
273            } else {
274                "medium"
275            },
276            vec!["reasoning".into()],
277            "codex-agent-1",
278            "inc-2",
279            Some(ExecutionProfileInheritance::RoleDefault),
280            Some("checkpoint-7".into()),
281            Some("codex-agent-0/inc-1".into()),
282        )
283    }
284
285    #[test]
286    fn fallback_is_explicit_and_actionable() {
287        assert!(profile(ExecutionProfileFallbackPolicy::Fallback, "balanced-model").is_ok());
288        assert!(profile(ExecutionProfileFallbackPolicy::Reject, "balanced-model").is_err());
289    }
290
291    #[test]
292    fn requested_and_actual_identity_are_retained() {
293        let selected = profile(ExecutionProfileFallbackPolicy::Fallback, "balanced-model").unwrap();
294        assert_eq!(selected.requested_model(), "strong-model");
295        assert_eq!(selected.actual_model(), "balanced-model");
296        assert_eq!(
297            selected.inherited_from(),
298            Some(ExecutionProfileInheritance::RoleDefault)
299        );
300        assert_eq!(selected.handoff_from(), Some("codex-agent-0/inc-1"));
301    }
302
303    #[test]
304    fn required_capabilities_must_be_realised_by_actual_selection() {
305        assert!(ExecutionProfile::new(
306            "strong-model",
307            "high",
308            vec!["reasoning".into(), "vision".into()],
309            ExecutionProfileFallbackPolicy::Reject,
310            None,
311            None,
312            "strong-model",
313            "high",
314            vec!["reasoning".into()],
315            "codex-agent-1",
316            "inc-2",
317            None,
318            None,
319            None,
320        )
321        .is_err());
322    }
323
324    #[test]
325    fn fallback_model_and_effort_are_one_coherent_selection() {
326        assert!(ExecutionProfile::new(
327            "strong-model",
328            "high",
329            vec!["reasoning".into()],
330            ExecutionProfileFallbackPolicy::Fallback,
331            Some("balanced-model".into()),
332            Some("high".into()),
333            "balanced-model",
334            "medium",
335            vec!["reasoning".into()],
336            "codex-agent-1",
337            "inc-2",
338            None,
339            None,
340            None,
341        )
342        .is_err());
343    }
344
345    #[test]
346    fn supported_requested_selection_does_not_use_declared_fallback() {
347        assert!(ExecutionProfile::new(
348            "strong-model",
349            "high",
350            vec!["reasoning".into()],
351            ExecutionProfileFallbackPolicy::Fallback,
352            Some("balanced-model".into()),
353            Some("medium".into()),
354            "strong-model",
355            "high",
356            vec!["reasoning".into()],
357            "codex-agent-1",
358            "inc-2",
359            None,
360            None,
361            None,
362        )
363        .is_ok());
364    }
365}