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}