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// Loop module (Phase 3 - Task 1)
37pub mod r#loop;
38
39// Audit module (Phase 3 - Task 2)
40pub mod audit;
41
42// Workflow module (Phase 8 - Plan 1)
43pub mod workflow;
44
45/// Error types for agent operations.
46#[derive(thiserror::Error, Debug)]
47pub enum AgentError {
48    /// Observation phase failed
49    #[error("Observation failed: {0}")]
50    ObservationFailed(String),
51
52    /// Planning phase failed
53    #[error("Planning failed: {0}")]
54    PlanningFailed(String),
55
56    /// Mutation phase failed
57    #[error("Mutation failed: {0}")]
58    MutationFailed(String),
59
60    /// Verification phase failed
61    #[error("Verification failed: {0}")]
62    VerificationFailed(String),
63
64    /// Commit phase failed
65    #[error("Commit failed: {0}")]
66    CommitFailed(String),
67
68    /// Policy constraint violated
69    #[error("Policy violation: {0}")]
70    PolicyViolation(String),
71
72    /// Error from Forge SDK
73    #[error("Forge error: {0}")]
74    ForgeError(#[from] forge_core::ForgeError),
75}
76
77/// Result type for agent operations.
78pub type Result<T> = std::result::Result<T, AgentError>;
79
80// Re-export policy module
81pub use policy::{Policy, PolicyReport, PolicyValidator, PolicyViolation};
82
83// Re-export observation types
84pub use observe::Observation;
85
86// Re-export loop types
87pub use r#loop::{AgentLoop, AgentPhase, LoopResult};
88
89// Re-export audit types
90pub use audit::{AuditEvent, AuditLog};
91
92// Re-export workflow types
93pub use workflow::{
94    Dependency, TaskContext, TaskError, TaskId, TaskResult, ValidationReport, Workflow,
95    WorkflowError, WorkflowExecutor, WorkflowResult, WorkflowTask, WorkflowValidator,
96};
97
98/// Result of applying policy constraints.
99#[derive(Clone, Debug)]
100pub struct ConstrainedPlan {
101    /// The original observation
102    pub observation: Observation,
103    /// Any policy violations detected
104    pub policy_violations: Vec<policy::PolicyViolation>,
105}
106
107/// Execution plan for the mutation phase.
108#[derive(Clone, Debug)]
109pub struct ExecutionPlan {
110    /// Steps to execute
111    pub steps: Vec<planner::PlanStep>,
112    /// Estimated impact
113    pub estimated_impact: planner::ImpactEstimate,
114    /// Rollback plan
115    pub rollback_plan: Vec<planner::RollbackStep>,
116}
117
118/// Result of the mutation phase.
119#[derive(Clone, Debug)]
120pub struct MutationResult {
121    /// Files that were modified
122    pub modified_files: Vec<PathBuf>,
123    /// Diffs of changes made
124    pub diffs: Vec<String>,
125}
126
127/// Result of the verification phase.
128#[derive(Clone, Debug)]
129pub struct VerificationResult {
130    /// Whether verification passed
131    pub passed: bool,
132    /// Any diagnostics or errors
133    pub diagnostics: Vec<String>,
134}
135
136/// Result of the commit phase.
137#[derive(Clone, Debug)]
138pub struct CommitResult {
139    /// Transaction ID for the commit
140    pub transaction_id: String,
141    /// Files that were committed
142    pub files_committed: Vec<PathBuf>,
143}
144
145/// Agent for deterministic AI-driven code operations.
146///
147/// The agent follows a strict loop:
148/// 1. Observe: Gather context from the graph
149/// 2. Constrain: Apply policy rules
150/// 3. Plan: Generate execution steps
151/// 4. Mutate: Apply changes
152/// 5. Verify: Validate results
153/// 6. Commit: Finalize transaction
154///
155/// # Runtime Integration
156///
157/// The agent can integrate with `ForgeRuntime` for coordinated file watching
158/// and caching:
159///
160/// ```ignore
161/// let (agent, mut runtime) = Agent::with_runtime("./project").await?;
162/// let result = agent.run_with_runtime(&mut runtime, "refactor function").await?;
163/// ```
164///
165pub struct Agent {
166    /// Path to the codebase
167    #[allow(dead_code)]
168    codebase_path: PathBuf,
169    /// Forge SDK instance for graph queries
170    forge: Option<forge_core::Forge>,
171}
172
173impl Agent {
174    /// Creates a new agent for the given codebase.
175    ///
176    /// # Arguments
177    ///
178    /// * `codebase_path` - Path to the codebase
179    pub async fn new(codebase_path: impl AsRef<std::path::Path>) -> Result<Self> {
180        let path = codebase_path.as_ref().to_path_buf();
181
182        // Try to initialize Forge SDK
183        let forge = forge_core::Forge::open(&path).await.ok();
184
185        Ok(Self {
186            codebase_path: path,
187            forge,
188        })
189    }
190
191    /// Observes the codebase to gather context for a query.
192    ///
193    /// # Arguments
194    ///
195    /// * `query` - The natural language query or request
196    pub async fn observe(&self, query: &str) -> Result<Observation> {
197        let forge = self
198            .forge
199            .as_ref()
200            .ok_or_else(|| AgentError::ObservationFailed("Forge SDK not available".to_string()))?;
201
202        let observer = observe::Observer::new(forge.clone());
203        let obs = observer.gather(query).await?;
204
205        // Return the observation directly - it's already the correct type
206        Ok(obs)
207    }
208
209    /// Applies policy constraints to the observation.
210    ///
211    /// # Arguments
212    ///
213    /// * `observation` - The observation to constrain
214    /// * `policies` - The policies to validate
215    pub async fn constrain(
216        &self,
217        observation: Observation,
218        policies: Vec<policy::Policy>,
219    ) -> Result<ConstrainedPlan> {
220        let forge = self.forge.as_ref().ok_or_else(|| {
221            AgentError::ObservationFailed(
222                "Forge SDK not available for policy validation".to_string(),
223            )
224        })?;
225
226        // Create a validator
227        let validator = policy::PolicyValidator::new(forge.clone());
228
229        // For observation, create a placeholder diff
230        // In production, this would be the actual planned diff
231        let diff = policy::Diff {
232            file_path: std::path::PathBuf::from(""),
233            original: String::new(),
234            modified: String::new(),
235            changes: Vec::new(),
236        };
237
238        // Validate policies
239        let report = validator.validate(&diff, &policies).await?;
240
241        Ok(ConstrainedPlan {
242            observation,
243            policy_violations: report.violations,
244        })
245    }
246
247    /// Generates an execution plan from the constrained observation.
248    pub async fn plan(&self, constrained: ConstrainedPlan) -> Result<ExecutionPlan> {
249        // Create planner
250        let planner_instance = planner::Planner::new();
251
252        // Convert observation to the planner's format
253        let obs = observe::Observation {
254            query: constrained.observation.query.clone(),
255            symbols: vec![],
256        };
257
258        // Generate steps
259        let steps = planner_instance.generate_steps(&obs).await?;
260
261        // Estimate impact
262        let impact = planner_instance.estimate_impact(&steps).await?;
263
264        // Detect conflicts
265        let conflicts = planner_instance.detect_conflicts(&steps)?;
266
267        if !conflicts.is_empty() {
268            return Err(AgentError::PlanningFailed(format!(
269                "Found {} conflicts in plan",
270                conflicts.len()
271            )));
272        }
273
274        // Order steps based on dependencies
275        let mut ordered_steps = steps;
276        planner_instance.order_steps(&mut ordered_steps)?;
277
278        // Generate rollback plan
279        let rollback = planner_instance.generate_rollback(&ordered_steps);
280
281        Ok(ExecutionPlan {
282            steps: ordered_steps,
283            estimated_impact: planner::ImpactEstimate {
284                affected_files: impact.affected_files,
285                complexity: impact.complexity,
286            },
287            rollback_plan: rollback,
288        })
289    }
290
291    /// Executes the mutation phase of the plan.
292    pub async fn mutate(&self, plan: ExecutionPlan) -> Result<MutationResult> {
293        // Verify forge is available
294        self.forge.as_ref().ok_or_else(|| AgentError::MutationFailed("Forge SDK not available".to_string()))?;
295
296        let mut mutator = mutate::Mutator::new();
297        mutator.begin_transaction().await?;
298
299        for step in &plan.steps {
300            mutator.apply_step(step).await?;
301        }
302
303        Ok(MutationResult {
304            modified_files: vec![],
305            diffs: vec!["Transaction completed".to_string()],
306        })
307    }
308
309    /// Verifies the mutation result.
310    pub async fn verify(&self, _result: MutationResult) -> Result<VerificationResult> {
311        let verifier = verify::Verifier::new();
312        let report = verifier.verify(&self.codebase_path).await?;
313
314        Ok(VerificationResult {
315            passed: report.passed,
316            diagnostics: report
317                .diagnostics
318                .iter()
319                .map(|d| d.message.clone())
320                .collect(),
321        })
322    }
323
324    /// Commits the verified mutation.
325    pub async fn commit(&self, result: VerificationResult) -> Result<CommitResult> {
326        let committer = commit::Committer::new();
327        let files: Vec<std::path::PathBuf> = result
328            .diagnostics
329            .iter()
330            .filter_map(|d| {
331                // Parse diagnostics to extract file paths
332                // Format: "file:line:col: message"
333                d.split(':')
334                    .next()
335                    .map(|s| std::path::PathBuf::from(s.trim()))
336            })
337            .collect();
338
339        let commit_report = committer.finalize(&self.codebase_path, &files).await?;
340
341        Ok(CommitResult {
342            transaction_id: commit_report.transaction_id,
343            files_committed: commit_report.files_committed,
344        })
345    }
346
347    /// Runs the full agent loop: Observe -> Constrain -> Plan -> Mutate -> Verify -> Commit
348    ///
349    /// This is the main entry point for executing a complete agent operation.
350    /// Each phase receives the output of the previous phase, and failures
351    /// trigger rollback with audit trail entries.
352    ///
353    /// # Arguments
354    ///
355    /// * `query` - The natural language query or request
356    ///
357    /// # Returns
358    ///
359    /// Returns `LoopResult` with transaction ID, modified files, and audit trail.
360    ///
361    /// # Example
362    ///
363    /// ```no_run
364    /// use forge_agent::Agent;
365    ///
366    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
367    /// let agent = Agent::new(".").await?;
368    /// let result = agent.run("Add error handling to the parser").await?;
369    /// println!("Transaction ID: {}", result.transaction_id);
370    /// # Ok(())
371    /// # }
372    /// ```
373    pub async fn run(&self, query: &str) -> Result<LoopResult> {
374        let forge = self
375            .forge
376            .as_ref()
377            .ok_or_else(|| AgentError::ObservationFailed("Forge SDK not available".to_string()))?;
378
379        // Create fresh loop state (no state leakage between runs)
380        let mut agent_loop = r#loop::AgentLoop::new(std::sync::Arc::new(forge.clone()));
381
382        agent_loop.run(query).await
383    }
384}
385
386// Transaction module (Phase 3 - Plan 3)
387pub mod transaction;
388
389// Re-export transaction types
390pub use transaction::{FileSnapshot, Transaction, TransactionState};
391
392// Runtime integration module (Phase 3 - Plan 4)
393pub mod runtime_integration;
394
395// Re-export runtime types for convenience
396pub use forge_runtime::{ForgeRuntime, RuntimeConfig, RuntimeStats};
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    #[tokio::test]
403    async fn test_agent_creation() {
404        let temp = tempfile::tempdir().unwrap();
405        let agent = Agent::new(temp.path()).await.unwrap();
406
407        assert_eq!(agent.codebase_path, temp.path());
408    }
409
410    #[tokio::test]
411    async fn test_agent_with_runtime() {
412        let temp = tempfile::tempdir().unwrap();
413        let (_agent, mut runtime) = Agent::with_runtime(temp.path()).await.unwrap();
414
415        // Verify runtime is accessible
416        assert_eq!(runtime.codebase_path(), temp.path());
417
418        // Run agent with runtime
419        let result = _agent.run("test query").await;
420
421        // Should complete (may fail on actual query, but infrastructure works)
422        assert!(result.is_ok() || result.is_err());
423    }
424
425    #[tokio::test]
426    async fn test_agent_runtime_stats() {
427        let temp = tempfile::tempdir().unwrap();
428        let (_agent, runtime) = Agent::with_runtime(temp.path()).await.unwrap();
429
430        let stats = runtime.stats();
431        assert!(!stats.watch_active); // Not started
432    }
433
434    #[tokio::test]
435    async fn test_agent_backward_compatibility() {
436        // Agent should work without runtime (backward compatibility)
437        let temp = tempfile::tempdir().unwrap();
438        let agent = Agent::new(temp.path()).await.unwrap();
439
440        // Agent should be functional standalone
441        assert_eq!(agent.codebase_path, temp.path());
442
443        // runtime_cache and runtime_stats should return None
444        assert!(agent.runtime_cache().is_none());
445        assert!(agent.runtime_stats().is_none());
446    }
447}