1use std::sync::Arc;
2
3use tokio::sync::oneshot;
4use uuid::Uuid;
5
6use crate::error::RuntimeError;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct PromptId(pub Uuid);
12
13impl PromptId {
14 pub fn now() -> Self {
15 Self(Uuid::now_v7())
16 }
17}
18
19impl std::fmt::Display for PromptId {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 self.0.fmt(f)
22 }
23}
24
25pub trait PromptResolver: Send + Sync {
26 fn register(&self, id: PromptId) -> oneshot::Receiver<serde_json::Value>;
27 fn drop_pending(&self, id: &PromptId);
28
29 fn expire_pending(&self, id: &PromptId) -> bool {
30 self.drop_pending(id);
31 true
32 }
33
34 fn register_with_payload(
35 &self,
36 id: PromptId,
37 _kind: &str,
38 _payload: serde_json::Value,
39 ) -> oneshot::Receiver<serde_json::Value> {
40 self.register(id)
41 }
42}
43
44pub struct AutoResolveResolver {
47 pub default: serde_json::Value,
48}
49
50impl PromptResolver for AutoResolveResolver {
51 fn register(&self, _id: PromptId) -> oneshot::Receiver<serde_json::Value> {
52 let (tx, rx) = oneshot::channel();
53 let _ = tx.send(self.default.clone());
54 rx
55 }
56 fn drop_pending(&self, _id: &PromptId) {}
57}
58
59pub async fn await_prompt(
60 resolver: &Arc<dyn PromptResolver>,
61 id: PromptId,
62 timeout: std::time::Duration,
63) -> Result<serde_json::Value, RuntimeError> {
64 await_prompt_inner(resolver.register(id), resolver, id, timeout).await
65}
66
67pub async fn await_prompt_with_payload(
68 resolver: &Arc<dyn PromptResolver>,
69 id: PromptId,
70 kind: &str,
71 payload: serde_json::Value,
72 timeout: std::time::Duration,
73) -> Result<serde_json::Value, RuntimeError> {
74 await_prompt_inner(
75 resolver.register_with_payload(id, kind, payload),
76 resolver,
77 id,
78 timeout,
79 )
80 .await
81}
82
83pub async fn await_prompt_with_payload_cancel(
84 resolver: &Arc<dyn PromptResolver>,
85 id: PromptId,
86 kind: &str,
87 payload: serde_json::Value,
88 timeout: std::time::Duration,
89 cancel: &tokio_util::sync::CancellationToken,
90) -> Result<serde_json::Value, RuntimeError> {
91 await_prompt_inner_cancel(
92 resolver.register_with_payload(id, kind, payload),
93 resolver,
94 id,
95 timeout,
96 cancel,
97 )
98 .await
99}
100
101pub async fn await_expirable_prompt_with_payload(
102 resolver: &Arc<dyn PromptResolver>,
103 id: PromptId,
104 kind: &str,
105 payload: serde_json::Value,
106 timeout: std::time::Duration,
107) -> Result<serde_json::Value, RuntimeError> {
108 let mut rx = resolver.register_with_payload(id, kind, payload);
109 match tokio::time::timeout(timeout, &mut rx).await {
110 Ok(Ok(value)) => Ok(value),
111 Ok(Err(_)) => {
112 resolver.drop_pending(&id);
113 Err(RuntimeError::ToolFailed(format!(
114 "prompt {id} channel closed before answer"
115 )))
116 }
117 Err(_) => {
118 if resolver.expire_pending(&id) {
119 Err(RuntimeError::ToolFailed(format!(
120 "prompt {id} timed out after {}s",
121 timeout.as_secs()
122 )))
123 } else {
124 rx.await.map_err(|_| {
125 RuntimeError::ToolFailed(format!("prompt {id} channel closed before answer"))
126 })
127 }
128 }
129 }
130}
131
132pub async fn await_expirable_prompt_with_payload_cancel(
133 resolver: &Arc<dyn PromptResolver>,
134 id: PromptId,
135 kind: &str,
136 payload: serde_json::Value,
137 timeout: std::time::Duration,
138 cancel: &tokio_util::sync::CancellationToken,
139) -> Result<Option<serde_json::Value>, RuntimeError> {
140 let mut rx = resolver.register_with_payload(id, kind, payload);
141 tokio::select! {
142 result = tokio::time::timeout(timeout, &mut rx) => match result {
143 Ok(Ok(value)) => Ok(Some(value)),
144 Ok(Err(_)) => {
145 resolver.drop_pending(&id);
146 Err(RuntimeError::ToolFailed(format!(
147 "prompt {id} channel closed before answer"
148 )))
149 }
150 Err(_) => {
151 if resolver.expire_pending(&id) {
152 Ok(None)
153 } else {
154 tokio::select! {
155 result = &mut rx => result.map(Some).map_err(|_| {
156 RuntimeError::ToolFailed(format!(
157 "prompt {id} channel closed before answer"
158 ))
159 }),
160 _ = cancel.cancelled() => {
161 resolver.drop_pending(&id);
162 Err(RuntimeError::Cancelled(format!("prompt {id} cancelled")))
163 }
164 }
165 }
166 }
167 },
168 _ = cancel.cancelled() => {
169 resolver.drop_pending(&id);
170 Err(RuntimeError::Cancelled(format!("prompt {id} cancelled")))
171 }
172 }
173}
174
175async fn await_prompt_inner(
176 rx: oneshot::Receiver<serde_json::Value>,
177 resolver: &Arc<dyn PromptResolver>,
178 id: PromptId,
179 timeout: std::time::Duration,
180) -> Result<serde_json::Value, RuntimeError> {
181 match tokio::time::timeout(timeout, rx).await {
182 Ok(Ok(v)) => Ok(v),
183 Ok(Err(_)) => {
184 resolver.drop_pending(&id);
185 Err(RuntimeError::ToolFailed(format!(
186 "prompt {id} channel closed before answer"
187 )))
188 }
189 Err(_) => {
190 resolver.drop_pending(&id);
191 Err(RuntimeError::ToolFailed(format!(
192 "prompt {id} timed out after {}s",
193 timeout.as_secs()
194 )))
195 }
196 }
197}
198
199async fn await_prompt_inner_cancel(
200 mut rx: oneshot::Receiver<serde_json::Value>,
201 resolver: &Arc<dyn PromptResolver>,
202 id: PromptId,
203 timeout: std::time::Duration,
204 cancel: &tokio_util::sync::CancellationToken,
205) -> Result<serde_json::Value, RuntimeError> {
206 tokio::select! {
207 result = tokio::time::timeout(timeout, &mut rx) => match result {
208 Ok(Ok(value)) => Ok(value),
209 Ok(Err(_)) => {
210 resolver.drop_pending(&id);
211 Err(RuntimeError::ToolFailed(format!(
212 "prompt {id} channel closed before answer"
213 )))
214 }
215 Err(_) => {
216 resolver.drop_pending(&id);
217 Err(RuntimeError::ToolFailed(format!(
218 "prompt {id} timed out after {}s",
219 timeout.as_secs()
220 )))
221 }
222 },
223 _ = cancel.cancelled() => {
224 resolver.drop_pending(&id);
225 Err(RuntimeError::Cancelled(format!("prompt {id} cancelled")))
226 }
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use std::sync::Mutex;
234 use std::sync::atomic::{AtomicBool, Ordering};
235
236 struct PendingResolver {
237 sender: Mutex<Option<oneshot::Sender<serde_json::Value>>>,
238 dropped: AtomicBool,
239 expired: AtomicBool,
240 }
241
242 impl PendingResolver {
243 fn new() -> Self {
244 Self {
245 sender: Mutex::new(None),
246 dropped: AtomicBool::new(false),
247 expired: AtomicBool::new(false),
248 }
249 }
250 }
251
252 impl PromptResolver for PendingResolver {
253 fn register(&self, _id: PromptId) -> oneshot::Receiver<serde_json::Value> {
254 let (tx, rx) = oneshot::channel();
255 *self.sender.lock().unwrap() = Some(tx);
256 rx
257 }
258
259 fn drop_pending(&self, _id: &PromptId) {
260 self.dropped.store(true, Ordering::SeqCst);
261 self.sender.lock().unwrap().take();
262 }
263
264 fn expire_pending(&self, _id: &PromptId) -> bool {
265 self.expired.store(true, Ordering::SeqCst);
266 self.sender.lock().unwrap().take();
267 true
268 }
269 }
270
271 #[tokio::test]
272 async fn expirable_prompt_timeout_is_not_a_tool_error() {
273 let concrete = Arc::new(PendingResolver::new());
274 let resolver: Arc<dyn PromptResolver> = concrete.clone();
275 let result = await_expirable_prompt_with_payload_cancel(
276 &resolver,
277 PromptId::now(),
278 "form_ask",
279 serde_json::Value::Null,
280 std::time::Duration::from_millis(1),
281 &tokio_util::sync::CancellationToken::new(),
282 )
283 .await
284 .unwrap();
285
286 assert!(result.is_none());
287 assert!(concrete.expired.load(Ordering::SeqCst));
288 assert!(!concrete.dropped.load(Ordering::SeqCst));
289 }
290
291 #[tokio::test]
292 async fn cancelling_prompt_drops_it_immediately() {
293 let concrete = Arc::new(PendingResolver::new());
294 let resolver: Arc<dyn PromptResolver> = concrete.clone();
295 let cancel = tokio_util::sync::CancellationToken::new();
296 cancel.cancel();
297 let result = await_prompt_with_payload_cancel(
298 &resolver,
299 PromptId::now(),
300 "form_ask",
301 serde_json::Value::Null,
302 std::time::Duration::from_secs(300),
303 &cancel,
304 )
305 .await;
306
307 assert!(matches!(result, Err(RuntimeError::Cancelled(_))));
308 assert!(concrete.dropped.load(Ordering::SeqCst));
309 }
310}