enact-core 0.0.1

Core agent runtime for Enact - Graph-Native AI agents
Documentation
//! Interruptable execution for plan approval flows
//!
//! Kernel-owned interrupt handling logic.

use super::ids::ExecutionId;
use crate::streaming::{EventEmitter, StreamEvent};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::sync::mpsc;

/// Interrupt reason
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum InterruptReason {
    /// Waiting for plan approval
    PlanApproval { plan: Value },
    /// Waiting for user input
    UserInput { prompt: String },
    /// Custom interrupt
    Custom { reason: String, data: Value },
}

/// User decision for an interrupt
#[derive(Debug, Clone)]
pub enum InterruptDecision {
    /// Approve and continue
    Approve,
    /// Reject and abort/replan
    Reject { reason: Option<String> },
    /// Provide input and continue
    Input { value: Value },
}

/// Interruptable execution handler
pub struct InterruptableRunner {
    execution_id: ExecutionId,
    emitter: EventEmitter,
    interrupt_tx: mpsc::Sender<InterruptDecision>,
    interrupt_rx: Option<mpsc::Receiver<InterruptDecision>>,
}

impl InterruptableRunner {
    pub fn new() -> Self {
        let (tx, rx) = mpsc::channel(1);
        Self {
            execution_id: ExecutionId::new(),
            emitter: EventEmitter::new(),
            interrupt_tx: tx,
            interrupt_rx: Some(rx),
        }
    }

    /// Get the execution ID
    pub fn execution_id(&self) -> &ExecutionId {
        &self.execution_id
    }

    /// Backward compatibility alias
    #[deprecated(note = "Use execution_id() instead")]
    pub fn run_id(&self) -> &ExecutionId {
        &self.execution_id
    }

    pub fn emitter(&self) -> &EventEmitter {
        &self.emitter
    }

    /// Get the decision sender (for external use to send decisions)
    pub fn decision_sender(&self) -> mpsc::Sender<InterruptDecision> {
        self.interrupt_tx.clone()
    }

    /// Send approval decision
    pub async fn approve(&self) {
        let _ = self.interrupt_tx.send(InterruptDecision::Approve).await;
    }

    /// Send rejection decision
    pub async fn reject(&self, reason: Option<String>) {
        let _ = self.interrupt_tx.send(InterruptDecision::Reject { reason }).await;
    }

    /// Request approval and wait for decision
    pub async fn request_approval(&mut self, plan: Value) -> anyhow::Result<bool> {
        // Emit waiting event
        self.emitter.emit(StreamEvent::text_delta(
            self.execution_id.as_str(),
            format!("Waiting for approval: {}", serde_json::to_string_pretty(&plan)?),
        ));

        // Wait for decision
        if let Some(ref mut rx) = self.interrupt_rx {
            if let Some(decision) = rx.recv().await {
                match decision {
                    InterruptDecision::Approve => {
                        self.emitter.emit(StreamEvent::text_delta(
                            self.execution_id.as_str(),
                            "Plan approved, continuing execution",
                        ));
                        Ok(true)
                    }
                    InterruptDecision::Reject { reason } => {
                        self.emitter.emit(StreamEvent::text_delta(
                            self.execution_id.as_str(),
                            format!("Plan rejected: {:?}", reason),
                        ));
                        Ok(false)
                    }
                    InterruptDecision::Input { .. } => {
                        Ok(true)
                    }
                }
            } else {
                anyhow::bail!("Interrupt channel closed")
            }
        } else {
            anyhow::bail!("Interrupt receiver not available")
        }
    }
}

impl Default for InterruptableRunner {
    fn default() -> Self {
        Self::new()
    }
}