Skip to main content

ag_ui/server/
state.rs

1//! Publishing shared state as snapshots or patches.
2//!
3//! The client keeps a copy of the agent's state. Sending the whole thing on
4//! every change is wasteful for a large document that gained one field, and
5//! sending a patch is wasteful for a small document that changed completely.
6//! [`StateManager`] keeps the last published snapshot and picks per publish:
7//!
8//! 1. the first publish is always a `STATE_SNAPSHOT` — the client's copy may
9//!    have drifted, and a patch against an unknown base is inapplicable;
10//! 2. afterwards it diffs against the last snapshot with [RFC 6902] and emits
11//!    `STATE_DELTA`;
12//! 3. unless the serialized patch is no smaller than the serialized snapshot,
13//!    in which case it snapshots instead.
14//!
15//! [RFC 6902]: https://datatracker.ietf.org/doc/html/rfc6902
16
17use crate::{Event, JsonPatch, PatchOperation};
18use serde_json::Value;
19
20use crate::server::agent::AgentState;
21use crate::server::emit::EventSink;
22use crate::server::error::Result;
23
24/// What a publish decided to send.
25#[derive(Clone, Debug, PartialEq)]
26pub enum StatePublish {
27    /// The whole state — the first publish, or a change too large to patch.
28    Snapshot(Value),
29    /// A patch against the previously published snapshot.
30    Delta(JsonPatch),
31    /// The state is byte-identical to the last publish; nothing to send.
32    Unchanged,
33}
34
35impl StatePublish {
36    /// The event to emit, or `None` for [`StatePublish::Unchanged`].
37    pub fn into_event(self) -> Option<Event> {
38        match self {
39            Self::Snapshot(value) => Some(Event::state_snapshot(value)),
40            Self::Delta(patch) => Some(Event::state_delta(patch)),
41            Self::Unchanged => None,
42        }
43    }
44}
45
46/// Tracks the last published state so changes can go out as patches.
47///
48/// ```
49/// # use ag_ui::PatchOperation;
50/// # use ag_ui::server::{StateManager, StatePublish};
51/// # use serde_json::json;
52/// let mut states = StateManager::new();
53/// let notes = "the document the user is editing, at some length";
54///
55/// // First publish: a snapshot, whatever the size.
56/// let first = states.publish(json!({"step": 1, "notes": notes}))?;
57/// assert!(matches!(first, StatePublish::Snapshot(_)));
58///
59/// // One field of a large document: a patch, because it is smaller.
60/// let second = states.publish(json!({"step": 2, "notes": notes}))?;
61/// assert_eq!(
62///     second,
63///     StatePublish::Delta(vec![PatchOperation::replace("/step", 2)])
64/// );
65///
66/// // A small document changing wholesale: back to a snapshot, because the
67/// // patch would be bigger than the state it describes.
68/// let mut small = StateManager::new();
69/// small.publish(json!({"a": 1}))?;
70/// assert_eq!(
71///     small.publish(json!({"b": 2}))?,
72///     StatePublish::Snapshot(json!({"b": 2}))
73/// );
74/// # Ok::<(), ag_ui::server::Error>(())
75/// ```
76#[derive(Clone, Debug, Default)]
77pub struct StateManager {
78    published: Option<Value>,
79}
80
81impl StateManager {
82    /// A manager that has published nothing yet.
83    pub fn new() -> Self {
84        Self::default()
85    }
86
87    /// The last published state, or `None` before the first publish.
88    pub fn published(&self) -> Option<&Value> {
89        self.published.as_ref()
90    }
91
92    /// Forgets the last publish, so the next one is a snapshot again.
93    ///
94    /// Call this after emitting a `STATE_SNAPSHOT` by hand, or after a
95    /// reconnect where the client's copy is no longer known.
96    pub fn reset(&mut self) {
97        self.published = None;
98    }
99
100    /// Decides how to publish `next` and records it as the new baseline.
101    ///
102    /// Returns [`StatePublish::Unchanged`] when nothing moved, so callers can
103    /// skip emitting entirely.
104    pub fn publish(&mut self, next: Value) -> Result<StatePublish> {
105        let Some(previous) = self.published.as_ref() else {
106            self.published = Some(next.clone());
107            return Ok(StatePublish::Snapshot(next));
108        };
109
110        if previous == &next {
111            return Ok(StatePublish::Unchanged);
112        }
113
114        let patch = diff(previous, &next)?;
115        // An empty patch with a non-equal value cannot happen for well-formed
116        // JSON, but treating it as "unchanged" is safer than emitting a
117        // STATE_DELTA the client would apply as a no-op.
118        if patch.is_empty() {
119            self.published = Some(next);
120            return Ok(StatePublish::Unchanged);
121        }
122
123        let patch_size = serde_json::to_vec(&patch)?.len();
124        let snapshot_size = serde_json::to_vec(&next)?.len();
125        self.published = Some(next.clone());
126
127        if patch_size < snapshot_size {
128            Ok(StatePublish::Delta(patch))
129        } else {
130            Ok(StatePublish::Snapshot(next))
131        }
132    }
133}
134
135/// A run's typed state together with the publish history that encodes it.
136///
137/// One cell rather than two fields on [`RunContext`](crate::server::RunContext),
138/// because an open handle borrows it *beside* the event sink: `&mut self.state`
139/// and `&mut self.sink` are disjoint borrows of the context, so a tool call can
140/// mutate and publish the state without holding a reference to the context
141/// itself — which is what keeps a second overlapping block a borrow-check
142/// error.
143#[derive(Debug)]
144pub(crate) struct RunState<S> {
145    value: S,
146    manager: StateManager,
147}
148
149impl<S> RunState<S> {
150    /// Wraps a decoded state, with nothing published yet.
151    pub(crate) fn new(value: S) -> Self {
152        Self {
153            value,
154            manager: StateManager::new(),
155        }
156    }
157
158    pub(crate) fn get(&self) -> &S {
159        &self.value
160    }
161
162    pub(crate) fn get_mut(&mut self) -> &mut S {
163        &mut self.value
164    }
165}
166
167impl<S: AgentState> RunState<S> {
168    /// Publishes whatever [`get_mut`](Self::get_mut) left behind.
169    pub(crate) fn publish(&mut self, sink: &mut EventSink) -> Result<()> {
170        let value = serde_json::to_value(&self.value)?;
171        self.publish_value(sink, value)
172    }
173
174    /// Replaces the value and publishes the change.
175    pub(crate) fn replace(&mut self, sink: &mut EventSink, value: &S) -> Result<()> {
176        let json = serde_json::to_value(value)?;
177        // Round-tripping keeps the typed view and the published snapshot in
178        // step without asking `S` to be `Clone`.
179        self.value = serde_json::from_value(json.clone())?;
180        self.publish_value(sink, json)
181    }
182
183    fn publish_value(&mut self, sink: &mut EventSink, value: Value) -> Result<()> {
184        match self.manager.publish(value)?.into_event() {
185            Some(event) => sink.emit(event),
186            None => Ok(()),
187        }
188    }
189}
190
191/// Computes an RFC 6902 patch and re-reads it as the protocol's own operation
192/// type.
193///
194/// `json-patch` has its own `PatchOperation` with `jsonptr` paths; the wire
195/// format is identical, so the round trip through `serde_json::Value` is the
196/// conversion. It costs one allocation per publish, which is nothing next to
197/// the diff itself.
198fn diff(previous: &Value, next: &Value) -> Result<JsonPatch> {
199    let patch = json_patch::diff(previous, next);
200    let operations: Vec<PatchOperation> = serde_json::from_value(serde_json::to_value(patch)?)?;
201    Ok(operations)
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use serde_json::json;
208
209    #[test]
210    fn first_publish_is_a_snapshot_even_when_tiny() {
211        let mut states = StateManager::new();
212        let published = states.publish(json!({"a": 1})).expect("publish failed");
213        assert_eq!(published, StatePublish::Snapshot(json!({"a": 1})));
214    }
215
216    #[test]
217    fn identical_state_publishes_nothing() {
218        let mut states = StateManager::new();
219        states.publish(json!({"a": 1})).expect("publish failed");
220        let second = states.publish(json!({"a": 1})).expect("publish failed");
221        assert_eq!(second, StatePublish::Unchanged);
222    }
223
224    #[test]
225    fn small_change_to_large_state_is_a_delta() {
226        let mut states = StateManager::new();
227        let big = json!({"notes": ["a".repeat(200)], "step": 1});
228        states.publish(big).expect("publish failed");
229        let published = states
230            .publish(json!({"notes": ["a".repeat(200)], "step": 2}))
231            .expect("publish failed");
232        assert_eq!(
233            published,
234            StatePublish::Delta(vec![PatchOperation::replace("/step", 2)])
235        );
236    }
237
238    #[test]
239    fn wholesale_change_falls_back_to_a_snapshot() {
240        let mut states = StateManager::new();
241        states.publish(json!({"a": 1})).expect("publish failed");
242        let next = json!({"b": 2});
243        let published = states.publish(next.clone()).expect("publish failed");
244        assert_eq!(published, StatePublish::Snapshot(next));
245    }
246
247    #[test]
248    fn reset_forces_the_next_publish_to_snapshot() {
249        let mut states = StateManager::new();
250        states
251            .publish(json!({"notes": ["a".repeat(200)], "step": 1}))
252            .expect("publish failed");
253        states.reset();
254        let next = json!({"notes": ["a".repeat(200)], "step": 2});
255        let published = states.publish(next.clone()).expect("publish failed");
256        assert_eq!(published, StatePublish::Snapshot(next));
257    }
258}