use std::fmt;
use std::sync::Arc;
use ferrin_message::Message;
use ferrin_spec::BoxStream;
use ferrin_spec::JsonValue;
use ferrin_spec::ToolCallId;
use futures_util::StreamExt;
use tokio_util::sync::CancellationToken;
use crate::error::ToolError;
pub type ToolOutputStream = BoxStream<'static, Result<ToolOutput, ToolError>>;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ToolOutput {
Preliminary(JsonValue),
Final(JsonValue),
}
impl ToolOutput {
#[must_use]
pub fn is_final(&self) -> bool {
matches!(self, Self::Final(_))
}
#[must_use]
pub fn value(&self) -> &JsonValue {
match self {
Self::Preliminary(value) | Self::Final(value) => value,
}
}
#[must_use]
pub fn into_value(self) -> JsonValue {
match self {
Self::Preliminary(value) | Self::Final(value) => value,
}
}
}
pub trait ToolExecute: Send + Sync {
fn execute(&self, input: JsonValue, ctx: ToolContext) -> ToolOutputStream;
}
#[derive(Clone)]
pub struct ToolContext {
pub tool_call_id: ToolCallId,
pub messages: Arc<[Message]>,
pub cancellation: CancellationToken,
pub tools_context: Option<JsonValue>,
#[cfg(feature = "sandbox")]
pub sandbox: Option<Arc<dyn crate::sandbox::Sandbox>>,
}
impl fmt::Debug for ToolContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut debug = f.debug_struct("ToolContext");
debug
.field("tool_call_id", &self.tool_call_id)
.field("messages", &self.messages.len())
.field("cancelled", &self.cancellation.is_cancelled())
.field("tools_context", &self.tools_context);
#[cfg(feature = "sandbox")]
debug.field(
"sandbox",
&self.sandbox.as_ref().map(|sandbox| sandbox.description()),
);
debug.finish()
}
}
impl ToolContext {
#[must_use]
pub fn new(tool_call_id: impl Into<ToolCallId>) -> Self {
Self {
tool_call_id: tool_call_id.into(),
messages: Arc::from(Vec::new()),
cancellation: CancellationToken::new(),
tools_context: None,
#[cfg(feature = "sandbox")]
sandbox: None,
}
}
#[must_use]
pub fn with_messages(mut self, messages: impl Into<Arc<[Message]>>) -> Self {
self.messages = messages.into();
self
}
#[must_use]
pub fn with_cancellation(mut self, cancellation: CancellationToken) -> Self {
self.cancellation = cancellation;
self
}
#[must_use]
pub fn with_tools_context(mut self, tools_context: Option<JsonValue>) -> Self {
self.tools_context = tools_context;
self
}
#[cfg(feature = "sandbox")]
#[must_use]
pub fn with_sandbox(mut self, sandbox: Arc<dyn crate::sandbox::Sandbox>) -> Self {
self.sandbox = Some(sandbox);
self
}
}
pub async fn execute_to_completion(
mut stream: ToolOutputStream,
mut on_preliminary: impl FnMut(JsonValue),
) -> Result<JsonValue, ToolError> {
while let Some(item) = stream.next().await {
match item? {
ToolOutput::Preliminary(value) => on_preliminary(value),
ToolOutput::Final(value) => return Ok(value),
}
}
Err(ToolError::message("tool produced no final output"))
}