Skip to main content

monoloop_loop/transaction/
tool_handler.rs

1//! Linked tool handlers, execution handles, and cancellation controls.
2
3use monoloop_contracts::{
4    ToolCall, ToolCallContext, ToolCompletion, ToolExecutionId, ToolStartError,
5};
6use std::future::Future;
7use std::pin::Pin;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::Arc;
10use tokio::sync::{oneshot, Notify};
11use tokio::task::AbortHandle;
12
13/// Host-linked tool implementation.
14pub trait ToolHandler: Send + Sync {
15    /// Start one execution. Must return a handle with a single completion.
16    fn start(
17        &self,
18        call: ToolCall,
19        context: ToolCallContext,
20    ) -> Result<LinkedToolExecutionHandle, ToolStartError>;
21
22    /// Whether cooperative/abort cancellation is honored (D-024 / D-028).
23    /// Default **false** (fail-closed): capability booleans must not self-assert.
24    fn supports_abort(&self) -> bool {
25        false
26    }
27
28    /// Whether isolated kill after grace is available (D-024 / D-028).
29    /// Default **false** (fail-closed); requires a structural [`ToolKillHandle`].
30    fn supports_isolated_kill(&self) -> bool {
31        false
32    }
33}
34
35/// Cancellation control for a running linked tool.
36#[derive(Clone, Debug)]
37pub struct ToolExecutionControl {
38    cancelled: Arc<AtomicBool>,
39    notify: Arc<Notify>,
40}
41
42impl ToolExecutionControl {
43    /// Create a fresh control channel.
44    pub fn new() -> Self {
45        Self {
46            cancelled: Arc::new(AtomicBool::new(false)),
47            notify: Arc::new(Notify::new()),
48        }
49    }
50
51    /// Request cooperative/abort cancel (idempotent).
52    pub fn cancel(&self) {
53        self.cancelled.store(true, Ordering::SeqCst);
54        self.notify.notify_waiters();
55    }
56
57    /// Whether cancel was requested.
58    pub fn is_cancelled(&self) -> bool {
59        self.cancelled.load(Ordering::SeqCst)
60    }
61
62    /// Wait until cancelled.
63    pub async fn cancelled(&self) {
64        loop {
65            if self.is_cancelled() {
66                return;
67            }
68            self.notify.notified().await;
69        }
70    }
71}
72
73impl Default for ToolExecutionControl {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79/// One-shot completion consumer for a linked tool execution.
80#[derive(Debug)]
81pub struct ToolExecutionCompletion {
82    rx: oneshot::Receiver<ToolCompletion>,
83}
84
85impl ToolExecutionCompletion {
86    /// Wrap a receiver (exactly-once consumption via [`Self::wait`]).
87    pub fn new(rx: oneshot::Receiver<ToolCompletion>) -> Self {
88        Self { rx }
89    }
90
91    /// Await the single completion (or lost-completion if dropped).
92    pub async fn wait(self) -> ToolCompletion {
93        self.rx.await.unwrap_or(ToolCompletion::RuntimeFailed(
94            monoloop_contracts::ToolRuntimeError::CompletionLost,
95        ))
96    }
97}
98
99/// Force-stop handle for IsolatedKillable / Abortable workers (D-024).
100#[derive(Clone, Debug)]
101pub struct ToolKillHandle {
102    abort: AbortHandle,
103}
104
105impl ToolKillHandle {
106    /// Wrap a Tokio task abort handle.
107    pub fn new(abort: AbortHandle) -> Self {
108        Self { abort }
109    }
110
111    /// Abort the isolated worker task (idempotent).
112    pub fn kill(&self) {
113        self.abort.abort();
114    }
115}
116
117/// Handle returned from [`ToolHandler::start`].
118#[derive(Debug)]
119pub struct LinkedToolExecutionHandle {
120    /// Stable execution id for this start.
121    pub execution_id: ToolExecutionId,
122    /// Cancellation control.
123    pub control: ToolExecutionControl,
124    /// Exactly-once completion.
125    pub completion: ToolExecutionCompletion,
126    /// Optional kill handle for escalate-after-grace (D-024).
127    pub kill: Option<ToolKillHandle>,
128}
129
130/// Handler that completes immediately from a synchronous function.
131pub struct ImmediateToolHandler<F> {
132    f: F,
133}
134
135impl<F> ImmediateToolHandler<F>
136where
137    F: Fn(ToolCall, ToolCallContext) -> Result<ToolCompletion, ToolStartError> + Send + Sync,
138{
139    /// Construct from a function.
140    pub fn new(f: F) -> Self {
141        Self { f }
142    }
143}
144
145impl<F> ToolHandler for ImmediateToolHandler<F>
146where
147    F: Fn(ToolCall, ToolCallContext) -> Result<ToolCompletion, ToolStartError> + Send + Sync,
148{
149    fn start(
150        &self,
151        call: ToolCall,
152        context: ToolCallContext,
153    ) -> Result<LinkedToolExecutionHandle, ToolStartError> {
154        let completion = (self.f)(call, context)?;
155        let (tx, rx) = oneshot::channel();
156        let _ = tx.send(completion);
157        Ok(LinkedToolExecutionHandle {
158            execution_id: ToolExecutionId::generate(),
159            control: ToolExecutionControl::new(),
160            completion: ToolExecutionCompletion::new(rx),
161            kill: None,
162        })
163    }
164}
165
166type BoxFut = Pin<Box<dyn Future<Output = ToolCompletion> + Send>>;
167
168/// Handler that runs an async body with abortable cancellation.
169pub struct AsyncToolHandler<F> {
170    f: F,
171}
172
173impl<F> AsyncToolHandler<F>
174where
175    F: Fn(ToolCall, ToolCallContext, ToolExecutionControl) -> BoxFut + Send + Sync,
176{
177    /// Construct from a function that returns a boxed future.
178    pub fn new(f: F) -> Self {
179        Self { f }
180    }
181}
182
183impl<F> ToolHandler for AsyncToolHandler<F>
184where
185    F: Fn(ToolCall, ToolCallContext, ToolExecutionControl) -> BoxFut + Send + Sync,
186{
187    fn start(
188        &self,
189        call: ToolCall,
190        context: ToolCallContext,
191    ) -> Result<LinkedToolExecutionHandle, ToolStartError> {
192        let control = ToolExecutionControl::new();
193        let control_body = control.clone();
194        let fut = (self.f)(call, context, control_body.clone());
195        let (tx, rx) = oneshot::channel();
196        // Single owned worker: select cancel vs body (no detached watcher — LAW 23).
197        let join = tokio::spawn(async move {
198            tokio::select! {
199                biased;
200                _ = control_body.cancelled() => {
201                    let _ = tx.send(ToolCompletion::RuntimeFailed(
202                        monoloop_contracts::ToolRuntimeError::TerminationFailed,
203                    ));
204                }
205                result = fut => {
206                    let _ = tx.send(result);
207                }
208            }
209        });
210        let kill = ToolKillHandle::new(join.abort_handle());
211        Ok(LinkedToolExecutionHandle {
212            execution_id: ToolExecutionId::generate(),
213            control,
214            completion: ToolExecutionCompletion::new(rx),
215            kill: Some(kill),
216        })
217    }
218
219    fn supports_abort(&self) -> bool {
220        true
221    }
222}
223
224/// Isolated worker that ignores cooperative cancel until [`ToolKillHandle::kill`] (D-024 tests).
225pub struct IsolatedKillableToolHandler<F> {
226    f: F,
227}
228
229impl<F> IsolatedKillableToolHandler<F>
230where
231    F: Fn(ToolCall, ToolCallContext) -> BoxFut + Send + Sync,
232{
233    /// Construct from a function that returns a boxed future.
234    pub fn new(f: F) -> Self {
235        Self { f }
236    }
237}
238
239impl<F> ToolHandler for IsolatedKillableToolHandler<F>
240where
241    F: Fn(ToolCall, ToolCallContext) -> BoxFut + Send + Sync,
242{
243    fn start(
244        &self,
245        call: ToolCall,
246        context: ToolCallContext,
247    ) -> Result<LinkedToolExecutionHandle, ToolStartError> {
248        let control = ToolExecutionControl::new();
249        let fut = (self.f)(call, context);
250        let (tx, rx) = oneshot::channel();
251        let join = tokio::spawn(async move {
252            let result = fut.await;
253            let _ = tx.send(result);
254        });
255        let kill = ToolKillHandle::new(join.abort_handle());
256        Ok(LinkedToolExecutionHandle {
257            execution_id: ToolExecutionId::generate(),
258            control,
259            completion: ToolExecutionCompletion::new(rx),
260            kill: Some(kill),
261        })
262    }
263
264    fn supports_abort(&self) -> bool {
265        // Cooperative cancel alone does not stop this worker.
266        false
267    }
268
269    fn supports_isolated_kill(&self) -> bool {
270        true
271    }
272}
273
274/// Handler that always fails at start (tests).
275#[derive(Debug, Default)]
276pub struct StartFailHandler {
277    /// Rejection message.
278    pub reason: &'static str,
279}
280
281impl ToolHandler for StartFailHandler {
282    fn start(
283        &self,
284        _call: ToolCall,
285        _context: ToolCallContext,
286    ) -> Result<LinkedToolExecutionHandle, ToolStartError> {
287        Err(ToolStartError::Rejected(self.reason))
288    }
289}
290
291/// Handler that panics on start (tests).
292#[derive(Debug, Default)]
293pub struct PanicOnStartHandler;
294
295impl ToolHandler for PanicOnStartHandler {
296    fn start(
297        &self,
298        _call: ToolCall,
299        _context: ToolCallContext,
300    ) -> Result<LinkedToolExecutionHandle, ToolStartError> {
301        panic!("deliberate tool panic");
302    }
303}
304
305/// Handler whose completion is never sent (tests).
306#[derive(Debug, Default)]
307pub struct LostCompletionHandler;
308
309impl ToolHandler for LostCompletionHandler {
310    fn start(
311        &self,
312        _call: ToolCall,
313        _context: ToolCallContext,
314    ) -> Result<LinkedToolExecutionHandle, ToolStartError> {
315        let (tx, rx) = oneshot::channel();
316        drop(tx);
317        Ok(LinkedToolExecutionHandle {
318            execution_id: ToolExecutionId::generate(),
319            control: ToolExecutionControl::new(),
320            completion: ToolExecutionCompletion::new(rx),
321            kill: None,
322        })
323    }
324}