enact-core 0.0.1

Core agent runtime for Enact - Graph-Native AI agents
Documentation
//! Callable trait definition
//!
//! The core abstraction for anything that can be invoked.

use async_trait::async_trait;
use std::sync::Arc;

/// Core Callable trait - the fundamental execution unit
///
/// Everything that can be "run" implements this trait:
/// - LLM agents
/// - Graph nodes
/// - Tools (when wrapped)
/// - Flow compositions
///
/// ## Design Principles
///
/// 1. **Simple**: Just run with input, get output
/// 2. **Async**: All execution is async by default
/// 3. **Named**: Every callable has a name for logging/debugging
/// 4. **Composable**: Callables can wrap other callables
#[async_trait]
pub trait Callable: Send + Sync {
    /// Callable logical name (for logging, debugging, UI)
    fn name(&self) -> &str;

    /// Callable description (optional, for documentation)
    fn description(&self) -> Option<&str> {
        None
    }

    /// Execute the callable with the given input
    ///
    /// This is the core execution method. Implementations should:
    /// - Be idempotent when possible
    /// - Handle their own retries if needed
    /// - Return meaningful error messages
    async fn run(&self, input: &str) -> anyhow::Result<String>;
}

/// Boxed callable for dynamic dispatch
///
/// Use this when you need to store heterogeneous callables together
/// or pass them through dynamic boundaries.
pub type DynCallable = Arc<dyn Callable>;

/// A simple function-based callable
///
/// Wraps a closure to implement Callable.
#[allow(dead_code)]
pub struct FnCallable<F>
where
    F: Fn(&str) -> anyhow::Result<String> + Send + Sync,
{
    name: String,
    description: Option<String>,
    func: F,
}

#[allow(dead_code)]
impl<F> FnCallable<F>
where
    F: Fn(&str) -> anyhow::Result<String> + Send + Sync,
{
    /// Create a new function-based callable
    pub fn new(name: impl Into<String>, func: F) -> Self {
        Self {
            name: name.into(),
            description: None,
            func,
        }
    }

    /// Add a description
    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
        self.description = Some(desc.into());
        self
    }
}

#[async_trait]
impl<F> Callable for FnCallable<F>
where
    F: Fn(&str) -> anyhow::Result<String> + Send + Sync,
{
    fn name(&self) -> &str {
        &self.name
    }

    fn description(&self) -> Option<&str> {
        self.description.as_deref()
    }

    async fn run(&self, input: &str) -> anyhow::Result<String> {
        (self.func)(input)
    }
}

/// Create a callable from an async function
#[allow(dead_code)]
pub struct AsyncFnCallable<F, Fut>
where
    F: Fn(String) -> Fut + Send + Sync,
    Fut: std::future::Future<Output = anyhow::Result<String>> + Send + Sync,
{
    name: String,
    description: Option<String>,
    func: F,
    _phantom: std::marker::PhantomData<fn() -> Fut>,
}

#[allow(dead_code)]
impl<F, Fut> AsyncFnCallable<F, Fut>
where
    F: Fn(String) -> Fut + Send + Sync,
    Fut: std::future::Future<Output = anyhow::Result<String>> + Send + Sync,
{
    /// Create a new async function-based callable
    pub fn new(name: impl Into<String>, func: F) -> Self {
        Self {
            name: name.into(),
            description: None,
            func,
            _phantom: std::marker::PhantomData,
        }
    }

    /// Add a description
    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
        self.description = Some(desc.into());
        self
    }
}

#[async_trait]
impl<F, Fut> Callable for AsyncFnCallable<F, Fut>
where
    F: Fn(String) -> Fut + Send + Sync,
    Fut: std::future::Future<Output = anyhow::Result<String>> + Send + Sync,
{
    fn name(&self) -> &str {
        &self.name
    }

    fn description(&self) -> Option<&str> {
        self.description.as_deref()
    }

    async fn run(&self, input: &str) -> anyhow::Result<String> {
        (self.func)(input.to_string()).await
    }
}