Skip to main content

git_internal/internal/object/
plan.rs

1//! AI Plan Definition
2//!
3//! A `Plan` represents a sequence of steps that an agent intends to execute to complete a task.
4//!
5//! # Versioning
6//!
7//! Plans are versioned monotonically. As the agent learns more or encounters obstacles,
8//! it may update the plan. Each update creates a new `Plan` object with `plan_version = previous + 1`.
9//!
10//! # Steps
11//!
12//! Each step has an `intent` (what to do) and a `status` (pending/in_progress/done).
13//! Steps can also define expected inputs/outputs for better chain-of-thought tracking.
14
15use std::fmt;
16
17use serde::{Deserialize, Serialize};
18use uuid::Uuid;
19
20use crate::{
21    errors::GitError,
22    hash::ObjectHash,
23    internal::object::{
24        ObjectTrait,
25        types::{ActorRef, Header, ObjectType},
26    },
27};
28
29/// Plan step status.
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31#[serde(rename_all = "snake_case")]
32pub enum PlanStatus {
33    /// Step is waiting to be executed.
34    Pending,
35    /// Step is currently being executed.
36    InProgress,
37    /// Step finished successfully.
38    Completed,
39    /// Step failed.
40    Failed,
41    /// Step was skipped (e.g. no longer necessary).
42    Skipped,
43}
44
45impl PlanStatus {
46    pub fn as_str(&self) -> &'static str {
47        match self {
48            PlanStatus::Pending => "pending",
49            PlanStatus::InProgress => "in_progress",
50            PlanStatus::Completed => "completed",
51            PlanStatus::Failed => "failed",
52            PlanStatus::Skipped => "skipped",
53        }
54    }
55}
56
57impl fmt::Display for PlanStatus {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        write!(f, "{}", self.as_str())
60    }
61}
62
63/// Plan step with inputs, outputs, and checks.
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65pub struct PlanStep {
66    pub intent: String,
67    pub inputs: Option<serde_json::Value>,
68    pub outputs: Option<serde_json::Value>,
69    pub checks: Option<serde_json::Value>,
70    pub owner_role: Option<String>,
71    pub status: PlanStatus,
72}
73
74/// Plan object for step decomposition.
75/// New versions are created via `new_next` with monotonic versioning.
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
77pub struct Plan {
78    #[serde(flatten)]
79    header: Header,
80    run_id: Uuid,
81    /// Plan version starts at 1 and must increase by 1 for each update.
82    plan_version: u32,
83    #[serde(default)]
84    steps: Vec<PlanStep>,
85}
86
87impl Plan {
88    /// Create a new plan object (version 1)
89    pub fn new(repo_id: Uuid, created_by: ActorRef, run_id: Uuid) -> Result<Self, String> {
90        Ok(Self {
91            header: Header::new(ObjectType::Plan, repo_id, created_by)?,
92            run_id,
93            plan_version: 1,
94            steps: Vec::new(),
95        })
96    }
97
98    /// Create the next version of a plan.
99    ///
100    /// # Arguments
101    /// * `previous_version` - The version number of the plan being updated.
102    pub fn new_next(
103        repo_id: Uuid,
104        created_by: ActorRef,
105        run_id: Uuid,
106        previous_version: u32,
107    ) -> Result<Self, String> {
108        let next_version = previous_version
109            .checked_add(1)
110            .ok_or_else(|| "plan_version overflow".to_string())?;
111        Ok(Self {
112            header: Header::new(ObjectType::Plan, repo_id, created_by)?,
113            run_id,
114            plan_version: next_version,
115            steps: Vec::new(),
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 plan_version(&self) -> u32 {
128        self.plan_version
129    }
130
131    pub fn steps(&self) -> &[PlanStep] {
132        &self.steps
133    }
134
135    pub fn add_step(&mut self, step: PlanStep) {
136        self.steps.push(step);
137    }
138}
139
140impl fmt::Display for Plan {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        write!(f, "Plan: {}", self.header.object_id())
143    }
144}
145
146impl ObjectTrait for Plan {
147    fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
148    where
149        Self: Sized,
150    {
151        serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
152    }
153
154    fn get_type(&self) -> ObjectType {
155        ObjectType::Plan
156    }
157
158    fn get_size(&self) -> usize {
159        serde_json::to_vec(self).map(|v| v.len()).unwrap_or(0)
160    }
161
162    fn to_data(&self) -> Result<Vec<u8>, GitError> {
163        serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_plan_version_ordering() {
173        let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
174        let actor = ActorRef::human("jackie").expect("actor");
175        let run_id = Uuid::from_u128(0x1);
176
177        let plan_v1 = Plan::new(repo_id, actor.clone(), run_id).expect("plan");
178        let plan_v2 =
179            Plan::new_next(repo_id, actor.clone(), run_id, plan_v1.plan_version()).expect("plan");
180        let plan_v3 =
181            Plan::new_next(repo_id, actor.clone(), run_id, plan_v2.plan_version()).expect("plan");
182
183        let mut plans = [plan_v2.clone(), plan_v1.clone(), plan_v3.clone()];
184        plans.sort_by_key(|plan| plan.plan_version());
185
186        assert_eq!(plans[0].plan_version(), 1);
187        assert_eq!(plans[1].plan_version(), 2);
188        assert_eq!(plans[2].plan_version(), 3);
189
190        assert!(plan_v3.plan_version() > plan_v2.plan_version());
191        assert!(plan_v2.plan_version() > plan_v1.plan_version());
192    }
193}