Skip to main content

git_internal/internal/object/
context.rs

1//! AI Context Snapshot Definition
2//!
3//! A `ContextSnapshot` represents the state of the codebase and external resources
4//! that an agent uses to perform its task.
5//!
6//! # Selection Strategy
7//!
8//! - **Explicit**: User manually selected files.
9//! - **Heuristic**: Agent automatically selected files based on relevance.
10//!
11//! # Integrity
12//!
13//! Each item in the snapshot has a content hash (`IntegrityHash`).
14//! This ensures that if the file changes on disk, we know the snapshot is stale or refers to an older version.
15
16use std::fmt::Display;
17
18use serde::{Deserialize, Serialize};
19use uuid::Uuid;
20
21use crate::{
22    errors::GitError,
23    hash::ObjectHash,
24    internal::object::{
25        ObjectTrait,
26        integrity::IntegrityHash,
27        types::{ActorRef, Header, ObjectType},
28    },
29};
30
31/// Selection strategy for context snapshots.
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33#[serde(rename_all = "snake_case")]
34pub enum SelectionStrategy {
35    /// Files explicitly chosen by the user.
36    Explicit,
37    /// Files automatically selected by the agent/system.
38    Heuristic,
39}
40
41/// Context item kind.
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
43#[serde(rename_all = "snake_case")]
44pub enum ContextItemKind {
45    /// A regular file in the repository.
46    File,
47}
48
49/// Context item describing a single input.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ContextItem {
52    pub kind: ContextItemKind,
53    pub path: String,
54    pub content_id: IntegrityHash,
55}
56
57impl ContextItem {
58    pub fn new(
59        kind: ContextItemKind,
60        path: impl Into<String>,
61        content_id: IntegrityHash,
62    ) -> Result<Self, String> {
63        let path = path.into();
64        if path.trim().is_empty() {
65            return Err("path cannot be empty".to_string());
66        }
67        Ok(Self {
68            kind,
69            path,
70            content_id,
71        })
72    }
73}
74
75/// Context snapshot describing selected inputs.
76/// Captures the selection strategy and content identifiers used by a run.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct ContextSnapshot {
79    #[serde(flatten)]
80    header: Header,
81    base_commit_sha: IntegrityHash,
82    selection_strategy: SelectionStrategy,
83    #[serde(default)]
84    items: Vec<ContextItem>,
85    summary: Option<String>,
86}
87
88impl ContextSnapshot {
89    pub fn new(
90        repo_id: Uuid,
91        created_by: ActorRef,
92        base_commit_sha: impl AsRef<str>,
93        selection_strategy: SelectionStrategy,
94    ) -> Result<Self, String> {
95        let base_commit_sha = base_commit_sha.as_ref().parse()?;
96        Ok(Self {
97            header: Header::new(ObjectType::ContextSnapshot, repo_id, created_by)?,
98            base_commit_sha,
99            selection_strategy,
100            items: Vec::new(),
101            summary: None,
102        })
103    }
104
105    pub fn header(&self) -> &Header {
106        &self.header
107    }
108
109    pub fn base_commit_sha(&self) -> &IntegrityHash {
110        &self.base_commit_sha
111    }
112
113    pub fn selection_strategy(&self) -> &SelectionStrategy {
114        &self.selection_strategy
115    }
116
117    pub fn items(&self) -> &[ContextItem] {
118        &self.items
119    }
120
121    pub fn summary(&self) -> Option<&str> {
122        self.summary.as_deref()
123    }
124
125    pub fn add_item(&mut self, item: ContextItem) {
126        self.items.push(item);
127    }
128
129    pub fn set_summary(&mut self, summary: Option<String>) {
130        self.summary = summary;
131    }
132}
133
134impl Display for ContextSnapshot {
135    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
136        write!(f, "ContextSnapshot: {}", self.header.object_id())
137    }
138}
139
140impl ObjectTrait for ContextSnapshot {
141    fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
142    where
143        Self: Sized,
144    {
145        serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
146    }
147
148    fn get_type(&self) -> ObjectType {
149        ObjectType::ContextSnapshot
150    }
151
152    fn get_size(&self) -> usize {
153        serde_json::to_vec(self).map(|v| v.len()).unwrap_or(0)
154    }
155
156    fn to_data(&self) -> Result<Vec<u8>, GitError> {
157        serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn test_context_snapshot_accessors_and_mutators() {
167        let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
168        let actor = ActorRef::agent("coder").expect("actor");
169        let mut snapshot = ContextSnapshot::new(
170            repo_id,
171            actor,
172            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
173            SelectionStrategy::Heuristic,
174        )
175        .expect("snapshot");
176
177        assert_eq!(snapshot.selection_strategy(), &SelectionStrategy::Heuristic);
178        assert!(snapshot.items().is_empty());
179        assert!(snapshot.summary().is_none());
180
181        let item = ContextItem::new(
182            ContextItemKind::File,
183            "src/main.rs",
184            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
185                .parse()
186                .expect("hash"),
187        )
188        .expect("item");
189        snapshot.add_item(item);
190        snapshot.set_summary(Some("selected by relevance".to_string()));
191
192        assert_eq!(snapshot.items().len(), 1);
193        assert_eq!(snapshot.summary(), Some("selected by relevance"));
194        assert_eq!(snapshot.base_commit_sha().to_hex().len(), 64);
195    }
196}