Skip to main content

crackle_runtime/
task.rs

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