1use serde::{Deserialize, Serialize};
6use serde_json::{Value, json};
7use std::collections::HashMap;
8use std::sync::Arc;
9
10pub type State = HashMap<String, Value>;
12
13#[derive(Clone)]
15pub enum Reducer {
16 Overwrite,
18 Append,
20 Sum,
22 Custom(Arc<dyn Fn(Value, Value) -> Value + Send + Sync>),
24}
25
26#[allow(clippy::derivable_impls)]
28impl Default for Reducer {
29 fn default() -> Self {
30 Self::Overwrite
31 }
32}
33
34impl std::fmt::Debug for Reducer {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 match self {
37 Self::Overwrite => write!(f, "Overwrite"),
38 Self::Append => write!(f, "Append"),
39 Self::Sum => write!(f, "Sum"),
40 Self::Custom(_) => write!(f, "Custom"),
41 }
42 }
43}
44
45#[derive(Clone)]
47pub struct Channel {
48 pub name: String,
50 pub reducer: Reducer,
52 pub default: Option<Value>,
54}
55
56impl Channel {
57 pub fn new(name: &str) -> Self {
59 Self { name: name.to_string(), reducer: Reducer::Overwrite, default: None }
60 }
61
62 pub fn list(name: &str) -> Self {
64 Self { name: name.to_string(), reducer: Reducer::Append, default: Some(json!([])) }
65 }
66
67 pub fn counter(name: &str) -> Self {
69 Self { name: name.to_string(), reducer: Reducer::Sum, default: Some(json!(0)) }
70 }
71
72 pub fn with_reducer(mut self, reducer: Reducer) -> Self {
74 self.reducer = reducer;
75 self
76 }
77
78 pub fn with_default(mut self, default: Value) -> Self {
80 self.default = Some(default);
81 self
82 }
83}
84
85#[derive(Clone, Default)]
87pub struct StateSchema {
88 pub channels: HashMap<String, Channel>,
90}
91
92impl StateSchema {
93 pub fn new() -> Self {
95 Self::default()
96 }
97
98 pub fn builder() -> StateSchemaBuilder {
100 StateSchemaBuilder::default()
101 }
102
103 pub fn simple(channels: &[&str]) -> Self {
105 let mut schema = Self::new();
106 for name in channels {
107 schema.channels.insert((*name).to_string(), Channel::new(name));
108 }
109 schema
110 }
111
112 pub fn get_reducer(&self, channel: &str) -> &Reducer {
114 self.channels.get(channel).map(|c| &c.reducer).unwrap_or(&Reducer::Overwrite)
115 }
116
117 pub fn first_undeclared<'a>(&self, keys: impl IntoIterator<Item = &'a str>) -> Option<&'a str> {
132 if self.channels.is_empty() {
133 return None;
134 }
135 keys.into_iter().find(|key| !self.channels.contains_key(*key))
136 }
137
138 pub fn get_default(&self, channel: &str) -> Option<&Value> {
140 self.channels.get(channel).and_then(|c| c.default.as_ref())
141 }
142
143 pub fn apply_update(&self, state: &mut State, key: &str, value: Value) {
145 let reducer = self.get_reducer(key);
146 let current = state.get(key).cloned().unwrap_or(Value::Null);
147
148 let new_value = match reducer {
149 Reducer::Overwrite => value,
150 Reducer::Append => {
151 let mut arr = match current {
152 Value::Array(a) => a,
153 Value::Null => vec![],
154 _ => vec![current],
155 };
156 match value {
157 Value::Array(items) => arr.extend(items),
158 _ => arr.push(value),
159 }
160 Value::Array(arr)
161 }
162 Reducer::Sum => {
163 let current_num = current.as_f64().unwrap_or(0.0);
164 let add_num = value.as_f64().unwrap_or(0.0);
165 json!(current_num + add_num)
166 }
167 Reducer::Custom(f) => f(current, value),
168 };
169
170 state.insert(key.to_string(), new_value);
171 }
172
173 pub fn initialize_state(&self) -> State {
175 let mut state = State::new();
176 for (name, channel) in &self.channels {
177 if let Some(default) = &channel.default {
178 state.insert(name.clone(), default.clone());
179 }
180 }
181 state
182 }
183}
184
185#[derive(Default)]
187pub struct StateSchemaBuilder {
188 channels: HashMap<String, Channel>,
189}
190
191impl StateSchemaBuilder {
192 pub fn channel(mut self, name: &str) -> Self {
194 self.channels.insert(name.to_string(), Channel::new(name));
195 self
196 }
197
198 pub fn list_channel(mut self, name: &str) -> Self {
200 self.channels.insert(name.to_string(), Channel::list(name));
201 self
202 }
203
204 pub fn counter_channel(mut self, name: &str) -> Self {
206 self.channels.insert(name.to_string(), Channel::counter(name));
207 self
208 }
209
210 pub fn channel_with_reducer(mut self, name: &str, reducer: Reducer) -> Self {
212 self.channels.insert(name.to_string(), Channel::new(name).with_reducer(reducer));
213 self
214 }
215
216 pub fn channel_with_default(mut self, name: &str, default: Value) -> Self {
218 self.channels.insert(name.to_string(), Channel::new(name).with_default(default));
219 self
220 }
221
222 pub fn build(self) -> StateSchema {
224 StateSchema { channels: self.channels }
225 }
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct Checkpoint {
231 pub thread_id: String,
233 pub checkpoint_id: String,
235 pub state: State,
237 pub step: usize,
239 pub pending_nodes: Vec<String>,
241 pub metadata: HashMap<String, Value>,
243 pub created_at: chrono::DateTime<chrono::Utc>,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub cleared_interrupt: Option<String>,
254 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
259 pub attempts: HashMap<String, u32>,
260 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
267 pub child_ledger: HashMap<String, Value>,
268}
269
270impl Checkpoint {
271 pub fn new(thread_id: &str, state: State, step: usize, pending_nodes: Vec<String>) -> Self {
273 Self {
274 thread_id: thread_id.to_string(),
275 checkpoint_id: uuid::Uuid::new_v4().to_string(),
276 state,
277 step,
278 pending_nodes,
279 metadata: HashMap::new(),
280 created_at: chrono::Utc::now(),
281 cleared_interrupt: None,
282 attempts: HashMap::new(),
283 child_ledger: HashMap::new(),
284 }
285 }
286
287 pub fn with_cleared_interrupt(mut self, node: impl Into<String>) -> Self {
289 self.cleared_interrupt = Some(node.into());
290 self
291 }
292
293 pub fn with_metadata(mut self, key: &str, value: Value) -> Self {
295 self.metadata.insert(key.to_string(), value);
296 self
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn test_overwrite_reducer() {
306 let schema = StateSchema::simple(&["value"]);
307 let mut state = State::new();
308
309 schema.apply_update(&mut state, "value", json!(1));
310 assert_eq!(state.get("value"), Some(&json!(1)));
311
312 schema.apply_update(&mut state, "value", json!(2));
313 assert_eq!(state.get("value"), Some(&json!(2)));
314 }
315
316 #[test]
317 fn test_append_reducer() {
318 let schema = StateSchema::builder().list_channel("messages").build();
319 let mut state = schema.initialize_state();
320
321 schema.apply_update(&mut state, "messages", json!({"role": "user", "content": "hi"}));
322 assert_eq!(state.get("messages"), Some(&json!([{"role": "user", "content": "hi"}])));
323
324 schema.apply_update(
325 &mut state,
326 "messages",
327 json!([{"role": "assistant", "content": "hello"}]),
328 );
329 assert_eq!(
330 state.get("messages"),
331 Some(&json!([
332 {"role": "user", "content": "hi"},
333 {"role": "assistant", "content": "hello"}
334 ]))
335 );
336 }
337
338 #[test]
339 fn test_sum_reducer() {
340 let schema = StateSchema::builder().counter_channel("count").build();
341 let mut state = schema.initialize_state();
342
343 assert_eq!(state.get("count"), Some(&json!(0)));
344
345 schema.apply_update(&mut state, "count", json!(5));
346 assert_eq!(state.get("count"), Some(&json!(5.0)));
347
348 schema.apply_update(&mut state, "count", json!(3));
349 assert_eq!(state.get("count"), Some(&json!(8.0)));
350 }
351
352 #[test]
353 fn test_custom_reducer() {
354 let schema = StateSchema::builder()
355 .channel_with_reducer(
356 "max",
357 Reducer::Custom(Arc::new(|a, b| {
358 let a_num = a.as_f64().unwrap_or(f64::MIN);
359 let b_num = b.as_f64().unwrap_or(f64::MIN);
360 json!(a_num.max(b_num))
361 })),
362 )
363 .build();
364 let mut state = State::new();
365
366 schema.apply_update(&mut state, "max", json!(5));
367 schema.apply_update(&mut state, "max", json!(3));
368 schema.apply_update(&mut state, "max", json!(8));
369 assert_eq!(state.get("max"), Some(&json!(8.0)));
370 }
371}