Skip to main content

forge_agent/
lib.rs

1//! ForgeKit agent layer - Deterministic AI loop.
2//!
3//! This crate provides a deterministic agent loop for AI-driven code operations:
4//!
5//! - Observation: Gather context from the graph
6//! - Constraint: Apply policy rules
7//! - Planning: Generate execution steps
8//! - Mutation: Apply changes
9//! - Verification: Validate results
10//! - Commit: Finalize transaction
11//!
12//! # Status
13//!
14//! This crate is under active development. Observation and planning phases are implemented.
15
16use std::path::PathBuf;
17
18// Observation module (Phase 4 - Task 1)
19pub mod observe;
20
21// Policy module (Phase 4 - Task 2)
22pub mod policy;
23
24// Planning module (Phase 4 - Task 3)
25pub mod planner;
26
27// Mutation module (Phase 4 - Task 4)
28pub mod mutate;
29
30// Verification module (Phase 4 - Task 5)
31pub mod verify;
32
33// Commit module (Phase 4 - Task 6)
34pub mod commit;
35
36/// Error types for agent operations.
37#[derive(thiserror::Error, Debug)]
38pub enum AgentError {
39    /// Observation phase failed
40    #[error("Observation failed: {0}")]
41    ObservationFailed(String),
42
43    /// Planning phase failed
44    #[error("Planning failed: {0}")]
45    PlanningFailed(String),
46
47    /// Mutation phase failed
48    #[error("Mutation failed: {0}")]
49    MutationFailed(String),
50
51    /// Verification phase failed
52    #[error("Verification failed: {0}")]
53    VerificationFailed(String),
54
55    /// Commit phase failed
56    #[error("Commit failed: {0}")]
57    CommitFailed(String),
58
59    /// Policy constraint violated
60    #[error("Policy violation: {0}")]
61    PolicyViolation(String),
62
63    /// Error from Forge SDK
64    #[error("Forge error: {0}")]
65    ForgeError(#[from] forge_core::ForgeError),
66}
67
68/// Result type for agent operations.
69pub type Result<T> = std::result::Result<T, AgentError>;
70
71// Re-export policy module
72pub use policy::{Policy, PolicyReport, PolicyValidator, PolicyViolation};
73
74// Re-export observation types
75pub use observe::Observation;
76
77/// Result of applying policy constraints.
78#[derive(Clone, Debug)]
79pub struct ConstrainedPlan {
80    /// The original observation
81    pub observation: Observation,
82    /// Any policy violations detected
83    pub policy_violations: Vec<policy::PolicyViolation>,
84}
85
86/// Execution plan for the mutation phase.
87#[derive(Clone, Debug)]
88pub struct ExecutionPlan {
89    /// Steps to execute
90    pub steps: Vec<planner::PlanStep>,
91    /// Estimated impact
92    pub estimated_impact: planner::ImpactEstimate,
93    /// Rollback plan
94    pub rollback_plan: Vec<planner::RollbackStep>,
95}
96
97/// Result of the mutation phase.
98#[derive(Clone, Debug)]
99pub struct MutationResult {
100    /// Files that were modified
101    pub modified_files: Vec<PathBuf>,
102    /// Diffs of changes made
103    pub diffs: Vec<String>,
104}
105
106/// Result of the verification phase.
107#[derive(Clone, Debug)]
108pub struct VerificationResult {
109    /// Whether verification passed
110    pub passed: bool,
111    /// Any diagnostics or errors
112    pub diagnostics: Vec<String>,
113}
114
115/// Result of the commit phase.
116#[derive(Clone, Debug)]
117pub struct CommitResult {
118    /// Transaction ID for the commit
119    pub transaction_id: String,
120    /// Files that were committed
121    pub files_committed: Vec<PathBuf>,
122}
123
124/// Agent for deterministic AI-driven code operations.
125///
126/// The agent follows a strict loop:
127/// 1. Observe: Gather context from the graph
128/// 2. Constrain: Apply policy rules
129/// 3. Plan: Generate execution steps
130/// 4. Mutate: Apply changes
131/// 5. Verify: Validate results
132/// 6. Commit: Finalize transaction
133///
134pub struct Agent {
135    /// Path to the codebase
136    #[allow(dead_code)]
137    codebase_path: PathBuf,
138    /// Forge SDK instance for graph queries
139    forge: Option<forge_core::Forge>,
140}
141
142impl Agent {
143    /// Creates a new agent for the given codebase.
144    ///
145    /// # Arguments
146    ///
147    /// * `codebase_path` - Path to the codebase
148    pub async fn new(codebase_path: impl AsRef<std::path::Path>) -> Result<Self> {
149        let path = codebase_path.as_ref().to_path_buf();
150
151        // Try to initialize Forge SDK
152        let forge = forge_core::Forge::open(&path).await.ok();
153
154        Ok(Self {
155            codebase_path: path,
156            forge,
157        })
158    }
159
160    /// Observes the codebase to gather context for a query.
161    ///
162    /// # Arguments
163    ///
164    /// * `query` - The natural language query or request
165    pub async fn observe(&self, query: &str) -> Result<Observation> {
166        let forge = self
167            .forge
168            .as_ref()
169            .ok_or_else(|| AgentError::ObservationFailed("Forge SDK not available".to_string()))?;
170
171        let observer = observe::Observer::new(forge.clone());
172        let obs = observer.gather(query).await?;
173
174        // Return the observation directly - it's already the correct type
175        Ok(obs)
176    }
177
178    /// Applies policy constraints to the observation.
179    ///
180    /// # Arguments
181    ///
182    /// * `observation` - The observation to constrain
183    /// * `policies` - The policies to validate
184    pub async fn constrain(
185        &self,
186        observation: Observation,
187        policies: Vec<policy::Policy>,
188    ) -> Result<ConstrainedPlan> {
189        let forge = self.forge.as_ref().ok_or_else(|| {
190            AgentError::ObservationFailed(
191                "Forge SDK not available for policy validation".to_string(),
192            )
193        })?;
194
195        // Create a validator
196        let validator = policy::PolicyValidator::new(forge.clone());
197
198        // For observation, create a placeholder diff
199        // In production, this would be the actual planned diff
200        let diff = policy::Diff {
201            file_path: std::path::PathBuf::from(""),
202            original: String::new(),
203            modified: String::new(),
204            changes: Vec::new(),
205        };
206
207        // Validate policies
208        let report = validator.validate(&diff, &policies).await?;
209
210        Ok(ConstrainedPlan {
211            observation,
212            policy_violations: report.violations,
213        })
214    }
215
216    /// Generates an execution plan from the constrained observation.
217    pub async fn plan(&self, constrained: ConstrainedPlan) -> Result<ExecutionPlan> {
218        let forge = self.forge.as_ref().ok_or_else(|| {
219            AgentError::PlanningFailed("Forge SDK not available for planning".to_string())
220        })?;
221
222        // Create planner
223        let planner_instance = planner::Planner::new(forge.clone());
224
225        // Convert observation to the planner's format
226        let obs = observe::Observation {
227            query: constrained.observation.query.clone(),
228            symbols: vec![],
229        };
230
231        // Generate steps
232        let steps = planner_instance.generate_steps(&obs).await?;
233
234        // Estimate impact
235        let impact = planner_instance.estimate_impact(&steps).await?;
236
237        // Detect conflicts
238        let conflicts = planner_instance.detect_conflicts(&steps)?;
239
240        if !conflicts.is_empty() {
241            return Err(AgentError::PlanningFailed(format!(
242                "Found {} conflicts in plan",
243                conflicts.len()
244            )));
245        }
246
247        // Order steps based on dependencies
248        let mut ordered_steps = steps;
249        planner_instance.order_steps(&mut ordered_steps)?;
250
251        // Generate rollback plan
252        let rollback = planner_instance.generate_rollback(&ordered_steps);
253
254        Ok(ExecutionPlan {
255            steps: ordered_steps,
256            estimated_impact: planner::ImpactEstimate {
257                affected_files: impact.affected_files,
258                complexity: impact.complexity,
259            },
260            rollback_plan: rollback,
261        })
262    }
263
264    /// Executes the mutation phase of the plan.
265    pub async fn mutate(&self, plan: ExecutionPlan) -> Result<MutationResult> {
266        let forge = self
267            .forge
268            .as_ref()
269            .ok_or_else(|| AgentError::MutationFailed("Forge SDK not available".to_string()))?;
270
271        let mut mutator = mutate::Mutator::new(forge.clone());
272        mutator.begin_transaction().await?;
273
274        for step in &plan.steps {
275            mutator.apply_step(step).await?;
276        }
277
278        Ok(MutationResult {
279            modified_files: vec![],
280            diffs: vec!["Transaction completed".to_string()],
281        })
282    }
283
284    /// Verifies the mutation result.
285    pub async fn verify(&self, _result: MutationResult) -> Result<VerificationResult> {
286        let forge = self
287            .forge
288            .as_ref()
289            .ok_or_else(|| AgentError::VerificationFailed("Forge SDK not available".to_string()))?;
290
291        let verifier = verify::Verifier::new(forge.clone());
292        let report = verifier.verify(&self.codebase_path).await?;
293
294        Ok(VerificationResult {
295            passed: report.passed,
296            diagnostics: report
297                .diagnostics
298                .iter()
299                .map(|d| d.message.clone())
300                .collect(),
301        })
302    }
303
304    /// Commits the verified mutation.
305    pub async fn commit(&self, result: VerificationResult) -> Result<CommitResult> {
306        let forge = self
307            .forge
308            .as_ref()
309            .ok_or_else(|| AgentError::CommitFailed("Forge SDK not available".to_string()))?;
310
311        let committer = commit::Committer::new(forge.clone());
312        let files: Vec<std::path::PathBuf> = result
313            .diagnostics
314            .iter()
315            .filter_map(|d| {
316                // Parse diagnostics to extract file paths
317                // Format: "file:line:col: message"
318                d.split(':')
319                    .next()
320                    .map(|s| std::path::PathBuf::from(s.trim()))
321            })
322            .collect();
323
324        let commit_report = committer.finalize(&self.codebase_path, &files).await?;
325
326        Ok(CommitResult {
327            transaction_id: commit_report.transaction_id,
328            files_committed: commit_report.files_committed,
329        })
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[tokio::test]
338    async fn test_agent_creation() {
339        let temp = tempfile::tempdir().unwrap();
340        let agent = Agent::new(temp.path()).await.unwrap();
341
342        assert_eq!(agent.codebase_path, temp.path());
343    }
344}