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}