rightkit-agent-engine-protocol 0.1.0

Agent engine protocol: events, envelopes, tool calls, trust wrapping and settlement types shared by agent hosts
Documentation
//! Live tool output hook: a plain-data sink so a long-running process tool
//! (shell/exec) can report output chunks while it runs without owning any
//! session state. The engine installs a sink per tool call and turns the
//! (coalesced) chunks into `toolOutputDelta` protocol events.

use std::fmt;
use std::sync::Arc;

/// Which pipe a chunk was read from.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolOutputStream {
    Stdout,
    Stderr,
}

impl ToolOutputStream {
    /// Wire spelling used by `toolOutputDelta.stream`.
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Stdout => "stdout",
            Self::Stderr => "stderr",
        }
    }
}

type OutputFn = dyn Fn(ToolOutputStream, &str) + Send + Sync;

/// Cloneable, optional output sink. Default is a no-op, so a tool can call
/// [`ToolOutputNotifier::notify`] unconditionally.
#[derive(Clone, Default)]
pub struct ToolOutputNotifier(Option<Arc<OutputFn>>);

impl ToolOutputNotifier {
    pub fn new(f: impl Fn(ToolOutputStream, &str) + Send + Sync + 'static) -> Self {
        Self(Some(Arc::new(f)))
    }

    pub fn is_set(&self) -> bool {
        self.0.is_some()
    }

    pub fn notify(&self, stream: ToolOutputStream, text: &str) {
        if let Some(f) = &self.0 {
            f(stream, text);
        }
    }
}

impl fmt::Debug for ToolOutputNotifier {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(if self.0.is_some() {
            "ToolOutputNotifier(set)"
        } else {
            "ToolOutputNotifier(none)"
        })
    }
}

impl PartialEq for ToolOutputNotifier {
    fn eq(&self, other: &Self) -> bool {
        match (&self.0, &other.0) {
            (None, None) => true,
            (Some(a), Some(b)) => Arc::ptr_eq(a, b),
            _ => false,
        }
    }
}

impl Eq for ToolOutputNotifier {}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Mutex;

    #[test]
    fn notifier_delivers_and_default_is_silent() {
        ToolOutputNotifier::default().notify(ToolOutputStream::Stdout, "ignored");
        let seen = Arc::new(Mutex::new(Vec::new()));
        let sink = seen.clone();
        let notifier = ToolOutputNotifier::new(move |stream, text| {
            sink.lock()
                .unwrap()
                .push((stream.as_str(), text.to_string()));
        });
        notifier.notify(ToolOutputStream::Stderr, "warn");
        assert!(notifier.is_set());
        assert_eq!(*seen.lock().unwrap(), vec![("stderr", "warn".to_string())]);
    }
}