vv_agent/app_server/
thread_state.rs1use std::collections::{HashMap, HashSet, VecDeque};
2use std::sync::{Arc, Mutex as StdMutex};
3
4use tokio::sync::Mutex;
5
6use crate::app_server::protocol::{AppTurn, UserInput};
7use crate::app_server::transport::ConnectionId;
8use crate::runtime::state_v2::CheckpointStoreV2;
9use crate::RunHandle;
10
11pub type SteeringQueue = Arc<StdMutex<VecDeque<Vec<UserInput>>>>;
12
13#[derive(Clone, Default)]
14pub struct ThreadStateManager {
15 inner: Arc<Mutex<ThreadStateInner>>,
16}
17
18#[derive(Default)]
19struct ThreadStateInner {
20 subscribers: HashMap<String, HashSet<ConnectionId>>,
21 active_turns: HashMap<String, ActiveTurn>,
22 durable_resumes: HashMap<String, String>,
23 pending_approvals: HashMap<String, PendingApproval>,
24 follow_ups: HashMap<String, VecDeque<Vec<UserInput>>>,
25 closed_threads: HashSet<String>,
26}
27
28#[derive(Clone)]
29pub struct ActiveTurn {
30 pub turn: AppTurn,
31 pub handle: RunHandle,
32 pub steering: SteeringQueue,
33 pub owner_connection_id: ConnectionId,
34 pub checkpoint_store: Option<Arc<dyn CheckpointStoreV2>>,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct PendingApproval {
39 pub connection_id: ConnectionId,
40 pub turn_id: String,
41 pub request_id: String,
42}
43
44impl ThreadStateManager {
45 pub async fn subscribe(&self, thread_id: impl Into<String>, connection_id: ConnectionId) {
46 let thread_id = thread_id.into();
47 let mut inner = self.inner.lock().await;
48 inner.closed_threads.remove(&thread_id);
49 inner
50 .subscribers
51 .entry(thread_id)
52 .or_default()
53 .insert(connection_id);
54 }
55
56 pub async fn subscribe_and_snapshot<T, E>(
57 &self,
58 thread_id: impl Into<String>,
59 connection_id: ConnectionId,
60 snapshot: impl FnOnce() -> Result<T, E>,
61 ) -> Result<T, E> {
62 let thread_id = thread_id.into();
63 let mut inner = self.inner.lock().await;
64 let was_closed = inner.closed_threads.remove(&thread_id);
65 let inserted = inner
66 .subscribers
67 .entry(thread_id.clone())
68 .or_default()
69 .insert(connection_id);
70 let result = snapshot();
71 if result.is_err() {
72 if inserted {
73 let remove_entry =
74 inner
75 .subscribers
76 .get_mut(&thread_id)
77 .is_some_and(|subscribers| {
78 subscribers.remove(&connection_id);
79 subscribers.is_empty()
80 });
81 if remove_entry {
82 inner.subscribers.remove(&thread_id);
83 }
84 }
85 if was_closed {
86 inner.closed_threads.insert(thread_id);
87 }
88 }
89 result
90 }
91
92 pub async fn unsubscribe(&self, thread_id: &str, connection_id: ConnectionId) -> bool {
93 let mut inner = self.inner.lock().await;
94 if let Some(subscribers) = inner.subscribers.get_mut(thread_id) {
95 subscribers.remove(&connection_id);
96 if subscribers.is_empty() {
97 inner.subscribers.remove(thread_id);
98 }
99 }
100 let closed = !inner.subscribers.contains_key(thread_id)
101 && !inner.active_turns.contains_key(thread_id)
102 && !inner.durable_resumes.contains_key(thread_id);
103 if closed {
104 inner.closed_threads.insert(thread_id.to_string());
105 }
106 closed
107 }
108
109 pub async fn unsubscribe_connection(&self, connection_id: ConnectionId) {
110 let mut inner = self.inner.lock().await;
111 inner.subscribers.retain(|_thread_id, subscribers| {
112 subscribers.remove(&connection_id);
113 !subscribers.is_empty()
114 });
115 }
116
117 pub async fn subscribers(&self, thread_id: &str) -> Vec<ConnectionId> {
118 self.inner
119 .lock()
120 .await
121 .subscribers
122 .get(thread_id)
123 .map(|subscribers| subscribers.iter().copied().collect())
124 .unwrap_or_default()
125 }
126
127 pub async fn is_subscribed(&self, thread_id: &str, connection_id: ConnectionId) -> bool {
128 self.inner
129 .lock()
130 .await
131 .subscribers
132 .get(thread_id)
133 .is_some_and(|subscribers| subscribers.contains(&connection_id))
134 }
135
136 pub async fn is_closed(&self, thread_id: &str) -> bool {
137 self.inner.lock().await.closed_threads.contains(thread_id)
138 }
139
140 pub async fn reopen(&self, thread_id: &str) {
141 self.inner.lock().await.closed_threads.remove(thread_id);
142 }
143
144 pub async fn set_active_turn(&self, thread_id: impl Into<String>, active_turn: ActiveTurn) {
145 let thread_id = thread_id.into();
146 let mut inner = self.inner.lock().await;
147 inner.closed_threads.remove(&thread_id);
148 inner.active_turns.insert(thread_id, active_turn);
149 }
150
151 pub async fn active_turn(&self, thread_id: &str) -> Option<ActiveTurn> {
152 self.inner.lock().await.active_turns.get(thread_id).cloned()
153 }
154
155 pub async fn active_turn_id(&self, thread_id: &str) -> Option<String> {
156 let inner = self.inner.lock().await;
157 inner
158 .active_turns
159 .get(thread_id)
160 .map(|active| active.turn.turn_id.clone())
161 .or_else(|| inner.durable_resumes.get(thread_id).cloned())
162 }
163
164 pub async fn has_active_turn(&self, thread_id: &str) -> bool {
165 let inner = self.inner.lock().await;
166 inner.active_turns.contains_key(thread_id) || inner.durable_resumes.contains_key(thread_id)
167 }
168
169 pub async fn set_durable_resume(
170 &self,
171 thread_id: impl Into<String>,
172 turn_id: impl Into<String>,
173 ) {
174 let thread_id = thread_id.into();
175 let mut inner = self.inner.lock().await;
176 inner.closed_threads.remove(&thread_id);
177 inner.durable_resumes.insert(thread_id, turn_id.into());
178 }
179
180 pub async fn clear_durable_resume(&self, thread_id: &str, turn_id: &str) {
181 let mut inner = self.inner.lock().await;
182 if inner
183 .durable_resumes
184 .get(thread_id)
185 .is_some_and(|active_turn_id| active_turn_id == turn_id)
186 {
187 inner.durable_resumes.remove(thread_id);
188 }
189 }
190
191 pub async fn clear_active_turn(&self, thread_id: &str, turn_id: &str) {
192 let mut inner = self.inner.lock().await;
193 if inner
194 .active_turns
195 .get(thread_id)
196 .is_some_and(|active| active.turn.turn_id == turn_id)
197 {
198 inner.active_turns.remove(thread_id);
199 }
200 }
201
202 pub async fn queue_follow_up(&self, thread_id: &str, input: Vec<UserInput>) {
203 self.inner
204 .lock()
205 .await
206 .follow_ups
207 .entry(thread_id.to_string())
208 .or_default()
209 .push_back(input);
210 }
211
212 pub async fn pop_follow_up(&self, thread_id: &str) -> Option<Vec<UserInput>> {
213 let mut inner = self.inner.lock().await;
214 let next = inner
215 .follow_ups
216 .get_mut(thread_id)
217 .and_then(VecDeque::pop_front);
218 if inner
219 .follow_ups
220 .get(thread_id)
221 .is_some_and(VecDeque::is_empty)
222 {
223 inner.follow_ups.remove(thread_id);
224 }
225 next
226 }
227
228 pub async fn set_pending_approval(
229 &self,
230 thread_id: impl Into<String>,
231 turn_id: impl Into<String>,
232 request_id: impl Into<String>,
233 connection_id: ConnectionId,
234 ) {
235 self.inner.lock().await.pending_approvals.insert(
236 thread_id.into(),
237 PendingApproval {
238 connection_id,
239 turn_id: turn_id.into(),
240 request_id: request_id.into(),
241 },
242 );
243 }
244
245 pub async fn pending_approval(&self, thread_id: &str) -> Option<PendingApproval> {
246 self.inner
247 .lock()
248 .await
249 .pending_approvals
250 .get(thread_id)
251 .cloned()
252 }
253
254 pub async fn clear_pending_approval(&self, thread_id: &str, request_id: &str) {
255 let mut inner = self.inner.lock().await;
256 if inner
257 .pending_approvals
258 .get(thread_id)
259 .is_some_and(|pending| pending.request_id == request_id)
260 {
261 inner.pending_approvals.remove(thread_id);
262 }
263 }
264}