aither_core/llm/tool/
context.rs1use alloc::boxed::Box;
10use alloc::string::String;
11use core::fmt::{self, Debug, Display};
12use core::future::Future;
13use core::pin::Pin;
14
15#[derive(Debug, Clone, PartialEq)]
21pub struct Progress {
22 done: f64,
23 total: Option<f64>,
24 message: Option<String>,
25}
26
27impl Progress {
28 #[must_use]
30 pub const fn new(progress: f64) -> Self {
31 Self {
32 done: progress,
33 total: None,
34 message: None,
35 }
36 }
37
38 #[must_use]
40 pub const fn with_total(mut self, total: f64) -> Self {
41 self.total = Some(total);
42 self
43 }
44
45 #[must_use]
47 pub fn with_message(mut self, message: impl Into<String>) -> Self {
48 self.message = Some(message.into());
49 self
50 }
51
52 #[must_use]
54 pub const fn progress(&self) -> f64 {
55 self.done
56 }
57
58 #[must_use]
60 pub const fn total(&self) -> Option<f64> {
61 self.total
62 }
63
64 #[must_use]
66 pub fn message(&self) -> Option<&str> {
67 self.message.as_deref()
68 }
69}
70
71pub trait ProgressSink: Send + 'static {
79 fn report(&mut self, progress: Progress) -> impl Future<Output = ()> + Send;
86}
87
88trait ProgressSinkImpl: Send {
90 fn report(&mut self, progress: Progress) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
91}
92
93impl<S: ProgressSink> ProgressSinkImpl for S {
94 fn report(&mut self, progress: Progress) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
95 Box::pin(ProgressSink::report(self, progress))
96 }
97}
98
99#[derive(Debug, Clone, PartialEq)]
104pub enum ProgressError {
105 NotFinite(Progress),
107 NotIncreasing {
110 previous: f64,
112 reported: Progress,
114 },
115}
116
117impl Display for ProgressError {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 match self {
120 Self::NotFinite(progress) => write!(
121 f,
122 "progress report is not finite: progress {}, total {:?}",
123 progress.done, progress.total
124 ),
125 Self::NotIncreasing { previous, reported } => write!(
126 f,
127 "progress must increase with every report: {} does not exceed {previous}",
128 reported.done
129 ),
130 }
131 }
132}
133
134impl core::error::Error for ProgressError {}
135
136pub struct ToolContext {
143 sink: Option<Box<dyn ProgressSinkImpl>>,
144 last_progress: Option<f64>,
145}
146
147impl ToolContext {
148 #[must_use]
153 pub const fn new() -> Self {
154 Self {
155 sink: None,
156 last_progress: None,
157 }
158 }
159
160 #[must_use]
162 pub fn with_progress(sink: impl ProgressSink) -> Self {
163 Self {
164 sink: Some(Box::new(sink)),
165 last_progress: None,
166 }
167 }
168
169 #[must_use]
176 pub const fn is_listening(&self) -> bool {
177 self.sink.is_some()
178 }
179
180 pub async fn report_progress(&mut self, progress: Progress) -> Result<(), ProgressError> {
193 if !progress.done.is_finite() || progress.total.is_some_and(|total| !total.is_finite()) {
194 return Err(ProgressError::NotFinite(progress));
195 }
196 if let Some(previous) = self.last_progress
197 && progress.done <= previous
198 {
199 return Err(ProgressError::NotIncreasing {
200 previous,
201 reported: progress,
202 });
203 }
204 self.last_progress = Some(progress.done);
205 if let Some(sink) = &mut self.sink {
206 sink.report(progress).await;
207 }
208 Ok(())
209 }
210}
211
212impl Default for ToolContext {
213 fn default() -> Self {
214 Self::new()
215 }
216}
217
218impl Debug for ToolContext {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 f.debug_struct("ToolContext")
221 .field("listening", &self.sink.is_some())
222 .field("last_progress", &self.last_progress)
223 .finish()
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 extern crate std;
230
231 use super::*;
232 use alloc::vec::Vec;
233 use std::sync::mpsc;
234
235 struct Collect(mpsc::Sender<Progress>);
237
238 impl ProgressSink for Collect {
239 async fn report(&mut self, progress: Progress) {
240 self.0.send(progress).expect("test receiver alive");
241 }
242 }
243
244 fn listening() -> (ToolContext, mpsc::Receiver<Progress>) {
245 let (tx, rx) = mpsc::channel();
246 (ToolContext::with_progress(Collect(tx)), rx)
247 }
248
249 #[test]
250 fn reports_whether_the_caller_listens() {
251 assert!(!ToolContext::new().is_listening());
252 assert!(listening().0.is_listening());
253 }
254
255 #[tokio::test]
256 async fn delivers_increasing_reports_in_order() {
257 let (mut cx, seen) = listening();
258
259 cx.report_progress(Progress::new(1.0)).await.unwrap();
260 cx.report_progress(Progress::new(2.5).with_total(10.0).with_message("half"))
261 .await
262 .unwrap();
263
264 assert_eq!(
265 seen.try_iter().collect::<Vec<_>>(),
266 [
267 Progress::new(1.0),
268 Progress::new(2.5).with_total(10.0).with_message("half"),
269 ]
270 );
271 }
272
273 #[tokio::test]
274 async fn rejects_non_increasing_and_non_finite_reports() {
275 let (mut cx, seen) = listening();
276
277 cx.report_progress(Progress::new(3.0)).await.unwrap();
278 assert_eq!(
279 cx.report_progress(Progress::new(3.0)).await,
280 Err(ProgressError::NotIncreasing {
281 previous: 3.0,
282 reported: Progress::new(3.0),
283 })
284 );
285 assert!(matches!(
286 cx.report_progress(Progress::new(f64::NAN)).await,
287 Err(ProgressError::NotFinite(_))
288 ));
289 assert!(matches!(
290 cx.report_progress(Progress::new(4.0).with_total(f64::INFINITY))
291 .await,
292 Err(ProgressError::NotFinite(_))
293 ));
294 cx.report_progress(Progress::new(3.5)).await.unwrap();
296
297 assert_eq!(
298 seen.try_iter().collect::<Vec<_>>(),
299 [Progress::new(3.0), Progress::new(3.5)]
300 );
301 }
302
303 #[tokio::test]
304 async fn context_without_a_listener_still_validates() {
305 let mut cx = ToolContext::new();
306 cx.report_progress(Progress::new(1.0)).await.unwrap();
307 assert!(cx.report_progress(Progress::new(0.5)).await.is_err());
308 }
309}