Skip to main content

git_internal/internal/object/
decision.rs

1//! AI Decision Definition
2//!
3//! `Decision` represents the final outcome of an agent's run. It signals whether the proposed
4//! changes should be applied, rejected, or if the agent needs human intervention.
5//!
6//! # Decision Types
7//!
8//! - **Commit**: Changes are good, apply them.
9//! - **Abandon**: Task is impossible or not worth doing.
10//! - **Retry**: Something went wrong, try again (with different params/prompt).
11//! - **Checkpoint**: Save progress but don't finish yet.
12//! - **Rollback**: Revert changes.
13
14use std::fmt;
15
16use serde::{Deserialize, Serialize};
17use uuid::Uuid;
18
19use crate::{
20    errors::GitError,
21    hash::ObjectHash,
22    internal::object::{
23        ObjectTrait,
24        integrity::IntegrityHash,
25        types::{ActorRef, Header, ObjectType},
26    },
27};
28
29/// Type of decision.
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31#[serde(rename_all = "snake_case")]
32pub enum DecisionType {
33    /// Approve and commit changes.
34    Commit,
35    /// Save intermediate progress.
36    Checkpoint,
37    /// Give up on the task.
38    Abandon,
39    /// Try again (re-run).
40    Retry,
41    /// Revert applied changes.
42    Rollback,
43    #[serde(untagged)]
44    Other(String),
45}
46
47impl fmt::Display for DecisionType {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            DecisionType::Commit => write!(f, "commit"),
51            DecisionType::Checkpoint => write!(f, "checkpoint"),
52            DecisionType::Abandon => write!(f, "abandon"),
53            DecisionType::Retry => write!(f, "retry"),
54            DecisionType::Rollback => write!(f, "rollback"),
55            DecisionType::Other(s) => write!(f, "{}", s),
56        }
57    }
58}
59
60impl From<String> for DecisionType {
61    fn from(s: String) -> Self {
62        match s.as_str() {
63            "commit" => DecisionType::Commit,
64            "checkpoint" => DecisionType::Checkpoint,
65            "abandon" => DecisionType::Abandon,
66            "retry" => DecisionType::Retry,
67            "rollback" => DecisionType::Rollback,
68            _ => DecisionType::Other(s),
69        }
70    }
71}
72
73impl From<&str> for DecisionType {
74    fn from(s: &str) -> Self {
75        match s {
76            "commit" => DecisionType::Commit,
77            "checkpoint" => DecisionType::Checkpoint,
78            "abandon" => DecisionType::Abandon,
79            "retry" => DecisionType::Retry,
80            "rollback" => DecisionType::Rollback,
81            _ => DecisionType::Other(s.to_string()),
82        }
83    }
84}
85
86/// Decision object linking process to outcomes.
87/// Records the final outcome of a run.
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct Decision {
90    #[serde(flatten)]
91    header: Header,
92    run_id: Uuid,
93    decision_type: DecisionType,
94    chosen_patchset_id: Option<Uuid>,
95    result_commit_sha: Option<IntegrityHash>,
96    checkpoint_id: Option<String>,
97    rationale: Option<String>,
98}
99
100impl Decision {
101    /// Create a new decision object
102    pub fn new(
103        repo_id: Uuid,
104        created_by: ActorRef,
105        run_id: Uuid,
106        decision_type: impl Into<DecisionType>,
107    ) -> Result<Self, String> {
108        Ok(Self {
109            header: Header::new(ObjectType::Decision, repo_id, created_by)?,
110            run_id,
111            decision_type: decision_type.into(),
112            chosen_patchset_id: None,
113            result_commit_sha: None,
114            checkpoint_id: None,
115            rationale: None,
116        })
117    }
118
119    pub fn header(&self) -> &Header {
120        &self.header
121    }
122
123    pub fn run_id(&self) -> Uuid {
124        self.run_id
125    }
126
127    pub fn decision_type(&self) -> &DecisionType {
128        &self.decision_type
129    }
130
131    pub fn chosen_patchset_id(&self) -> Option<Uuid> {
132        self.chosen_patchset_id
133    }
134
135    pub fn result_commit_sha(&self) -> Option<&IntegrityHash> {
136        self.result_commit_sha.as_ref()
137    }
138
139    pub fn checkpoint_id(&self) -> Option<&str> {
140        self.checkpoint_id.as_deref()
141    }
142
143    pub fn rationale(&self) -> Option<&str> {
144        self.rationale.as_deref()
145    }
146
147    pub fn set_chosen_patchset_id(&mut self, chosen_patchset_id: Option<Uuid>) {
148        self.chosen_patchset_id = chosen_patchset_id;
149    }
150
151    pub fn set_result_commit_sha(&mut self, result_commit_sha: Option<IntegrityHash>) {
152        self.result_commit_sha = result_commit_sha;
153    }
154
155    pub fn set_checkpoint_id(&mut self, checkpoint_id: Option<String>) {
156        self.checkpoint_id = checkpoint_id;
157    }
158
159    pub fn set_rationale(&mut self, rationale: Option<String>) {
160        self.rationale = rationale;
161    }
162}
163
164impl fmt::Display for Decision {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        write!(f, "Decision: {}", self.header.object_id())
167    }
168}
169
170impl ObjectTrait for Decision {
171    fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
172    where
173        Self: Sized,
174    {
175        serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
176    }
177
178    fn get_type(&self) -> ObjectType {
179        ObjectType::Decision
180    }
181
182    fn get_size(&self) -> usize {
183        serde_json::to_vec(self).map(|v| v.len()).unwrap_or(0)
184    }
185
186    fn to_data(&self) -> Result<Vec<u8>, GitError> {
187        serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_decision_fields() {
197        let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
198        let actor = ActorRef::agent("test-agent").expect("actor");
199        let run_id = Uuid::from_u128(0x1);
200        let patchset_id = Uuid::from_u128(0x2);
201        let expected_hash = IntegrityHash::compute(b"decision-hash");
202
203        let mut decision = Decision::new(repo_id, actor, run_id, "commit").expect("decision");
204        decision.set_chosen_patchset_id(Some(patchset_id));
205        decision.set_result_commit_sha(Some(expected_hash));
206        decision.set_rationale(Some("tests passed".to_string()));
207
208        assert_eq!(decision.chosen_patchset_id(), Some(patchset_id));
209        assert_eq!(decision.result_commit_sha(), Some(&expected_hash));
210        assert_eq!(decision.rationale(), Some("tests passed"));
211        assert_eq!(decision.decision_type(), &DecisionType::Commit);
212    }
213}