use syncable_ag_ui_core::{
patch::{apply_patch_from_value, Patch},
Event, JsonValue, Message,
};
use crate::error::{ClientError, Result};
pub type StateResult<T> = std::result::Result<T, ClientError>;
#[derive(Debug, Clone)]
pub struct StateReconstructor {
state: JsonValue,
messages: Vec<Message>,
run_id: Option<String>,
run_active: bool,
}
impl Default for StateReconstructor {
fn default() -> Self {
Self::new()
}
}
impl StateReconstructor {
pub fn new() -> Self {
Self {
state: JsonValue::Object(serde_json::Map::new()),
messages: Vec::new(),
run_id: None,
run_active: false,
}
}
pub fn with_state(initial: JsonValue) -> Self {
Self {
state: initial,
messages: Vec::new(),
run_id: None,
run_active: false,
}
}
pub fn current(&self) -> &JsonValue {
&self.state
}
pub fn into_state(self) -> JsonValue {
self.state
}
pub fn messages(&self) -> &[Message] {
&self.messages
}
pub fn run_id(&self) -> Option<&str> {
self.run_id.as_deref()
}
pub fn is_run_active(&self) -> bool {
self.run_active
}
pub fn apply_event(&mut self, event: &Event<JsonValue>) -> Result<()> {
match event {
Event::RunStarted(e) => {
self.run_id = Some(e.run_id.to_string());
self.run_active = true;
}
Event::RunFinished(_) | Event::RunError(_) => {
self.run_active = false;
}
Event::StateSnapshot(e) => {
self.state = e.snapshot.clone();
}
Event::StateDelta(e) => {
self.apply_delta(&e.delta)?;
}
Event::MessagesSnapshot(e) => {
self.messages = e.messages.clone();
}
_ => {}
}
Ok(())
}
pub fn apply_delta(&mut self, delta: &[JsonValue]) -> Result<()> {
let delta_array = JsonValue::Array(delta.to_vec());
apply_patch_from_value(&mut self.state, &delta_array)
.map_err(|e| ClientError::state(e.to_string()))
}
pub fn apply_patch(&mut self, patch: &Patch) -> Result<()> {
syncable_ag_ui_core::patch::apply_patch(&mut self.state, patch)
.map_err(|e| ClientError::state(e.to_string()))
}
pub fn reset(&mut self, state: JsonValue) {
self.state = state;
}
pub fn clear(&mut self) {
self.state = JsonValue::Object(serde_json::Map::new());
self.messages.clear();
self.run_id = None;
self.run_active = false;
}
pub fn get(&self, path: &str) -> Option<&JsonValue> {
self.state.pointer(path)
}
pub fn get_as<T: serde::de::DeserializeOwned>(&self, path: &str) -> Option<T> {
self.state
.pointer(path)
.and_then(|v| serde_json::from_value(v.clone()).ok())
}
}
#[cfg(test)]
mod tests {
use super::*;
use syncable_ag_ui_core::{
BaseEvent, MessageId, MessagesSnapshotEvent, RunId, RunStartedEvent, StateDeltaEvent,
StateSnapshotEvent, ThreadId,
};
use serde_json::json;
fn base_event() -> BaseEvent {
BaseEvent::new()
}
#[test]
fn test_new_state() {
let state = StateReconstructor::new();
assert!(state.current().is_object());
assert!(state.messages().is_empty());
assert!(!state.is_run_active());
}
#[test]
fn test_with_initial_state() {
let initial = json!({"count": 0});
let state = StateReconstructor::with_state(initial.clone());
assert_eq!(state.current(), &initial);
}
#[test]
fn test_apply_state_snapshot() {
let mut state = StateReconstructor::new();
let event = Event::StateSnapshot(StateSnapshotEvent {
base: base_event(),
snapshot: json!({"count": 42, "name": "test"}),
});
state.apply_event(&event).unwrap();
assert_eq!(state.current()["count"], 42);
assert_eq!(state.current()["name"], "test");
}
#[test]
fn test_apply_state_delta() {
let mut state = StateReconstructor::with_state(json!({"count": 0}));
let event = Event::StateDelta(StateDeltaEvent {
base: base_event(),
delta: vec![json!({
"op": "replace",
"path": "/count",
"value": 10
})],
});
state.apply_event(&event).unwrap();
assert_eq!(state.current()["count"], 10);
}
#[test]
fn test_apply_run_started() {
let mut state = StateReconstructor::new();
let run_id = RunId::random();
let run_id_str = run_id.to_string();
let event = Event::RunStarted(RunStartedEvent {
base: base_event(),
thread_id: ThreadId::random(),
run_id,
});
state.apply_event(&event).unwrap();
assert!(state.is_run_active());
assert_eq!(state.run_id(), Some(run_id_str.as_str()));
}
#[test]
fn test_apply_messages_snapshot() {
let mut state = StateReconstructor::new();
let msg = Message::Assistant {
id: MessageId::random(),
content: Some("Hello".to_string()),
name: None,
tool_calls: None,
};
let event = Event::MessagesSnapshot(MessagesSnapshotEvent {
base: base_event(),
messages: vec![msg],
});
state.apply_event(&event).unwrap();
assert_eq!(state.messages().len(), 1);
}
#[test]
fn test_get_by_path() {
let state = StateReconstructor::with_state(json!({
"user": {
"name": "Alice",
"age": 30
}
}));
assert_eq!(state.get("/user/name"), Some(&json!("Alice")));
assert_eq!(state.get("/user/age"), Some(&json!(30)));
assert_eq!(state.get("/nonexistent"), None);
}
#[test]
fn test_get_as_typed() {
let state = StateReconstructor::with_state(json!({
"count": 42,
"name": "test"
}));
let count: Option<i32> = state.get_as("/count");
assert_eq!(count, Some(42));
let name: Option<String> = state.get_as("/name");
assert_eq!(name, Some("test".to_string()));
let missing: Option<i32> = state.get_as("/missing");
assert_eq!(missing, None);
}
#[test]
fn test_clear() {
let mut state = StateReconstructor::with_state(json!({"count": 42}));
state.run_active = true;
state.run_id = Some("run1".to_string());
state.clear();
assert!(state.current().is_object());
assert!(state.current().as_object().unwrap().is_empty());
assert!(!state.is_run_active());
assert!(state.run_id().is_none());
}
#[test]
fn test_reset() {
let mut state = StateReconstructor::with_state(json!({"old": true}));
state.reset(json!({"new": true}));
assert_eq!(state.current(), &json!({"new": true}));
}
}