Skip to main content

git_internal/internal/object/
run.rs

1//! AI Run Definition
2//!
3//! A `Run` represents a single execution instance of an AI agent attempting to perform a `Task`.
4//! It captures the execution context (environment, agent role) and tracks the progress.
5//!
6//! # Relationship to Task
7//!
8//! A `Task` can have multiple `Run`s. This happens when:
9//! - An agent fails and retries.
10//! - A user requests a different approach.
11//! - Multiple agents work on the same task in parallel (future).
12//!
13//! # Key Fields
14//!
15//! - `task_id`: Links back to the parent Task.
16//! - `base_commit_sha`: The Git commit hash where this run started.
17//! - `context_snapshot_id`: Links to the captured context (files, docs) used.
18
19use std::{collections::HashMap, fmt};
20
21use serde::{Deserialize, Serialize};
22use uuid::Uuid;
23
24use crate::{
25    errors::GitError,
26    hash::ObjectHash,
27    internal::object::{
28        ObjectTrait,
29        integrity::IntegrityHash,
30        types::{ActorRef, Header, ObjectType},
31    },
32};
33
34/// Run lifecycle status.
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
36#[serde(rename_all = "snake_case")]
37pub enum RunStatus {
38    /// Run created, agent not yet started.
39    Created,
40    /// Agent is generating patches.
41    Patching,
42    /// Agent is running verification tools.
43    Validating,
44    /// Agent has finished successfully.
45    Completed,
46    /// Agent encountered an unrecoverable error.
47    Failed,
48}
49
50impl RunStatus {
51    pub fn as_str(&self) -> &'static str {
52        match self {
53            RunStatus::Created => "created",
54            RunStatus::Patching => "patching",
55            RunStatus::Validating => "validating",
56            RunStatus::Completed => "completed",
57            RunStatus::Failed => "failed",
58        }
59    }
60}
61
62impl fmt::Display for RunStatus {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        write!(f, "{}", self.as_str())
65    }
66}
67
68/// Environment snapshot of the run host.
69/// Captured at run creation time.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct Environment {
72    pub os: String,   // e.g. "macos", "linux"
73    pub arch: String, // e.g. "aarch64", "x86_64"
74    pub cwd: String,  // Current working directory
75    #[serde(flatten)]
76    pub extra: HashMap<String, serde_json::Value>,
77}
78
79impl Environment {
80    /// Create a new environment object from the current system environment
81    pub fn capture() -> Self {
82        Self {
83            os: std::env::consts::OS.to_string(),
84            arch: std::env::consts::ARCH.to_string(),
85            cwd: std::env::current_dir()
86                .map(|p| p.to_string_lossy().to_string())
87                .unwrap_or_else(|e| {
88                    tracing::warn!("Failed to get current directory: {}", e);
89                    "unknown".to_string()
90                }),
91            extra: HashMap::new(),
92        }
93    }
94}
95
96/// Agent instance participating in a run.
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct AgentInstance {
99    pub role: String,
100    pub provider_route: Option<String>,
101}
102
103/// Run object for a single orchestration execution.
104/// Links a task to execution state and environment.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct Run {
107    #[serde(flatten)]
108    header: Header,
109    task_id: Uuid,
110    orchestrator_version: String,
111    base_commit_sha: IntegrityHash,
112    status: RunStatus,
113    context_snapshot_id: Option<Uuid>,
114    #[serde(default)]
115    agent_instances: Vec<AgentInstance>,
116    metrics: Option<serde_json::Value>,
117    error: Option<String>,
118    environment: Option<Environment>,
119}
120
121impl Run {
122    /// Create a new Run.
123    ///
124    /// # Arguments
125    /// * `repo_id` - Repository UUID
126    /// * `created_by` - Actor (usually the Orchestrator)
127    /// * `task_id` - The Task this run belongs to
128    /// * `base_commit_sha` - The Git commit hash of the checkout
129    pub fn new(
130        repo_id: Uuid,
131        created_by: ActorRef,
132        task_id: Uuid,
133        base_commit_sha: impl AsRef<str>,
134    ) -> Result<Self, String> {
135        let base_commit_sha = base_commit_sha.as_ref().parse()?;
136        Ok(Self {
137            header: Header::new(ObjectType::Run, repo_id, created_by)?,
138            task_id,
139            orchestrator_version: "libra-builtin".to_string(),
140            base_commit_sha,
141            status: RunStatus::Created,
142            context_snapshot_id: None,
143            agent_instances: Vec::new(),
144            metrics: None,
145            error: None,
146            environment: Some(Environment::capture()),
147        })
148    }
149
150    pub fn header(&self) -> &Header {
151        &self.header
152    }
153
154    pub fn task_id(&self) -> Uuid {
155        self.task_id
156    }
157
158    pub fn orchestrator_version(&self) -> &str {
159        &self.orchestrator_version
160    }
161
162    pub fn base_commit_sha(&self) -> &IntegrityHash {
163        &self.base_commit_sha
164    }
165
166    pub fn status(&self) -> &RunStatus {
167        &self.status
168    }
169
170    pub fn context_snapshot_id(&self) -> Option<Uuid> {
171        self.context_snapshot_id
172    }
173
174    pub fn agent_instances(&self) -> &[AgentInstance] {
175        &self.agent_instances
176    }
177
178    pub fn metrics(&self) -> Option<&serde_json::Value> {
179        self.metrics.as_ref()
180    }
181
182    pub fn error(&self) -> Option<&str> {
183        self.error.as_deref()
184    }
185
186    pub fn environment(&self) -> Option<&Environment> {
187        self.environment.as_ref()
188    }
189
190    pub fn set_status(&mut self, status: RunStatus) {
191        self.status = status;
192    }
193
194    pub fn set_context_snapshot_id(&mut self, context_snapshot_id: Option<Uuid>) {
195        self.context_snapshot_id = context_snapshot_id;
196    }
197
198    pub fn add_agent_instance(&mut self, instance: AgentInstance) {
199        self.agent_instances.push(instance);
200    }
201
202    pub fn set_metrics(&mut self, metrics: Option<serde_json::Value>) {
203        self.metrics = metrics;
204    }
205
206    pub fn set_error(&mut self, error: Option<String>) {
207        self.error = error;
208    }
209}
210
211impl fmt::Display for Run {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        write!(f, "Run: {}", self.header.object_id())
214    }
215}
216
217impl ObjectTrait for Run {
218    fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
219    where
220        Self: Sized,
221    {
222        serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
223    }
224
225    fn get_type(&self) -> ObjectType {
226        ObjectType::Run
227    }
228
229    fn get_size(&self) -> usize {
230        serde_json::to_vec(self).map(|v| v.len()).unwrap_or(0)
231    }
232
233    fn to_data(&self) -> Result<Vec<u8>, GitError> {
234        serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    fn test_hash_hex() -> String {
243        IntegrityHash::compute(b"ai-process-test").to_hex()
244    }
245
246    #[test]
247    fn test_new_objects_creation() {
248        let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
249        let actor = ActorRef::agent("test-agent").expect("actor");
250        let base_hash = test_hash_hex();
251
252        // Run with environment (auto captured)
253        let run = Run::new(repo_id, actor.clone(), Uuid::from_u128(0x1), &base_hash).expect("run");
254
255        let env = run.environment().unwrap();
256        // Check if it captured real values (assuming we are running on some OS)
257        assert!(!env.os.is_empty());
258        assert!(!env.arch.is_empty());
259        assert!(!env.cwd.is_empty());
260    }
261}