use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use futures::future::BoxFuture;
use oino_ai::{ConstrainedSampling, JsonObject, JsonValue, ToolDecl, Usage, UserContent};
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ExecutionMode {
#[default]
Parallel,
Sequential,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Replay {
Never,
Safe,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ToolDefinitionMeta {
pub name: String,
pub label: String,
pub description: String,
pub parameters: JsonValue,
pub output_schema: Option<JsonValue>,
pub execution_mode: ExecutionMode,
pub replay: Option<Replay>,
pub constrained_sampling: Option<ConstrainedSampling>,
}
impl ToolDefinitionMeta {
#[must_use]
pub fn new(
name: impl Into<String>,
label: impl Into<String>,
description: impl Into<String>,
parameters: JsonValue,
) -> Self {
Self {
name: name.into(),
label: label.into(),
description: description.into(),
parameters,
output_schema: None,
execution_mode: ExecutionMode::Parallel,
replay: None,
constrained_sampling: None,
}
}
#[must_use]
pub fn declaration(&self) -> ToolDecl {
ToolDecl {
name: self.name.clone(),
description: self.description.clone(),
parameters: self.parameters.clone(),
constrained_sampling: self.constrained_sampling.clone(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentToolResult {
#[serde(default)]
pub content: Vec<UserContent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub details: Option<JsonValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub structured_content: Option<JsonValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub is_error: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub terminate: bool,
}
impl AgentToolResult {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self {
content: vec![UserContent::text(text)],
..Self::default()
}
}
#[must_use]
pub fn error(message: impl Into<String>) -> Self {
Self {
content: vec![UserContent::text(message)],
details: Some(JsonValue::Object(JsonObject::new())),
..Self::default()
}
}
}
pub type ToolUpdate = AgentToolResult;
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{0}")]
pub struct ToolError(pub String);
impl ToolError {
#[must_use]
pub fn new(message: impl Into<String>) -> Self {
Self(message.into())
}
}
impl From<String> for ToolError {
fn from(message: String) -> Self {
Self(message)
}
}
impl From<&str> for ToolError {
fn from(message: &str) -> Self {
Self(message.to_owned())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ToolInvocation {
pub tool_call_id: String,
pub args: JsonObject,
pub parent_tool_call_id: Option<String>,
}
#[derive(Clone, Debug)]
pub struct UpdateSink {
tx: Option<mpsc::UnboundedSender<ToolUpdate>>,
open: Arc<AtomicBool>,
}
impl UpdateSink {
#[must_use]
pub fn discard() -> Self {
Self {
tx: None,
open: Arc::new(AtomicBool::new(false)),
}
}
pub(crate) fn channel() -> (Self, mpsc::UnboundedReceiver<ToolUpdate>, Arc<AtomicBool>) {
let (tx, rx) = mpsc::unbounded_channel();
let open = Arc::new(AtomicBool::new(true));
(
Self {
tx: Some(tx),
open: Arc::clone(&open),
},
rx,
open,
)
}
pub fn send(&self, update: ToolUpdate) {
if !self.open.load(Ordering::Acquire) {
return;
}
if let Some(tx) = &self.tx {
let _ = tx.send(update);
}
}
#[must_use]
pub fn is_open(&self) -> bool {
self.open.load(Ordering::Acquire)
}
}
pub trait AgentTool: Send + Sync {
fn meta(&self) -> &ToolDefinitionMeta;
fn prepare_arguments(&self, raw: JsonObject) -> JsonObject {
raw
}
fn execute(
&self,
call: ToolInvocation,
updates: UpdateSink,
cancel: CancellationToken,
) -> BoxFuture<'static, Result<AgentToolResult, ToolError>>;
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn error_result_matches_pi_shape() {
let value = serde_json::to_value(AgentToolResult::error("boom")).unwrap();
assert_eq!(
value,
json!({"content":[{"type":"text","text":"boom"}],"details":{}})
);
}
#[test]
fn closed_sink_drops_updates() {
let (sink, mut rx, open) = UpdateSink::channel();
sink.send(AgentToolResult::text("a"));
open.store(false, Ordering::Release);
sink.send(AgentToolResult::text("b"));
assert_eq!(rx.try_recv().ok(), Some(AgentToolResult::text("a")));
assert!(rx.try_recv().is_err());
}
}