use monoloop_contracts::{
ToolCall, ToolCallContext, ToolCompletion, ToolExecutionId, ToolStartError,
};
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::sync::{oneshot, Notify};
use tokio::task::AbortHandle;
pub trait ToolHandler: Send + Sync {
fn start(
&self,
call: ToolCall,
context: ToolCallContext,
) -> Result<LinkedToolExecutionHandle, ToolStartError>;
fn supports_abort(&self) -> bool {
false
}
fn supports_isolated_kill(&self) -> bool {
false
}
}
#[derive(Clone, Debug)]
pub struct ToolExecutionControl {
cancelled: Arc<AtomicBool>,
notify: Arc<Notify>,
}
impl ToolExecutionControl {
pub fn new() -> Self {
Self {
cancelled: Arc::new(AtomicBool::new(false)),
notify: Arc::new(Notify::new()),
}
}
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::SeqCst);
self.notify.notify_waiters();
}
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::SeqCst)
}
pub async fn cancelled(&self) {
loop {
if self.is_cancelled() {
return;
}
self.notify.notified().await;
}
}
}
impl Default for ToolExecutionControl {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct ToolExecutionCompletion {
rx: oneshot::Receiver<ToolCompletion>,
}
impl ToolExecutionCompletion {
pub fn new(rx: oneshot::Receiver<ToolCompletion>) -> Self {
Self { rx }
}
pub async fn wait(self) -> ToolCompletion {
self.rx.await.unwrap_or(ToolCompletion::RuntimeFailed(
monoloop_contracts::ToolRuntimeError::CompletionLost,
))
}
}
#[derive(Clone, Debug)]
pub struct ToolKillHandle {
abort: AbortHandle,
}
impl ToolKillHandle {
pub fn new(abort: AbortHandle) -> Self {
Self { abort }
}
pub fn kill(&self) {
self.abort.abort();
}
}
#[derive(Debug)]
pub struct LinkedToolExecutionHandle {
pub execution_id: ToolExecutionId,
pub control: ToolExecutionControl,
pub completion: ToolExecutionCompletion,
pub kill: Option<ToolKillHandle>,
}
pub struct ImmediateToolHandler<F> {
f: F,
}
impl<F> ImmediateToolHandler<F>
where
F: Fn(ToolCall, ToolCallContext) -> Result<ToolCompletion, ToolStartError> + Send + Sync,
{
pub fn new(f: F) -> Self {
Self { f }
}
}
impl<F> ToolHandler for ImmediateToolHandler<F>
where
F: Fn(ToolCall, ToolCallContext) -> Result<ToolCompletion, ToolStartError> + Send + Sync,
{
fn start(
&self,
call: ToolCall,
context: ToolCallContext,
) -> Result<LinkedToolExecutionHandle, ToolStartError> {
let completion = (self.f)(call, context)?;
let (tx, rx) = oneshot::channel();
let _ = tx.send(completion);
Ok(LinkedToolExecutionHandle {
execution_id: ToolExecutionId::generate(),
control: ToolExecutionControl::new(),
completion: ToolExecutionCompletion::new(rx),
kill: None,
})
}
}
type BoxFut = Pin<Box<dyn Future<Output = ToolCompletion> + Send>>;
pub struct AsyncToolHandler<F> {
f: F,
}
impl<F> AsyncToolHandler<F>
where
F: Fn(ToolCall, ToolCallContext, ToolExecutionControl) -> BoxFut + Send + Sync,
{
pub fn new(f: F) -> Self {
Self { f }
}
}
impl<F> ToolHandler for AsyncToolHandler<F>
where
F: Fn(ToolCall, ToolCallContext, ToolExecutionControl) -> BoxFut + Send + Sync,
{
fn start(
&self,
call: ToolCall,
context: ToolCallContext,
) -> Result<LinkedToolExecutionHandle, ToolStartError> {
let control = ToolExecutionControl::new();
let control_body = control.clone();
let fut = (self.f)(call, context, control_body.clone());
let (tx, rx) = oneshot::channel();
let join = tokio::spawn(async move {
tokio::select! {
biased;
_ = control_body.cancelled() => {
let _ = tx.send(ToolCompletion::RuntimeFailed(
monoloop_contracts::ToolRuntimeError::TerminationFailed,
));
}
result = fut => {
let _ = tx.send(result);
}
}
});
let kill = ToolKillHandle::new(join.abort_handle());
Ok(LinkedToolExecutionHandle {
execution_id: ToolExecutionId::generate(),
control,
completion: ToolExecutionCompletion::new(rx),
kill: Some(kill),
})
}
fn supports_abort(&self) -> bool {
true
}
}
pub struct IsolatedKillableToolHandler<F> {
f: F,
}
impl<F> IsolatedKillableToolHandler<F>
where
F: Fn(ToolCall, ToolCallContext) -> BoxFut + Send + Sync,
{
pub fn new(f: F) -> Self {
Self { f }
}
}
impl<F> ToolHandler for IsolatedKillableToolHandler<F>
where
F: Fn(ToolCall, ToolCallContext) -> BoxFut + Send + Sync,
{
fn start(
&self,
call: ToolCall,
context: ToolCallContext,
) -> Result<LinkedToolExecutionHandle, ToolStartError> {
let control = ToolExecutionControl::new();
let fut = (self.f)(call, context);
let (tx, rx) = oneshot::channel();
let join = tokio::spawn(async move {
let result = fut.await;
let _ = tx.send(result);
});
let kill = ToolKillHandle::new(join.abort_handle());
Ok(LinkedToolExecutionHandle {
execution_id: ToolExecutionId::generate(),
control,
completion: ToolExecutionCompletion::new(rx),
kill: Some(kill),
})
}
fn supports_abort(&self) -> bool {
false
}
fn supports_isolated_kill(&self) -> bool {
true
}
}
#[derive(Debug, Default)]
pub struct StartFailHandler {
pub reason: &'static str,
}
impl ToolHandler for StartFailHandler {
fn start(
&self,
_call: ToolCall,
_context: ToolCallContext,
) -> Result<LinkedToolExecutionHandle, ToolStartError> {
Err(ToolStartError::Rejected(self.reason))
}
}
#[derive(Debug, Default)]
pub struct PanicOnStartHandler;
impl ToolHandler for PanicOnStartHandler {
fn start(
&self,
_call: ToolCall,
_context: ToolCallContext,
) -> Result<LinkedToolExecutionHandle, ToolStartError> {
panic!("deliberate tool panic");
}
}
#[derive(Debug, Default)]
pub struct LostCompletionHandler;
impl ToolHandler for LostCompletionHandler {
fn start(
&self,
_call: ToolCall,
_context: ToolCallContext,
) -> Result<LinkedToolExecutionHandle, ToolStartError> {
let (tx, rx) = oneshot::channel();
drop(tx);
Ok(LinkedToolExecutionHandle {
execution_id: ToolExecutionId::generate(),
control: ToolExecutionControl::new(),
completion: ToolExecutionCompletion::new(rx),
kill: None,
})
}
}