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
15use super::CallIdentity;
16
17#[derive(Debug, Clone, PartialEq)]
23pub struct Progress {
24 done: f64,
25 total: Option<f64>,
26 message: Option<String>,
27}
28
29impl Progress {
30 #[must_use]
32 pub const fn new(progress: f64) -> Self {
33 Self {
34 done: progress,
35 total: None,
36 message: None,
37 }
38 }
39
40 #[must_use]
42 pub const fn with_total(mut self, total: f64) -> Self {
43 self.total = Some(total);
44 self
45 }
46
47 #[must_use]
49 pub fn with_message(mut self, message: impl Into<String>) -> Self {
50 self.message = Some(message.into());
51 self
52 }
53
54 #[must_use]
56 pub const fn progress(&self) -> f64 {
57 self.done
58 }
59
60 #[must_use]
62 pub const fn total(&self) -> Option<f64> {
63 self.total
64 }
65
66 #[must_use]
68 pub fn message(&self) -> Option<&str> {
69 self.message.as_deref()
70 }
71}
72
73pub trait ProgressSink: Send + 'static {
81 fn report(&mut self, progress: Progress) -> impl Future<Output = ()> + Send;
88}
89
90trait ProgressSinkImpl: Send {
92 fn report(&mut self, progress: Progress) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
93}
94
95impl<S: ProgressSink> ProgressSinkImpl for S {
96 fn report(&mut self, progress: Progress) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
97 Box::pin(ProgressSink::report(self, progress))
98 }
99}
100
101#[derive(Debug, Clone, PartialEq)]
106pub enum ProgressError {
107 NotFinite(Progress),
109 NotIncreasing {
112 previous: f64,
114 reported: Progress,
116 },
117}
118
119impl Display for ProgressError {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 match self {
122 Self::NotFinite(progress) => write!(
123 f,
124 "progress report is not finite: progress {}, total {:?}",
125 progress.done, progress.total
126 ),
127 Self::NotIncreasing { previous, reported } => write!(
128 f,
129 "progress must increase with every report: {} does not exceed {previous}",
130 reported.done
131 ),
132 }
133 }
134}
135
136impl core::error::Error for ProgressError {}
137
138pub struct ToolContext {
145 sink: Option<Box<dyn ProgressSinkImpl>>,
146 last_progress: Option<f64>,
147 identity: Option<CallIdentity>,
148 working_dir: Option<String>,
149}
150
151impl ToolContext {
152 #[must_use]
157 pub const fn new() -> Self {
158 Self {
159 sink: None,
160 last_progress: None,
161 identity: None,
162 working_dir: None,
163 }
164 }
165
166 #[must_use]
168 pub fn with_progress(sink: impl ProgressSink) -> Self {
169 Self {
170 sink: Some(Box::new(sink)),
171 last_progress: None,
172 identity: None,
173 working_dir: None,
174 }
175 }
176
177 #[must_use]
179 pub fn with_working_dir(mut self, working_dir: impl Into<String>) -> Self {
180 self.working_dir = Some(working_dir.into());
181 self
182 }
183
184 #[must_use]
191 pub fn working_dir(&self) -> Option<&str> {
192 self.working_dir.as_deref()
193 }
194
195 #[must_use]
197 pub fn with_identity(mut self, identity: CallIdentity) -> Self {
198 self.identity = Some(identity);
199 self
200 }
201
202 #[must_use]
207 pub const fn identity(&self) -> Option<&CallIdentity> {
208 self.identity.as_ref()
209 }
210
211 #[must_use]
218 pub const fn is_listening(&self) -> bool {
219 self.sink.is_some()
220 }
221
222 pub async fn report_progress(&mut self, progress: Progress) -> Result<(), ProgressError> {
235 if !progress.done.is_finite() || progress.total.is_some_and(|total| !total.is_finite()) {
236 return Err(ProgressError::NotFinite(progress));
237 }
238 if let Some(previous) = self.last_progress
239 && progress.done <= previous
240 {
241 return Err(ProgressError::NotIncreasing {
242 previous,
243 reported: progress,
244 });
245 }
246 self.last_progress = Some(progress.done);
247 if let Some(sink) = &mut self.sink {
248 sink.report(progress).await;
249 }
250 Ok(())
251 }
252}
253
254impl Default for ToolContext {
255 fn default() -> Self {
256 Self::new()
257 }
258}
259
260impl Debug for ToolContext {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262 f.debug_struct("ToolContext")
263 .field("listening", &self.sink.is_some())
264 .field("last_progress", &self.last_progress)
265 .field("identity", &self.identity)
266 .field("working_dir", &self.working_dir)
267 .finish()
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 extern crate std;
274
275 use super::*;
276 use alloc::vec::Vec;
277 use std::sync::mpsc;
278
279 struct Collect(mpsc::Sender<Progress>);
281
282 impl ProgressSink for Collect {
283 async fn report(&mut self, progress: Progress) {
284 self.0.send(progress).expect("test receiver alive");
285 }
286 }
287
288 fn listening() -> (ToolContext, mpsc::Receiver<Progress>) {
289 let (tx, rx) = mpsc::channel();
290 (ToolContext::with_progress(Collect(tx)), rx)
291 }
292
293 #[test]
294 fn reports_whether_the_caller_listens() {
295 assert!(!ToolContext::new().is_listening());
296 assert!(listening().0.is_listening());
297 }
298
299 #[test]
300 fn carries_the_identity_it_was_given() {
301 use super::super::{AgentIdentity, ToolCallId};
302
303 assert_eq!(ToolContext::new().identity(), None);
304 let identity = CallIdentity::new(ToolCallId::new("call_1"), AgentIdentity::Main);
305 let cx = ToolContext::new().with_identity(identity.clone());
306 assert_eq!(cx.identity(), Some(&identity));
307 assert_eq!(identity.call().as_str(), "call_1");
308 }
309
310 #[tokio::test]
311 async fn delivers_increasing_reports_in_order() {
312 let (mut cx, seen) = listening();
313
314 cx.report_progress(Progress::new(1.0)).await.unwrap();
315 cx.report_progress(Progress::new(2.5).with_total(10.0).with_message("half"))
316 .await
317 .unwrap();
318
319 assert_eq!(
320 seen.try_iter().collect::<Vec<_>>(),
321 [
322 Progress::new(1.0),
323 Progress::new(2.5).with_total(10.0).with_message("half"),
324 ]
325 );
326 }
327
328 #[tokio::test]
329 async fn rejects_non_increasing_and_non_finite_reports() {
330 let (mut cx, seen) = listening();
331
332 cx.report_progress(Progress::new(3.0)).await.unwrap();
333 assert_eq!(
334 cx.report_progress(Progress::new(3.0)).await,
335 Err(ProgressError::NotIncreasing {
336 previous: 3.0,
337 reported: Progress::new(3.0),
338 })
339 );
340 assert!(matches!(
341 cx.report_progress(Progress::new(f64::NAN)).await,
342 Err(ProgressError::NotFinite(_))
343 ));
344 assert!(matches!(
345 cx.report_progress(Progress::new(4.0).with_total(f64::INFINITY))
346 .await,
347 Err(ProgressError::NotFinite(_))
348 ));
349 cx.report_progress(Progress::new(3.5)).await.unwrap();
351
352 assert_eq!(
353 seen.try_iter().collect::<Vec<_>>(),
354 [Progress::new(3.0), Progress::new(3.5)]
355 );
356 }
357
358 #[tokio::test]
359 async fn context_without_a_listener_still_validates() {
360 let mut cx = ToolContext::new();
361 cx.report_progress(Progress::new(1.0)).await.unwrap();
362 assert!(cx.report_progress(Progress::new(0.5)).await.is_err());
363 }
364}