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}