1use async_trait::async_trait;
4use serde::{Deserialize, Serialize, de::DeserializeOwned};
5use serde_json::{
6 Value,
7 value::{RawValue, to_raw_value},
8};
9
10pub use crate::responses::ToolDefinition;
11
12use crate::{ImageDetail, ResponseItem};
13
14pub const DEFAULT_TOOL_OUTPUT_TOKENS: usize = 10_000;
16
17#[derive(Clone, Debug, Deserialize, Serialize)]
19#[serde(untagged)]
20pub enum ToolOutputBody {
21 Text(String),
23 Content(Vec<ToolOutputContent>),
25}
26
27impl ToolOutputBody {
28 #[must_use]
30 pub fn structured_result(&self) -> Value {
31 match self {
32 Self::Text(text) => Value::String(text.clone()),
33 Self::Content(content) => serde_json::to_value(content).unwrap_or(Value::Null),
34 }
35 }
36}
37
38#[derive(Clone, Debug, Deserialize, Serialize)]
40#[serde(tag = "type", rename_all = "snake_case")]
41pub enum ToolOutputContent {
42 InputText {
44 text: String,
46 },
47 InputImage {
49 image_url: String,
51 detail: ImageDetail,
53 },
54 InputAudio {
56 audio_url: String,
58 },
59}
60
61pub struct ToolOutput {
67 pub output: ToolOutputBody,
69 pub success: bool,
71 pub metadata: Option<Box<RawValue>>,
73 structured_result: Option<Value>,
74 process_trace: Option<ToolProcessTrace>,
75}
76
77#[doc(hidden)]
79#[allow(missing_docs)]
80#[derive(Deserialize, Serialize)]
81pub struct ToolOutputWire {
82 pub output: ToolOutputBody,
83 pub success: bool,
84 pub structured_result: Option<Box<RawValue>>,
85 pub metadata: Option<Box<RawValue>>,
86 pub process_trace: Option<ToolProcessTraceWire>,
87}
88
89#[doc(hidden)]
91#[allow(missing_docs)]
92#[derive(Clone, Copy, Debug)]
93pub struct ToolProcessTrace {
94 pub exit_code: Option<i32>,
95 pub session_id: Option<i64>,
96 pub original_token_count: Option<usize>,
97 pub output_bytes: usize,
98 pub wall_time_seconds: f64,
99}
100
101#[doc(hidden)]
103#[allow(missing_docs)]
104#[derive(Deserialize, Serialize)]
105pub struct ToolProcessTraceWire {
106 pub exit_code: Option<i32>,
107 pub session_id: Option<i64>,
108 pub original_token_count: Option<usize>,
109 pub output_bytes: usize,
110 pub wall_time_seconds: f64,
111}
112
113pub type ToolError = Box<dyn std::error::Error + Send + Sync + 'static>;
115
116pub type ToolResult = std::result::Result<ToolOutput, ToolError>;
122
123impl ToolOutput {
124 #[must_use]
126 pub fn text(output: impl Into<String>) -> Self {
127 Self {
128 output: ToolOutputBody::Text(output.into()),
129 success: true,
130 metadata: None,
131 structured_result: None,
132 process_trace: None,
133 }
134 }
135
136 #[must_use]
138 pub fn error(error: impl Into<String>) -> Self {
139 Self {
140 output: ToolOutputBody::Text(error.into()),
141 success: false,
142 metadata: None,
143 structured_result: None,
144 process_trace: None,
145 }
146 }
147
148 #[must_use]
150 pub fn json(output: &impl Serialize) -> Self {
151 match serde_json::to_value(output) {
152 Ok(output) => Self::from_json(output, true),
153 Err(error) => Self::error(format!("failed to encode tool result: {error}")),
154 }
155 }
156
157 #[must_use]
162 pub fn from_json(output: Value, success: bool) -> Self {
163 match serde_json::to_string(&output) {
164 Ok(encoded) => Self {
165 output: ToolOutputBody::Text(encoded),
166 success,
167 metadata: None,
168 structured_result: Some(output),
169 process_trace: None,
170 },
171 Err(error) => Self::error(format!("failed to encode tool result: {error}")),
172 }
173 }
174
175 #[must_use]
177 pub const fn content(output: Vec<ToolOutputContent>) -> Self {
178 Self {
179 output: ToolOutputBody::Content(output),
180 success: true,
181 metadata: None,
182 structured_result: None,
183 process_trace: None,
184 }
185 }
186
187 #[must_use]
191 pub fn with_metadata(mut self, metadata: impl Serialize) -> Self {
192 match to_raw_value(&metadata) {
193 Ok(metadata) => self.metadata = Some(metadata),
194 Err(error) => {
195 self.output =
196 ToolOutputBody::Text(format!("failed to encode tool result metadata: {error}"));
197 self.success = false;
198 }
199 }
200 self
201 }
202
203 #[must_use]
208 pub fn structured_result(&self) -> Value {
209 if let Some(value) = &self.structured_result {
210 return value.clone();
211 }
212 self.output.structured_result()
213 }
214
215 #[must_use]
217 pub fn with_structured_result(mut self, value: Value) -> Self {
218 self.structured_result = Some(value);
219 self
220 }
221
222 #[doc(hidden)]
224 #[must_use]
225 pub const fn with_process_trace(
226 mut self,
227 exit_code: Option<i32>,
228 session_id: Option<i64>,
229 original_token_count: Option<usize>,
230 output_bytes: usize,
231 wall_time_seconds: f64,
232 ) -> Self {
233 self.process_trace = Some(ToolProcessTrace {
234 exit_code,
235 session_id,
236 original_token_count,
237 output_bytes,
238 wall_time_seconds,
239 });
240 self
241 }
242
243 #[doc(hidden)]
245 #[must_use]
246 pub const fn process_trace(&self) -> Option<&ToolProcessTrace> {
247 self.process_trace.as_ref()
248 }
249
250 #[doc(hidden)]
256 pub fn into_wire(self) -> Result<ToolOutputWire, serde_json::Error> {
257 Ok(ToolOutputWire {
258 output: self.output,
259 success: self.success,
260 structured_result: self
261 .structured_result
262 .map(|value| to_raw_value(&value))
263 .transpose()?,
264 metadata: self.metadata,
265 process_trace: self.process_trace.map(Into::into),
266 })
267 }
268
269 #[doc(hidden)]
275 pub fn from_wire(wire: ToolOutputWire) -> Result<Self, serde_json::Error> {
276 Ok(Self {
277 output: wire.output,
278 success: wire.success,
279 metadata: wire.metadata,
280 structured_result: wire
281 .structured_result
282 .map(|value| serde_json::from_str(value.get()))
283 .transpose()?,
284 process_trace: wire.process_trace.map(Into::into),
285 })
286 }
287}
288
289impl From<ToolProcessTrace> for ToolProcessTraceWire {
290 fn from(trace: ToolProcessTrace) -> Self {
291 Self {
292 exit_code: trace.exit_code,
293 session_id: trace.session_id,
294 original_token_count: trace.original_token_count,
295 output_bytes: trace.output_bytes,
296 wall_time_seconds: trace.wall_time_seconds,
297 }
298 }
299}
300
301impl From<ToolProcessTraceWire> for ToolProcessTrace {
302 fn from(trace: ToolProcessTraceWire) -> Self {
303 Self {
304 exit_code: trace.exit_code,
305 session_id: trace.session_id,
306 original_token_count: trace.original_token_count,
307 output_bytes: trace.output_bytes,
308 wall_time_seconds: trace.wall_time_seconds,
309 }
310 }
311}
312
313#[derive(Clone, Copy)]
315pub struct ToolContext<'a> {
316 model: &'a str,
317 session_id: &'a str,
318 call_id: &'a str,
319 history: &'a [ResponseItem],
320 output_token_budget: usize,
321}
322
323impl<'a> ToolContext<'a> {
324 #[must_use]
326 pub const fn new(
327 model: &'a str,
328 session_id: &'a str,
329 call_id: &'a str,
330 history: &'a [ResponseItem],
331 output_token_budget: usize,
332 ) -> Self {
333 Self {
334 model,
335 session_id,
336 call_id,
337 history,
338 output_token_budget,
339 }
340 }
341
342 #[must_use]
344 pub const fn model(self) -> &'a str {
345 self.model
346 }
347
348 #[must_use]
350 pub const fn session_id(self) -> &'a str {
351 self.session_id
352 }
353
354 #[must_use]
356 pub const fn call_id(self) -> &'a str {
357 self.call_id
358 }
359
360 #[must_use]
362 pub const fn history(self) -> &'a [ResponseItem] {
363 self.history
364 }
365
366 #[must_use]
368 pub const fn output_token_budget(self) -> usize {
369 self.output_token_budget
370 }
371}
372
373pub enum ToolInput {
375 Function(Box<RawValue>),
377 Freeform(String),
379}
380
381impl ToolInput {
382 pub fn function_json(&self) -> Result<&RawValue, ToolInputError> {
388 match self {
389 Self::Function(input) => Ok(input),
390 Self::Freeform(_) => Err(ToolInputError::ExpectedFunction),
391 }
392 }
393
394 pub fn decode_json<T: DeserializeOwned>(&self) -> Result<T, ToolInputError> {
400 serde_json::from_str(self.function_json()?.get()).map_err(ToolInputError::Decode)
401 }
402
403 pub fn into_freeform(self) -> Result<String, ToolInputError> {
409 match self {
410 Self::Freeform(input) => Ok(input),
411 Self::Function(_) => Err(ToolInputError::ExpectedFreeform),
412 }
413 }
414}
415
416#[derive(Debug, thiserror::Error)]
418pub enum ToolInputError {
419 #[error("expected JSON function arguments")]
421 ExpectedFunction,
422 #[error("expected freeform tool input")]
424 ExpectedFreeform,
425 #[error("failed to parse function arguments: {0}")]
427 Decode(#[source] serde_json::Error),
428}
429
430#[async_trait]
469pub trait Tool: Send + Sync + 'static {
470 fn definition(&self) -> ToolDefinition;
472
473 fn supports_parallel_tool_calls(&self) -> bool {
478 false
479 }
480
481 async fn execute(&self, input: ToolInput, context: ToolContext<'_>) -> ToolResult;
483}
484
485#[cfg(test)]
486mod tests {
487 use serde_json::json;
488
489 use super::ToolOutput;
490
491 #[test]
492 fn structured_result_preserves_text_and_json_types() {
493 assert_eq!(ToolOutput::text("42").structured_result(), json!("42"));
494 assert_eq!(ToolOutput::json(&42).structured_result(), json!(42));
495 }
496}