Skip to main content

enact_core/callable/
callable.rs

1//! Callable trait definition
2//!
3//! The core abstraction for anything that can be invoked.
4
5use crate::streaming::StreamEvent;
6use async_trait::async_trait;
7use std::sync::Arc;
8use tokio::sync::mpsc;
9
10/// Core Callable trait - the fundamental execution unit
11///
12/// Everything that can be "run" implements this trait:
13/// - LLM agents
14/// - Graph nodes
15/// - Tools (when wrapped)
16/// - Flow compositions
17///
18/// ## Design Principles
19///
20/// 1. **Simple**: Just run with input, get output
21/// 2. **Async**: All execution is async by default
22/// 3. **Named**: Every callable has a name for logging/debugging
23/// 4. **Composable**: Callables can wrap other callables
24#[async_trait]
25pub trait Callable: Send + Sync {
26    /// Callable logical name (for logging, debugging, UI)
27    fn name(&self) -> &str;
28
29    /// Callable description (optional, for documentation)
30    fn description(&self) -> Option<&str> {
31        None
32    }
33
34    /// Execute the callable with the given input
35    ///
36    /// This is the core execution method. Implementations should:
37    /// - Be idempotent when possible
38    /// - Handle their own retries if needed
39    /// - Return meaningful error messages
40    async fn run(&self, input: &str) -> anyhow::Result<String>;
41
42    /// Execute with streaming events forwarded to the given sender.
43    ///
44    /// Default implementation ignores the sender and calls `run()`.
45    /// LLM/agent callables should override to poll their event emitter
46    /// and forward events to `event_tx` while running.
47    async fn run_streaming(
48        &self,
49        input: &str,
50        event_tx: mpsc::Sender<StreamEvent>,
51    ) -> anyhow::Result<String> {
52        let _ = event_tx;
53        self.run(input).await
54    }
55
56    /// Token usage from the last successful run, if available.
57    ///
58    /// LLM-backed callables may set this from provider usage metadata;
59    /// others return `None`. The runner uses this for accurate cost tracking
60    /// when present, otherwise falls back to estimates.
61    fn last_usage(&self) -> Option<crate::kernel::LlmTokenUsage> {
62        None
63    }
64}
65
66/// Boxed callable for dynamic dispatch
67///
68/// Use this when you need to store heterogeneous callables together
69/// or pass them through dynamic boundaries.
70pub type DynCallable = Arc<dyn Callable>;
71
72/// A simple function-based callable
73///
74/// Wraps a closure to implement Callable.
75#[allow(dead_code)]
76pub struct FnCallable<F>
77where
78    F: Fn(&str) -> anyhow::Result<String> + Send + Sync,
79{
80    name: String,
81    description: Option<String>,
82    func: F,
83}
84
85#[allow(dead_code)]
86impl<F> FnCallable<F>
87where
88    F: Fn(&str) -> anyhow::Result<String> + Send + Sync,
89{
90    /// Create a new function-based callable
91    pub fn new(name: impl Into<String>, func: F) -> Self {
92        Self {
93            name: name.into(),
94            description: None,
95            func,
96        }
97    }
98
99    /// Add a description
100    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
101        self.description = Some(desc.into());
102        self
103    }
104}
105
106#[async_trait]
107impl<F> Callable for FnCallable<F>
108where
109    F: Fn(&str) -> anyhow::Result<String> + Send + Sync,
110{
111    fn name(&self) -> &str {
112        &self.name
113    }
114
115    fn description(&self) -> Option<&str> {
116        self.description.as_deref()
117    }
118
119    async fn run(&self, input: &str) -> anyhow::Result<String> {
120        (self.func)(input)
121    }
122}
123
124/// Create a callable from an async function
125#[allow(dead_code)]
126pub struct AsyncFnCallable<F, Fut>
127where
128    F: Fn(String) -> Fut + Send + Sync,
129    Fut: std::future::Future<Output = anyhow::Result<String>> + Send + Sync,
130{
131    name: String,
132    description: Option<String>,
133    func: F,
134    _phantom: std::marker::PhantomData<fn() -> Fut>,
135}
136
137#[allow(dead_code)]
138impl<F, Fut> AsyncFnCallable<F, Fut>
139where
140    F: Fn(String) -> Fut + Send + Sync,
141    Fut: std::future::Future<Output = anyhow::Result<String>> + Send + Sync,
142{
143    /// Create a new async function-based callable
144    pub fn new(name: impl Into<String>, func: F) -> Self {
145        Self {
146            name: name.into(),
147            description: None,
148            func,
149            _phantom: std::marker::PhantomData,
150        }
151    }
152
153    /// Add a description
154    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
155        self.description = Some(desc.into());
156        self
157    }
158}
159
160#[async_trait]
161impl<F, Fut> Callable for AsyncFnCallable<F, Fut>
162where
163    F: Fn(String) -> Fut + Send + Sync,
164    Fut: std::future::Future<Output = anyhow::Result<String>> + Send + Sync,
165{
166    fn name(&self) -> &str {
167        &self.name
168    }
169
170    fn description(&self) -> Option<&str> {
171        self.description.as_deref()
172    }
173
174    async fn run(&self, input: &str) -> anyhow::Result<String> {
175        (self.func)(input.to_string()).await
176    }
177}