enact-core 0.0.1

Core agent runtime for Enact - Graph-Native AI agents
Documentation
//! Agentic Loop implementation
//!
//! The core loop that drives long-running executions.
//! It handles the "Discovery -> Execute -> Repeat" cycle.

use crate::callable::{Callable, DynCallable};
use crate::graph::CheckpointStore;
use crate::policy::LongRunningExecutionPolicy;
use crate::runner::Runner;
use crate::streaming::StreamEvent;
use std::time::{Duration, Instant};

/// The agentic loop driver
#[allow(dead_code)]
pub struct AgenticLoop;

#[allow(dead_code)]
impl AgenticLoop {
    /// Run the agentic loop for a callable
    pub async fn run<S: CheckpointStore>(
        runner: &mut Runner<S>,
        callable: DynCallable,
        input: String,
        policy: LongRunningExecutionPolicy,
    ) -> anyhow::Result<String> {
        // Initial setup
        let start_time = Instant::now();
        let mut steps_executed = 0;
        let mut _tokens_used = 0;
        let mut history: Vec<String> = Vec::new();

        // Push initial input
        history.push(format!("User: {}", input));

        // Initial execution
        let current_input = input.clone();
        let current_callable = callable;

        // Main Loop
        loop {
            // 1. Check Limits (simplified for now, full enforcement in kernel/enforcement.rs)
            if let Some(max_steps) = policy.max_discovered_steps {
                if steps_executed > max_steps {
                    runner.emitter().emit(StreamEvent::execution_failed(
                        runner.execution_id(),
                        crate::kernel::ExecutionError::quota_exceeded("Max steps exceeded"),
                    ));
                    anyhow::bail!("Max steps exceeded");
                }
            }

            if let Some(timeout) = policy.idle_timeout_seconds {
                if start_time.elapsed() > Duration::from_secs(timeout) {
                    runner.emitter().emit(StreamEvent::execution_failed(
                        runner.execution_id(),
                        crate::kernel::ExecutionError::timeout(format!(
                            "Idle timeout after {}s",
                            timeout
                        )),
                    ));
                    anyhow::bail!("Idle timeout");
                }
            }

            // 2. Execute Step
            let result = runner
                .run_callable(current_callable.as_ref() as &dyn Callable, &current_input)
                .await;

            match result {
                Ok(output) => {
                    history.push(format!("Assistant: {}", output));
                    steps_executed += 1;

                    // 3. Check for Checkpoint
                    if policy.checkpointing.on_discovery
                        || policy
                            .checkpointing
                            .interval_steps
                            .map_or(false, |i| steps_executed % i == 0)
                    {
                        // Create a checkpoint state (simplified)
                        let state = crate::graph::NodeState::from_str(&output);
                        if let Err(e) = runner
                            .save_checkpoint(state, Some(current_callable.name()))
                            .await
                        {
                            // Log warning but continue?
                            eprintln!("Failed to save checkpoint: {}", e);
                        }
                    }

                    // 4. Determine Next Step (Discovery)
                    // In a real agentic DAG, the output might contain instructions to run more tools
                    // or discover new steps. For this v0 implementation, we check if the callable
                    // returned a "DONE" signal or if it's a single-shot callable.

                    // TODO: Parse output for discovered steps if this is a DiscoveryAgent
                    // For now, we assume simple single-turn or limited loop

                    // IF output suggests more work, continue.
                    // IF output suggests completion, break.

                    // Simple heuristic for now: if it's just a generic callable, run once.
                    // If we want a loop, we'd need a "LoopCallable" wrapper or parsing logic.
                    // Given the prompt asks for "loop for long running executions", we implement
                    // the framework here but defaults to single pass unless discovered steps are found.

                    // (Placeholder for proper Discovery logic which would be in the Callable result)

                    return Ok(output);
                }
                Err(e) => return Err(e),
            }
        }
    }
}