Skip to main content

git_internal/internal/object/
run.rs

1//! AI Run snapshot.
2//!
3//! `Run` stores one immutable execution attempt for a `Task`.
4//!
5//! # How to use this object
6//!
7//! - Create a `Run` when Libra starts a new execution attempt.
8//! - Set the selected `Plan`, optional `ContextSnapshot`, and runtime
9//!   `Environment` before persistence.
10//! - Create a fresh `Run` for retries instead of mutating a prior run.
11//!
12//! # How it works with other objects
13//!
14//! - `Provenance` records model/provider configuration for the run.
15//! - `ToolInvocation`, `RunEvent`, `PlanStepEvent`, `Evidence`,
16//!   `PatchSet`, `Decision`, and `RunUsage` all attach to `Run`.
17//! - `Decision` is the terminal verdict for a run.
18//!
19//! # How Libra should call it
20//!
21//! Libra should treat `Run` as the execution envelope and keep "active
22//! run", retries, and scheduling state in Libra. Execution progress,
23//! metrics, and failures must be appended as event objects rather than
24//! written back onto the run snapshot.
25
26use std::{collections::HashMap, fmt};
27
28use serde::{Deserialize, Serialize};
29use uuid::Uuid;
30
31use crate::{
32    errors::GitError,
33    hash::ObjectHash,
34    internal::object::{
35        ObjectTrait,
36        integrity::IntegrityHash,
37        types::{ActorRef, Header, ObjectType},
38    },
39};
40
41/// Best-effort runtime environment capture for one `Run`.
42///
43/// This is a lightweight reproducibility aid. Libra may augment or
44/// normalize these values before persistence if it needs stricter
45/// environment tracking.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47#[serde(deny_unknown_fields)]
48pub struct Environment {
49    /// Operating system identifier captured for the run environment.
50    pub os: String,
51    /// CPU architecture identifier captured for the run environment.
52    pub arch: String,
53    /// Working directory from which the run was started.
54    pub cwd: String,
55    /// Additional application-defined environment metadata.
56    #[serde(flatten)]
57    pub extra: HashMap<String, serde_json::Value>,
58}
59
60impl Environment {
61    /// Capture a best-effort environment snapshot from the current
62    /// process.
63    pub fn capture() -> Self {
64        Self {
65            os: std::env::consts::OS.to_string(),
66            arch: std::env::consts::ARCH.to_string(),
67            cwd: std::env::current_dir()
68                .map(|p| p.to_string_lossy().to_string())
69                .unwrap_or_else(|e| {
70                    tracing::warn!("Failed to get current directory: {}", e);
71                    "unknown".to_string()
72                }),
73            extra: HashMap::new(),
74        }
75    }
76}
77
78/// Immutable execution-attempt envelope.
79///
80/// A stored `Run` says "this attempt existed against this task / plan /
81/// commit baseline". It does not itself accumulate logs or status
82/// transitions after persistence.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(deny_unknown_fields)]
85pub struct Run {
86    /// Common object header carrying the immutable object id, type,
87    /// creator, and timestamps.
88    #[serde(flatten)]
89    header: Header,
90    /// Canonical owning task for this execution attempt.
91    task: Uuid,
92    /// Optional selected plan revision used by this attempt.
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    plan: Option<Uuid>,
95    /// Baseline repository integrity hash from which execution started.
96    commit: IntegrityHash,
97    /// Optional static context snapshot captured at run start.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    snapshot: Option<Uuid>,
100    /// Optional execution environment metadata.
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    environment: Option<Environment>,
103}
104
105impl Run {
106    /// Create a new execution attempt for the given task and commit
107    /// baseline.
108    pub fn new(created_by: ActorRef, task: Uuid, commit: impl AsRef<str>) -> Result<Self, String> {
109        let commit = commit.as_ref().parse()?;
110        Ok(Self {
111            header: Header::new(ObjectType::Run, created_by)?,
112            task,
113            plan: None,
114            commit,
115            snapshot: None,
116            environment: Some(Environment::capture()),
117        })
118    }
119
120    /// Return the immutable header for this run.
121    pub fn header(&self) -> &Header {
122        &self.header
123    }
124
125    /// Return the canonical owning task id.
126    pub fn task(&self) -> Uuid {
127        self.task
128    }
129
130    /// Return the selected plan revision, if one was stored.
131    pub fn plan(&self) -> Option<Uuid> {
132        self.plan
133    }
134
135    /// Set or clear the selected plan revision for this in-memory run.
136    pub fn set_plan(&mut self, plan: Option<Uuid>) {
137        self.plan = plan;
138    }
139
140    /// Return the baseline repository integrity hash.
141    pub fn commit(&self) -> &IntegrityHash {
142        &self.commit
143    }
144
145    /// Return the static context snapshot id, if present.
146    pub fn snapshot(&self) -> Option<Uuid> {
147        self.snapshot
148    }
149
150    /// Set or clear the static context snapshot link for this in-memory
151    /// run.
152    pub fn set_snapshot(&mut self, snapshot: Option<Uuid>) {
153        self.snapshot = snapshot;
154    }
155
156    /// Return the captured execution environment, if present.
157    pub fn environment(&self) -> Option<&Environment> {
158        self.environment.as_ref()
159    }
160}
161
162impl fmt::Display for Run {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        write!(f, "Run: {}", self.header.object_id())
165    }
166}
167
168impl ObjectTrait for Run {
169    fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
170    where
171        Self: Sized,
172    {
173        serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
174    }
175
176    fn get_type(&self) -> ObjectType {
177        ObjectType::Run
178    }
179
180    fn get_size(&self) -> usize {
181        match serde_json::to_vec(self) {
182            Ok(v) => v.len(),
183            Err(e) => {
184                tracing::warn!("failed to compute Run size: {}", e);
185                0
186            }
187        }
188    }
189
190    fn to_data(&self) -> Result<Vec<u8>, GitError> {
191        serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    // Coverage:
200    // - new run creation captures a non-empty environment snapshot
201    // - plan and context snapshot links can be assigned before storage
202
203    fn test_hash_hex() -> String {
204        IntegrityHash::compute(b"ai-process-test").to_hex()
205    }
206
207    #[test]
208    fn test_new_objects_creation() {
209        let actor = ActorRef::agent("test-agent").expect("actor");
210        let base_hash = test_hash_hex();
211        let run = Run::new(actor, Uuid::from_u128(0x1), &base_hash).expect("run");
212
213        let env = run.environment().expect("environment");
214        assert!(!env.os.is_empty());
215        assert!(!env.arch.is_empty());
216        assert!(!env.cwd.is_empty());
217    }
218
219    #[test]
220    fn test_run_plan_and_snapshot() {
221        let actor = ActorRef::agent("test-agent").expect("actor");
222        let base_hash = test_hash_hex();
223        let mut run = Run::new(actor, Uuid::from_u128(0x1), &base_hash).expect("run");
224        let plan_id = Uuid::from_u128(0x10);
225        let snapshot_id = Uuid::from_u128(0x20);
226
227        run.set_plan(Some(plan_id));
228        run.set_snapshot(Some(snapshot_id));
229
230        assert_eq!(run.plan(), Some(plan_id));
231        assert_eq!(run.snapshot(), Some(snapshot_id));
232    }
233}