Skip to main content

crackle_runtime/
task.rs

1use std::time::{Duration, SystemTime, UNIX_EPOCH};
2#[cfg(feature = "serde")]
3use serde::{Serialize, Deserialize};
4
5/// A timestamp for when a task was fired or cooled.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
8pub struct Timestamp(u64);
9
10impl Timestamp {
11    /// Create a timestamp from milliseconds since epoch.
12    pub fn from_millis(millis: u64) -> Self {
13        Timestamp(millis)
14    }
15
16    /// Create a timestamp representing "now".
17    pub fn now() -> Self {
18        Timestamp(
19            SystemTime::now()
20                .duration_since(UNIX_EPOCH)
21                .unwrap_or_default()
22                .as_millis() as u64,
23        )
24    }
25
26    /// Get the milliseconds since epoch.
27    pub fn as_millis(&self) -> u64 {
28        self.0
29    }
30
31    /// Duration elapsed since this timestamp.
32    pub fn elapsed(&self) -> Duration {
33        let now = Timestamp::now();
34        Duration::from_millis(now.0.saturating_sub(self.0))
35    }
36}
37
38/// Metadata about a task's execution.
39#[derive(Debug, Clone)]
40#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
41pub struct TaskMetadata {
42    /// When the task was fired.
43    pub fired_at: Timestamp,
44    /// When the task's cooling phase completed.
45    pub cooled_at: Option<Timestamp>,
46    /// How long the firing phase took.
47    pub fire_duration: Duration,
48    /// A user-defined label for this task.
49    pub label: String,
50}
51
52impl TaskMetadata {
53    /// Create new metadata with the given label.
54    pub fn new(label: impl Into<String>) -> Self {
55        TaskMetadata {
56            fired_at: Timestamp::now(),
57            cooled_at: None,
58            fire_duration: Duration::ZERO,
59            label: label.into(),
60        }
61    }
62
63    /// Create metadata with a specific fired-at timestamp.
64    pub fn fired_at(label: impl Into<String>, ts: Timestamp) -> Self {
65        TaskMetadata {
66            fired_at: ts,
67            cooled_at: None,
68            fire_duration: Duration::ZERO,
69            label: label.into(),
70        }
71    }
72}
73
74/// The output of firing a task, including named metrics for pattern detection.
75#[derive(Debug, Clone)]
76#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
77pub struct TaskOutput<T> {
78    /// The primary output value.
79    pub value: T,
80    /// Named metrics extracted during firing, used by pattern detectors.
81    pub metrics: Vec<(String, f64)>,
82}
83
84impl<T> TaskOutput<T> {
85    /// Create a new task output with a value and metrics.
86    pub fn new(value: T, metrics: Vec<(String, f64)>) -> Self {
87        TaskOutput { value, metrics }
88    }
89
90    /// Create a task output with no metrics.
91    pub fn simple(value: T) -> Self {
92        TaskOutput {
93            value,
94            metrics: vec![],
95        }
96    }
97
98    /// Add a named metric.
99    pub fn with_metric(mut self, name: impl Into<String>, value: f64) -> Self {
100        self.metrics.push((name.into(), value));
101        self
102    }
103}
104
105/// The core trait for tasks in the crackle runtime.
106///
107/// Every task has two phases:
108/// - **Firing** (`fire`): Hot execution. Do the work. Produce output.
109/// - **Cooling** (`cool`): Post-completion reflection. Examine what happened
110///   across all completed tasks and contribute observations.
111///
112/// The crackle glaze forms in the cooling, not the firing. Beauty arrives in the descent.
113pub trait CrackleTask {
114    /// The type of value produced by firing.
115    type Output;
116
117    /// Execute the task (the hot phase).
118    ///
119    /// This is where the primary work happens. Return the output along with
120    /// named metrics that pattern detectors can analyze during cooling.
121    fn fire(&self) -> TaskOutput<Self::Output>;
122
123    /// Reflect on the task during cooling (optional).
124    ///
125    /// Receive the output from firing and a snapshot of all completed task metrics.
126    /// Return any additional observations or modified metrics.
127    ///
128    /// The default implementation does nothing — many tasks don't need to
129    /// participate actively in cooling. The runtime detects patterns regardless.
130    fn cool(
131        &self,
132        _output: &TaskOutput<Self::Output>,
133        _all_metrics: &[(String, Vec<(String, f64)>)],
134    ) -> Vec<(String, f64)> {
135        vec![]
136    }
137
138    /// A human-readable label for this task (used in pattern descriptions).
139    fn label(&self) -> String {
140        "anonymous task".to_string()
141    }
142}