use monoloop_contracts::{
ToolCall, ToolCallContext, ToolCompletion, ToolExecutionId, ToolStartError,
};
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
use tokio::sync::{oneshot, Notify};
#[derive(Debug)]
pub(crate) struct OwnedProcessLease {
counter: Arc<AtomicU32>,
}
impl OwnedProcessLease {
fn acquire(counter: Arc<AtomicU32>) -> Self {
counter.fetch_add(1, Ordering::SeqCst);
Self { counter }
}
}
impl Drop for OwnedProcessLease {
fn drop(&mut self) {
self.counter.fetch_sub(1, Ordering::SeqCst);
}
}
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
}
fn os_process_isolated(&self) -> bool {
false
}
fn runtime_owns_abortable_drive(&self) -> bool {
false
}
}
mod abortable_seal {
pub trait Sealed {}
}
pub trait AbortableAtYieldHandler: ToolHandler + abortable_seal::Sealed {}
#[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 {
inner: Arc<KillInner>,
}
#[derive(Debug)]
enum KillInner {
CancelOnly { control: ToolExecutionControl },
Process {
child: Arc<Mutex<Option<tokio::process::Child>>>,
owned_slot: Mutex<Option<OwnedProcessLease>>,
},
}
impl ToolKillHandle {
pub fn cancel_only(control: ToolExecutionControl) -> Self {
Self {
inner: Arc::new(KillInner::CancelOnly { control }),
}
}
pub(crate) fn from_process(child: Arc<Mutex<Option<tokio::process::Child>>>) -> Self {
Self {
inner: Arc::new(KillInner::Process {
child,
owned_slot: Mutex::new(None),
}),
}
}
pub fn register_owned_process(&self, counter: Arc<AtomicU32>) {
let KillInner::Process { owned_slot, .. } = &*self.inner else {
return;
};
let mut slot = owned_slot.lock().unwrap_or_else(|e| e.into_inner());
if slot.is_none() {
*slot = Some(OwnedProcessLease::acquire(counter));
}
}
pub fn note_process_reaped(&self) {
let KillInner::Process { owned_slot, .. } = &*self.inner else {
return;
};
let _ = owned_slot.lock().unwrap_or_else(|e| e.into_inner()).take();
}
#[allow(dead_code)] pub(crate) fn take_process_lease(&self) -> Option<OwnedProcessLease> {
let KillInner::Process { owned_slot, .. } = &*self.inner else {
return None;
};
owned_slot.lock().unwrap_or_else(|e| e.into_inner()).take()
}
pub fn kill(&self) {
match &*self.inner {
KillInner::CancelOnly { control } => control.cancel(),
KillInner::Process { child, .. } => {
if let Some(c) = child.lock().unwrap_or_else(|e| e.into_inner()).as_mut() {
let _ = c.start_kill();
}
}
}
}
pub async fn join_timeout(&self, budget: std::time::Duration) -> Result<(), ()> {
match &*self.inner {
KillInner::CancelOnly { .. } => {
Ok(())
}
KillInner::Process { child, owned_slot } => {
let deadline = std::time::Instant::now() + budget;
loop {
let done = {
let mut guard = child.lock().unwrap_or_else(|e| e.into_inner());
match guard.as_mut() {
Some(c) => match c.try_wait() {
Ok(Some(_)) => {
let _ = guard.take();
true
}
Ok(None) => false,
Err(_) => true,
},
None => true,
}
};
if done {
let _ = owned_slot.lock().unwrap_or_else(|e| e.into_inner()).take();
return Ok(());
}
if std::time::Instant::now() >= deadline {
return Err(());
}
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
}
}
}
pub fn has_join(&self) -> bool {
match &*self.inner {
KillInner::Process { child, owned_slot } => {
if Self::process_still_alive(child) {
true
} else {
let _ = owned_slot.lock().unwrap_or_else(|e| e.into_inner()).take();
false
}
}
KillInner::CancelOnly { .. } => false,
}
}
fn process_still_alive(child: &Mutex<Option<tokio::process::Child>>) -> bool {
let mut guard = child.lock().unwrap_or_else(|e| e.into_inner());
match guard.as_mut() {
Some(c) => match c.try_wait() {
Ok(None) => true,
Ok(Some(_)) => {
let _ = guard.take();
false
}
Err(_) => true, },
None => false,
}
}
pub fn is_process_isolated(&self) -> bool {
matches!(&*self.inner, KillInner::Process { .. })
}
pub fn os_pid(&self) -> Option<u32> {
match &*self.inner {
KillInner::Process { child, .. } => {
let guard = child.lock().unwrap_or_else(|e| e.into_inner());
guard.as_ref().and_then(|c| c.id())
}
KillInner::CancelOnly { .. } => None,
}
}
pub fn is_cancel_only(&self) -> bool {
matches!(&*self.inner, KillInner::CancelOnly { .. })
}
}
pub struct LinkedToolExecutionHandle {
pub execution_id: ToolExecutionId,
pub control: ToolExecutionControl,
pub completion: ToolExecutionCompletion,
pub kill: Option<ToolKillHandle>,
pub drive: Option<Pin<Box<dyn Future<Output = ()> + Send>>>,
}
impl std::fmt::Debug for LinkedToolExecutionHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LinkedToolExecutionHandle")
.field("execution_id", &self.execution_id)
.field("control", &self.control)
.field("completion", &self.completion)
.field("kill", &self.kill)
.field("drive", &self.drive.as_ref().map(|_| "<drive>"))
.finish()
}
}
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,
drive: 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 drive = Box::pin(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::cancel_only(control.clone());
Ok(LinkedToolExecutionHandle {
execution_id: ToolExecutionId::generate(),
control,
completion: ToolExecutionCompletion::new(rx),
kill: Some(kill),
drive: Some(drive),
})
}
fn supports_abort(&self) -> bool {
true
}
fn runtime_owns_abortable_drive(&self) -> bool {
true
}
}
impl<F> abortable_seal::Sealed for AsyncToolHandler<F> where
F: Fn(ToolCall, ToolCallContext, ToolExecutionControl) -> BoxFut + Send + Sync
{
}
impl<F> AbortableAtYieldHandler for AsyncToolHandler<F> where
F: Fn(ToolCall, ToolCallContext, ToolExecutionControl) -> BoxFut + Send + Sync
{
}
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 control_body = control.clone();
let fut = (self.f)(call, context);
let (tx, rx) = oneshot::channel();
let drive = Box::pin(async move {
let _ = control_body;
let result = fut.await;
let _ = tx.send(result);
});
let kill = ToolKillHandle::cancel_only(control.clone());
Ok(LinkedToolExecutionHandle {
execution_id: ToolExecutionId::generate(),
control,
completion: ToolExecutionCompletion::new(rx),
kill: Some(kill),
drive: Some(drive),
})
}
fn supports_abort(&self) -> bool {
true
}
fn supports_isolated_kill(&self) -> bool {
false
}
fn runtime_owns_abortable_drive(&self) -> bool {
true
}
}
impl<F> abortable_seal::Sealed for IsolatedKillableToolHandler<F> where
F: Fn(ToolCall, ToolCallContext) -> BoxFut + Send + Sync
{
}
impl<F> AbortableAtYieldHandler for IsolatedKillableToolHandler<F> where
F: Fn(ToolCall, ToolCallContext) -> BoxFut + Send + Sync
{
}
#[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,
drive: None,
})
}
}