monoloop_loop/transaction/
tool_handler.rs1use monoloop_contracts::{
4 ToolCall, ToolCallContext, ToolCompletion, ToolExecutionId, ToolStartError,
5};
6use std::future::Future;
7use std::pin::Pin;
8use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
9use std::sync::{Arc, Mutex};
10use tokio::sync::{oneshot, Notify};
11
12#[derive(Debug)]
14pub(crate) struct OwnedProcessLease {
15 counter: Arc<AtomicU32>,
16}
17
18impl OwnedProcessLease {
19 fn acquire(counter: Arc<AtomicU32>) -> Self {
20 counter.fetch_add(1, Ordering::SeqCst);
21 Self { counter }
22 }
23}
24
25impl Drop for OwnedProcessLease {
26 fn drop(&mut self) {
27 self.counter.fetch_sub(1, Ordering::SeqCst);
28 }
29}
30
31pub trait ToolHandler: Send + Sync {
33 fn start(
35 &self,
36 call: ToolCall,
37 context: ToolCallContext,
38 ) -> Result<LinkedToolExecutionHandle, ToolStartError>;
39
40 fn supports_abort(&self) -> bool {
43 false
44 }
45
46 fn supports_isolated_kill(&self) -> bool {
50 false
51 }
52
53 fn os_process_isolated(&self) -> bool {
58 false
59 }
60
61 fn runtime_owns_abortable_drive(&self) -> bool {
68 false
69 }
70}
71
72mod abortable_seal {
73 pub trait Sealed {}
75}
76
77pub trait AbortableAtYieldHandler: ToolHandler + abortable_seal::Sealed {}
82
83#[derive(Clone, Debug)]
85pub struct ToolExecutionControl {
86 cancelled: Arc<AtomicBool>,
87 notify: Arc<Notify>,
88}
89
90impl ToolExecutionControl {
91 pub fn new() -> Self {
93 Self {
94 cancelled: Arc::new(AtomicBool::new(false)),
95 notify: Arc::new(Notify::new()),
96 }
97 }
98
99 pub fn cancel(&self) {
101 self.cancelled.store(true, Ordering::SeqCst);
102 self.notify.notify_waiters();
103 }
104
105 pub fn is_cancelled(&self) -> bool {
107 self.cancelled.load(Ordering::SeqCst)
108 }
109
110 pub async fn cancelled(&self) {
112 loop {
113 if self.is_cancelled() {
114 return;
115 }
116 self.notify.notified().await;
117 }
118 }
119}
120
121impl Default for ToolExecutionControl {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127#[derive(Debug)]
129pub struct ToolExecutionCompletion {
130 rx: oneshot::Receiver<ToolCompletion>,
131}
132
133impl ToolExecutionCompletion {
134 pub fn new(rx: oneshot::Receiver<ToolCompletion>) -> Self {
136 Self { rx }
137 }
138
139 pub async fn wait(self) -> ToolCompletion {
141 self.rx.await.unwrap_or(ToolCompletion::RuntimeFailed(
142 monoloop_contracts::ToolRuntimeError::CompletionLost,
143 ))
144 }
145}
146
147#[derive(Clone, Debug)]
152pub struct ToolKillHandle {
153 inner: Arc<KillInner>,
154}
155
156#[derive(Debug)]
157enum KillInner {
158 CancelOnly { control: ToolExecutionControl },
163 Process {
165 child: Arc<Mutex<Option<tokio::process::Child>>>,
166 owned_slot: Mutex<Option<OwnedProcessLease>>,
168 },
169}
170
171impl ToolKillHandle {
172 pub fn cancel_only(control: ToolExecutionControl) -> Self {
177 Self {
178 inner: Arc::new(KillInner::CancelOnly { control }),
179 }
180 }
181
182 pub(crate) fn from_process(child: Arc<Mutex<Option<tokio::process::Child>>>) -> Self {
188 Self {
189 inner: Arc::new(KillInner::Process {
190 child,
191 owned_slot: Mutex::new(None),
192 }),
193 }
194 }
195
196 pub fn register_owned_process(&self, counter: Arc<AtomicU32>) {
200 let KillInner::Process { owned_slot, .. } = &*self.inner else {
201 return;
202 };
203 let mut slot = owned_slot.lock().unwrap_or_else(|e| e.into_inner());
204 if slot.is_none() {
205 *slot = Some(OwnedProcessLease::acquire(counter));
206 }
207 }
208
209 pub fn note_process_reaped(&self) {
211 let KillInner::Process { owned_slot, .. } = &*self.inner else {
212 return;
213 };
214 let _ = owned_slot.lock().unwrap_or_else(|e| e.into_inner()).take();
215 }
216
217 #[allow(dead_code)] pub(crate) fn take_process_lease(&self) -> Option<OwnedProcessLease> {
220 let KillInner::Process { owned_slot, .. } = &*self.inner else {
221 return None;
222 };
223 owned_slot.lock().unwrap_or_else(|e| e.into_inner()).take()
224 }
225
226 pub fn kill(&self) {
228 match &*self.inner {
229 KillInner::CancelOnly { control } => control.cancel(),
230 KillInner::Process { child, .. } => {
231 if let Some(c) = child.lock().unwrap_or_else(|e| e.into_inner()).as_mut() {
232 let _ = c.start_kill();
233 }
234 }
235 }
236 }
237
238 pub async fn join_timeout(&self, budget: std::time::Duration) -> Result<(), ()> {
241 match &*self.inner {
242 KillInner::CancelOnly { .. } => {
243 Ok(())
245 }
246 KillInner::Process { child, owned_slot } => {
247 let deadline = std::time::Instant::now() + budget;
249 loop {
250 let done = {
251 let mut guard = child.lock().unwrap_or_else(|e| e.into_inner());
252 match guard.as_mut() {
253 Some(c) => match c.try_wait() {
254 Ok(Some(_)) => {
255 let _ = guard.take();
256 true
257 }
258 Ok(None) => false,
259 Err(_) => true,
260 },
261 None => true,
262 }
263 };
264 if done {
265 let _ = owned_slot.lock().unwrap_or_else(|e| e.into_inner()).take();
266 return Ok(());
267 }
268 if std::time::Instant::now() >= deadline {
269 return Err(());
270 }
271 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
272 }
273 }
274 }
275 }
276
277 pub fn has_join(&self) -> bool {
279 match &*self.inner {
280 KillInner::Process { child, owned_slot } => {
281 if Self::process_still_alive(child) {
283 true
284 } else {
285 let _ = owned_slot.lock().unwrap_or_else(|e| e.into_inner()).take();
286 false
287 }
288 }
289 KillInner::CancelOnly { .. } => false,
290 }
291 }
292
293 fn process_still_alive(child: &Mutex<Option<tokio::process::Child>>) -> bool {
294 let mut guard = child.lock().unwrap_or_else(|e| e.into_inner());
295 match guard.as_mut() {
296 Some(c) => match c.try_wait() {
297 Ok(None) => true,
298 Ok(Some(_)) => {
299 let _ = guard.take();
301 false
302 }
303 Err(_) => true, },
305 None => false,
306 }
307 }
308
309 pub fn is_process_isolated(&self) -> bool {
311 matches!(&*self.inner, KillInner::Process { .. })
312 }
313
314 pub fn os_pid(&self) -> Option<u32> {
318 match &*self.inner {
319 KillInner::Process { child, .. } => {
320 let guard = child.lock().unwrap_or_else(|e| e.into_inner());
321 guard.as_ref().and_then(|c| c.id())
322 }
323 KillInner::CancelOnly { .. } => None,
324 }
325 }
326
327 pub fn is_cancel_only(&self) -> bool {
329 matches!(&*self.inner, KillInner::CancelOnly { .. })
330 }
331}
332
333pub struct LinkedToolExecutionHandle {
335 pub execution_id: ToolExecutionId,
337 pub control: ToolExecutionControl,
339 pub completion: ToolExecutionCompletion,
341 pub kill: Option<ToolKillHandle>,
343 pub drive: Option<Pin<Box<dyn Future<Output = ()> + Send>>>,
346}
347
348impl std::fmt::Debug for LinkedToolExecutionHandle {
349 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350 f.debug_struct("LinkedToolExecutionHandle")
351 .field("execution_id", &self.execution_id)
352 .field("control", &self.control)
353 .field("completion", &self.completion)
354 .field("kill", &self.kill)
355 .field("drive", &self.drive.as_ref().map(|_| "<drive>"))
356 .finish()
357 }
358}
359
360pub struct ImmediateToolHandler<F> {
362 f: F,
363}
364
365impl<F> ImmediateToolHandler<F>
366where
367 F: Fn(ToolCall, ToolCallContext) -> Result<ToolCompletion, ToolStartError> + Send + Sync,
368{
369 pub fn new(f: F) -> Self {
371 Self { f }
372 }
373}
374
375impl<F> ToolHandler for ImmediateToolHandler<F>
376where
377 F: Fn(ToolCall, ToolCallContext) -> Result<ToolCompletion, ToolStartError> + Send + Sync,
378{
379 fn start(
380 &self,
381 call: ToolCall,
382 context: ToolCallContext,
383 ) -> Result<LinkedToolExecutionHandle, ToolStartError> {
384 let completion = (self.f)(call, context)?;
385 let (tx, rx) = oneshot::channel();
386 let _ = tx.send(completion);
387 Ok(LinkedToolExecutionHandle {
388 execution_id: ToolExecutionId::generate(),
389 control: ToolExecutionControl::new(),
390 completion: ToolExecutionCompletion::new(rx),
391 kill: None,
392 drive: None,
393 })
394 }
395}
396
397type BoxFut = Pin<Box<dyn Future<Output = ToolCompletion> + Send>>;
398
399pub struct AsyncToolHandler<F> {
405 f: F,
406}
407
408impl<F> AsyncToolHandler<F>
409where
410 F: Fn(ToolCall, ToolCallContext, ToolExecutionControl) -> BoxFut + Send + Sync,
411{
412 pub fn new(f: F) -> Self {
414 Self { f }
415 }
416}
417
418impl<F> ToolHandler for AsyncToolHandler<F>
419where
420 F: Fn(ToolCall, ToolCallContext, ToolExecutionControl) -> BoxFut + Send + Sync,
421{
422 fn start(
423 &self,
424 call: ToolCall,
425 context: ToolCallContext,
426 ) -> Result<LinkedToolExecutionHandle, ToolStartError> {
427 let control = ToolExecutionControl::new();
428 let control_body = control.clone();
429 let fut = (self.f)(call, context, control_body.clone());
430 let (tx, rx) = oneshot::channel();
431 let drive = Box::pin(async move {
433 tokio::select! {
434 biased;
435 _ = control_body.cancelled() => {
436 let _ = tx.send(ToolCompletion::RuntimeFailed(
437 monoloop_contracts::ToolRuntimeError::TerminationFailed,
438 ));
439 }
440 result = fut => {
441 let _ = tx.send(result);
442 }
443 }
444 });
445 let kill = ToolKillHandle::cancel_only(control.clone());
446 Ok(LinkedToolExecutionHandle {
447 execution_id: ToolExecutionId::generate(),
448 control,
449 completion: ToolExecutionCompletion::new(rx),
450 kill: Some(kill),
451 drive: Some(drive),
452 })
453 }
454
455 fn supports_abort(&self) -> bool {
456 true
457 }
458
459 fn runtime_owns_abortable_drive(&self) -> bool {
460 true
461 }
462}
463
464impl<F> abortable_seal::Sealed for AsyncToolHandler<F> where
465 F: Fn(ToolCall, ToolCallContext, ToolExecutionControl) -> BoxFut + Send + Sync
466{
467}
468
469impl<F> AbortableAtYieldHandler for AsyncToolHandler<F> where
470 F: Fn(ToolCall, ToolCallContext, ToolExecutionControl) -> BoxFut + Send + Sync
471{
472}
473
474pub struct IsolatedKillableToolHandler<F> {
480 f: F,
481}
482
483impl<F> IsolatedKillableToolHandler<F>
484where
485 F: Fn(ToolCall, ToolCallContext) -> BoxFut + Send + Sync,
486{
487 pub fn new(f: F) -> Self {
489 Self { f }
490 }
491}
492
493impl<F> ToolHandler for IsolatedKillableToolHandler<F>
494where
495 F: Fn(ToolCall, ToolCallContext) -> BoxFut + Send + Sync,
496{
497 fn start(
498 &self,
499 call: ToolCall,
500 context: ToolCallContext,
501 ) -> Result<LinkedToolExecutionHandle, ToolStartError> {
502 let control = ToolExecutionControl::new();
503 let control_body = control.clone();
504 let fut = (self.f)(call, context);
505 let (tx, rx) = oneshot::channel();
506 let drive = Box::pin(async move {
509 let _ = control_body;
510 let result = fut.await;
511 let _ = tx.send(result);
512 });
513 let kill = ToolKillHandle::cancel_only(control.clone());
514 Ok(LinkedToolExecutionHandle {
515 execution_id: ToolExecutionId::generate(),
516 control,
517 completion: ToolExecutionCompletion::new(rx),
518 kill: Some(kill),
519 drive: Some(drive),
520 })
521 }
522
523 fn supports_abort(&self) -> bool {
524 true
526 }
527
528 fn supports_isolated_kill(&self) -> bool {
529 false
531 }
532
533 fn runtime_owns_abortable_drive(&self) -> bool {
534 true
535 }
536}
537
538impl<F> abortable_seal::Sealed for IsolatedKillableToolHandler<F> where
539 F: Fn(ToolCall, ToolCallContext) -> BoxFut + Send + Sync
540{
541}
542
543impl<F> AbortableAtYieldHandler for IsolatedKillableToolHandler<F> where
544 F: Fn(ToolCall, ToolCallContext) -> BoxFut + Send + Sync
545{
546}
547
548#[derive(Debug, Default)]
550pub struct StartFailHandler {
551 pub reason: &'static str,
553}
554
555impl ToolHandler for StartFailHandler {
556 fn start(
557 &self,
558 _call: ToolCall,
559 _context: ToolCallContext,
560 ) -> Result<LinkedToolExecutionHandle, ToolStartError> {
561 Err(ToolStartError::Rejected(self.reason))
562 }
563}
564
565#[derive(Debug, Default)]
567pub struct PanicOnStartHandler;
568
569impl ToolHandler for PanicOnStartHandler {
570 fn start(
571 &self,
572 _call: ToolCall,
573 _context: ToolCallContext,
574 ) -> Result<LinkedToolExecutionHandle, ToolStartError> {
575 panic!("deliberate tool panic");
576 }
577}
578
579#[derive(Debug, Default)]
581pub struct LostCompletionHandler;
582
583impl ToolHandler for LostCompletionHandler {
584 fn start(
585 &self,
586 _call: ToolCall,
587 _context: ToolCallContext,
588 ) -> Result<LinkedToolExecutionHandle, ToolStartError> {
589 let (tx, rx) = oneshot::channel();
590 drop(tx);
591 Ok(LinkedToolExecutionHandle {
592 execution_id: ToolExecutionId::generate(),
593 control: ToolExecutionControl::new(),
594 completion: ToolExecutionCompletion::new(rx),
595 kill: None,
596 drive: None,
597 })
598 }
599}