1use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, Ordering};
5
6use futures::future::BoxFuture;
7use oino_ai::{ConstrainedSampling, JsonObject, JsonValue, ToolDecl, Usage, UserContent};
8use serde::{Deserialize, Serialize};
9use tokio::sync::mpsc;
10use tokio_util::sync::CancellationToken;
11
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum ExecutionMode {
17 #[default]
19 Parallel,
20 Sequential,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "lowercase")]
27pub enum Replay {
28 Never,
30 Safe,
32}
33
34#[derive(Clone, Debug, PartialEq)]
36pub struct ToolDefinitionMeta {
37 pub name: String,
39 pub label: String,
41 pub description: String,
43 pub parameters: JsonValue,
45 pub output_schema: Option<JsonValue>,
47 pub execution_mode: ExecutionMode,
49 pub replay: Option<Replay>,
51 pub constrained_sampling: Option<ConstrainedSampling>,
53}
54
55impl ToolDefinitionMeta {
56 #[must_use]
58 pub fn new(
59 name: impl Into<String>,
60 label: impl Into<String>,
61 description: impl Into<String>,
62 parameters: JsonValue,
63 ) -> Self {
64 Self {
65 name: name.into(),
66 label: label.into(),
67 description: description.into(),
68 parameters,
69 output_schema: None,
70 execution_mode: ExecutionMode::Parallel,
71 replay: None,
72 constrained_sampling: None,
73 }
74 }
75
76 #[must_use]
78 pub fn declaration(&self) -> ToolDecl {
79 ToolDecl {
80 name: self.name.clone(),
81 description: self.description.clone(),
82 parameters: self.parameters.clone(),
83 constrained_sampling: self.constrained_sampling.clone(),
84 }
85 }
86}
87
88#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct AgentToolResult {
92 #[serde(default)]
94 pub content: Vec<UserContent>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub details: Option<JsonValue>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub structured_content: Option<JsonValue>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub usage: Option<Usage>,
104 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
106 pub is_error: bool,
107 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
109 pub terminate: bool,
110}
111
112impl AgentToolResult {
113 #[must_use]
115 pub fn text(text: impl Into<String>) -> Self {
116 Self {
117 content: vec![UserContent::text(text)],
118 ..Self::default()
119 }
120 }
121
122 #[must_use]
124 pub fn error(message: impl Into<String>) -> Self {
125 Self {
126 content: vec![UserContent::text(message)],
127 details: Some(JsonValue::Object(JsonObject::new())),
128 ..Self::default()
129 }
130 }
131}
132
133pub type ToolUpdate = AgentToolResult;
135
136#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
138#[error("{0}")]
139pub struct ToolError(pub String);
140
141impl ToolError {
142 #[must_use]
144 pub fn new(message: impl Into<String>) -> Self {
145 Self(message.into())
146 }
147}
148
149impl From<String> for ToolError {
150 fn from(message: String) -> Self {
151 Self(message)
152 }
153}
154
155impl From<&str> for ToolError {
156 fn from(message: &str) -> Self {
157 Self(message.to_owned())
158 }
159}
160
161#[derive(Clone, Debug, PartialEq)]
163pub struct ToolInvocation {
164 pub tool_call_id: String,
166 pub args: JsonObject,
168 pub parent_tool_call_id: Option<String>,
170}
171
172#[derive(Clone, Debug)]
175pub struct UpdateSink {
176 tx: Option<mpsc::UnboundedSender<ToolUpdate>>,
177 open: Arc<AtomicBool>,
178}
179
180impl UpdateSink {
181 #[must_use]
183 pub fn discard() -> Self {
184 Self {
185 tx: None,
186 open: Arc::new(AtomicBool::new(false)),
187 }
188 }
189
190 pub(crate) fn channel() -> (Self, mpsc::UnboundedReceiver<ToolUpdate>, Arc<AtomicBool>) {
192 let (tx, rx) = mpsc::unbounded_channel();
193 let open = Arc::new(AtomicBool::new(true));
194 (
195 Self {
196 tx: Some(tx),
197 open: Arc::clone(&open),
198 },
199 rx,
200 open,
201 )
202 }
203
204 pub fn send(&self, update: ToolUpdate) {
206 if !self.open.load(Ordering::Acquire) {
207 return;
208 }
209 if let Some(tx) = &self.tx {
210 let _ = tx.send(update);
212 }
213 }
214
215 #[must_use]
217 pub fn is_open(&self) -> bool {
218 self.open.load(Ordering::Acquire)
219 }
220}
221
222pub trait AgentTool: Send + Sync {
224 fn meta(&self) -> &ToolDefinitionMeta;
226
227 fn prepare_arguments(&self, raw: JsonObject) -> JsonObject {
229 raw
230 }
231
232 fn execute(
234 &self,
235 call: ToolInvocation,
236 updates: UpdateSink,
237 cancel: CancellationToken,
238 ) -> BoxFuture<'static, Result<AgentToolResult, ToolError>>;
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use serde_json::json;
245
246 #[test]
247 fn error_result_matches_pi_shape() {
248 let value = serde_json::to_value(AgentToolResult::error("boom")).unwrap();
249 assert_eq!(
250 value,
251 json!({"content":[{"type":"text","text":"boom"}],"details":{}})
252 );
253 }
254
255 #[test]
256 fn closed_sink_drops_updates() {
257 let (sink, mut rx, open) = UpdateSink::channel();
258 sink.send(AgentToolResult::text("a"));
259 open.store(false, Ordering::Release);
260 sink.send(AgentToolResult::text("b"));
261 assert_eq!(rx.try_recv().ok(), Some(AgentToolResult::text("a")));
262 assert!(rx.try_recv().is_err());
263 }
264}