Skip to main content

nanocodex_oai_api/tools/
mod.rs

1//! Dependency-light contract for caller-defined and runtime-provided tools.
2
3use 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
14/// Default maximum model-visible output budget for one tool call.
15pub const DEFAULT_TOOL_OUTPUT_TOKENS: usize = 10_000;
16
17/// Model-visible body returned by a tool.
18#[derive(Clone, Debug, Deserialize, Serialize)]
19#[serde(untagged)]
20pub enum ToolOutputBody {
21    /// Plain text, including serialized JSON returned by function tools.
22    Text(String),
23    /// Ordered multimodal output.
24    Content(Vec<ToolOutputContent>),
25}
26
27/// One model-visible item in a multimodal tool output.
28#[derive(Clone, Debug, Deserialize, Serialize)]
29#[serde(tag = "type", rename_all = "snake_case")]
30pub enum ToolOutputContent {
31    /// Text input returned to the model.
32    InputText {
33        /// Complete text.
34        text: String,
35    },
36    /// Image input returned to the model.
37    InputImage {
38        /// Data URL or provider-supported image URL.
39        image_url: String,
40        /// Requested model image detail.
41        detail: ImageDetail,
42    },
43    /// Audio input returned to the model.
44    InputAudio {
45        /// Data URL or provider-supported audio URL.
46        audio_url: String,
47    },
48}
49
50/// Complete output of one tool invocation.
51///
52/// Use [`Self::text`], [`Self::json`], or [`Self::content`] for successful
53/// results. [`Self::error`] creates a structured model-visible failure without
54/// turning the handler invocation itself into an error.
55pub struct ToolOutput {
56    /// Model-visible output body.
57    pub output: ToolOutputBody,
58    /// Whether the remote or local operation succeeded.
59    pub success: bool,
60    /// Optional validated opaque metadata for events and adapters.
61    pub metadata: Option<Box<RawValue>>,
62    code_mode_value: Option<Value>,
63    process_trace: Option<ToolProcessTrace>,
64}
65
66/// Lossless process-boundary representation of a tool output.
67#[doc(hidden)]
68#[allow(missing_docs)]
69#[derive(Deserialize, Serialize)]
70pub struct ToolOutputWire {
71    pub output: ToolOutputBody,
72    pub success: bool,
73    pub code_mode_value: Option<Box<RawValue>>,
74    pub metadata: Option<Box<RawValue>>,
75    pub process_trace: Option<ToolProcessTraceWire>,
76}
77
78/// Process measurements attached by process-backed tool implementations.
79#[doc(hidden)]
80#[allow(missing_docs)]
81#[derive(Clone, Copy, Debug)]
82pub struct ToolProcessTrace {
83    pub exit_code: Option<i32>,
84    pub session_id: Option<i64>,
85    pub original_token_count: Option<usize>,
86    pub output_bytes: usize,
87    pub wall_time_seconds: f64,
88}
89
90/// Serialized process measurements.
91#[doc(hidden)]
92#[allow(missing_docs)]
93#[derive(Deserialize, Serialize)]
94pub struct ToolProcessTraceWire {
95    pub exit_code: Option<i32>,
96    pub session_id: Option<i64>,
97    pub original_token_count: Option<usize>,
98    pub output_bytes: usize,
99    pub wall_time_seconds: f64,
100}
101
102/// Error returned by an application-defined tool handler.
103pub type ToolError = Box<dyn std::error::Error + Send + Sync + 'static>;
104
105/// Result returned by [`Tool::execute`].
106///
107/// The owning runtime converts an error into a failed model-visible tool
108/// result so the model can recover. Return `Ok(ToolOutput::error(...))` only
109/// when preserving a structured failure from a remote tool protocol.
110pub type ToolResult = std::result::Result<ToolOutput, ToolError>;
111
112impl ToolOutput {
113    /// Creates a successful plain-text output.
114    #[must_use]
115    pub fn text(output: impl Into<String>) -> Self {
116        Self {
117            output: ToolOutputBody::Text(output.into()),
118            success: true,
119            metadata: None,
120            code_mode_value: None,
121            process_trace: None,
122        }
123    }
124
125    /// Creates a model-visible failed output.
126    #[must_use]
127    pub fn error(error: impl Into<String>) -> Self {
128        Self {
129            output: ToolOutputBody::Text(error.into()),
130            success: false,
131            metadata: None,
132            code_mode_value: None,
133            process_trace: None,
134        }
135    }
136
137    /// Serializes one successful function result as JSON text.
138    #[must_use]
139    pub fn json(output: &impl Serialize) -> Self {
140        match serde_json::to_string(output) {
141            Ok(output) => Self::text(output),
142            Err(error) => Self::error(format!("failed to encode tool result: {error}")),
143        }
144    }
145
146    /// Creates a JSON result with an explicit success state.
147    ///
148    /// Code Mode receives the typed JSON value while the Responses API
149    /// receives its serialized text representation.
150    #[must_use]
151    pub fn from_json(output: Value, success: bool) -> Self {
152        match serde_json::to_string(&output) {
153            Ok(encoded) => Self {
154                output: ToolOutputBody::Text(encoded),
155                success,
156                metadata: None,
157                code_mode_value: Some(output),
158                process_trace: None,
159            },
160            Err(error) => Self::error(format!("failed to encode tool result: {error}")),
161        }
162    }
163
164    /// Creates a successful multimodal output.
165    #[must_use]
166    pub const fn content(output: Vec<ToolOutputContent>) -> Self {
167        Self {
168            output: ToolOutputBody::Content(output),
169            success: true,
170            metadata: None,
171            code_mode_value: None,
172            process_trace: None,
173        }
174    }
175
176    /// Attaches validated opaque metadata.
177    ///
178    /// An encoding failure converts this output into a model-visible failure.
179    #[must_use]
180    pub fn with_metadata(mut self, metadata: impl Serialize) -> Self {
181        match to_raw_value(&metadata) {
182            Ok(metadata) => self.metadata = Some(metadata),
183            Err(error) => {
184                self.output =
185                    ToolOutputBody::Text(format!("failed to encode tool result metadata: {error}"));
186                self.success = false;
187            }
188        }
189        self
190    }
191
192    /// Returns the value exposed to Code Mode.
193    #[doc(hidden)]
194    #[must_use]
195    pub fn code_mode_value(&self) -> Value {
196        if let Some(value) = &self.code_mode_value {
197            return value.clone();
198        }
199        match &self.output {
200            ToolOutputBody::Text(text) => {
201                serde_json::from_str(text).unwrap_or_else(|_| Value::String(text.clone()))
202            }
203            ToolOutputBody::Content(content) => {
204                serde_json::to_value(content).unwrap_or(Value::Null)
205            }
206        }
207    }
208
209    /// Replaces the value exposed to Code Mode.
210    #[doc(hidden)]
211    #[must_use]
212    pub fn with_code_mode_value(mut self, value: Value) -> Self {
213        self.code_mode_value = Some(value);
214        self
215    }
216
217    /// Attaches process measurements from a process-backed implementation.
218    #[doc(hidden)]
219    #[must_use]
220    pub const fn with_process_trace(
221        mut self,
222        exit_code: Option<i32>,
223        session_id: Option<i64>,
224        original_token_count: Option<usize>,
225        output_bytes: usize,
226        wall_time_seconds: f64,
227    ) -> Self {
228        self.process_trace = Some(ToolProcessTrace {
229            exit_code,
230            session_id,
231            original_token_count,
232            output_bytes,
233            wall_time_seconds,
234        });
235        self
236    }
237
238    /// Returns attached process measurements.
239    #[doc(hidden)]
240    #[must_use]
241    pub const fn process_trace(&self) -> Option<&ToolProcessTrace> {
242        self.process_trace.as_ref()
243    }
244
245    /// Converts this output into its lossless process-boundary form.
246    ///
247    /// # Errors
248    ///
249    /// Returns an error if the internal Code Mode JSON cannot be encoded.
250    #[doc(hidden)]
251    pub fn into_wire(self) -> Result<ToolOutputWire, serde_json::Error> {
252        Ok(ToolOutputWire {
253            output: self.output,
254            success: self.success,
255            code_mode_value: self
256                .code_mode_value
257                .map(|value| to_raw_value(&value))
258                .transpose()?,
259            metadata: self.metadata,
260            process_trace: self.process_trace.map(Into::into),
261        })
262    }
263
264    /// Restores an output received from a process boundary.
265    ///
266    /// # Errors
267    ///
268    /// Returns an error if an opaque Code Mode value cannot be decoded.
269    #[doc(hidden)]
270    pub fn from_wire(wire: ToolOutputWire) -> Result<Self, serde_json::Error> {
271        Ok(Self {
272            output: wire.output,
273            success: wire.success,
274            metadata: wire.metadata,
275            code_mode_value: wire
276                .code_mode_value
277                .map(|value| serde_json::from_str(value.get()))
278                .transpose()?,
279            process_trace: wire.process_trace.map(Into::into),
280        })
281    }
282}
283
284impl From<ToolProcessTrace> for ToolProcessTraceWire {
285    fn from(trace: ToolProcessTrace) -> Self {
286        Self {
287            exit_code: trace.exit_code,
288            session_id: trace.session_id,
289            original_token_count: trace.original_token_count,
290            output_bytes: trace.output_bytes,
291            wall_time_seconds: trace.wall_time_seconds,
292        }
293    }
294}
295
296impl From<ToolProcessTraceWire> for ToolProcessTrace {
297    fn from(trace: ToolProcessTraceWire) -> Self {
298        Self {
299            exit_code: trace.exit_code,
300            session_id: trace.session_id,
301            original_token_count: trace.original_token_count,
302            output_bytes: trace.output_bytes,
303            wall_time_seconds: trace.wall_time_seconds,
304        }
305    }
306}
307
308/// Read-only context for one tool invocation.
309#[derive(Clone, Copy)]
310pub struct ToolContext<'a> {
311    model: &'a str,
312    session_id: &'a str,
313    call_id: &'a str,
314    history: &'a [ResponseItem],
315    output_token_budget: usize,
316}
317
318impl<'a> ToolContext<'a> {
319    /// Creates the complete read-only context for one tool invocation.
320    #[must_use]
321    pub const fn new(
322        model: &'a str,
323        session_id: &'a str,
324        call_id: &'a str,
325        history: &'a [ResponseItem],
326        output_token_budget: usize,
327    ) -> Self {
328        Self {
329            model,
330            session_id,
331            call_id,
332            history,
333            output_token_budget,
334        }
335    }
336
337    /// Returns the fixed model contract for this invocation.
338    #[must_use]
339    pub const fn model(self) -> &'a str {
340        self.model
341    }
342
343    /// Returns the stable client-owned session identity.
344    #[must_use]
345    pub const fn session_id(self) -> &'a str {
346        self.session_id
347    }
348
349    /// Returns the provider tool-call identity.
350    #[must_use]
351    pub const fn call_id(self) -> &'a str {
352        self.call_id
353    }
354
355    /// Returns committed authoritative history visible at this call boundary.
356    #[must_use]
357    pub const fn history(self) -> &'a [ResponseItem] {
358        self.history
359    }
360
361    /// Returns the maximum model-visible tool-output budget.
362    #[must_use]
363    pub const fn output_token_budget(self) -> usize {
364        self.output_token_budget
365    }
366}
367
368/// Canonical input presented to function and freeform tools.
369pub enum ToolInput {
370    /// Validated raw JSON arguments from a function call.
371    Function(Box<RawValue>),
372    /// Complete freeform custom-tool input.
373    Freeform(String),
374}
375
376impl ToolInput {
377    /// Borrows raw JSON function arguments without materializing a value tree.
378    ///
379    /// # Errors
380    ///
381    /// Returns an error for freeform input.
382    pub fn function_json(&self) -> Result<&RawValue, ToolInputError> {
383        match self {
384            Self::Function(input) => Ok(input),
385            Self::Freeform(_) => Err(ToolInputError::ExpectedFunction),
386        }
387    }
388
389    /// Decodes JSON function arguments into a caller-selected type.
390    ///
391    /// # Errors
392    ///
393    /// Returns an error for freeform input or invalid JSON arguments.
394    pub fn decode_json<T: DeserializeOwned>(&self) -> Result<T, ToolInputError> {
395        serde_json::from_str(self.function_json()?.get()).map_err(ToolInputError::Decode)
396    }
397
398    /// Extracts freeform source text.
399    ///
400    /// # Errors
401    ///
402    /// Returns an error for JSON function arguments.
403    pub fn into_freeform(self) -> Result<String, ToolInputError> {
404        match self {
405            Self::Freeform(input) => Ok(input),
406            Self::Function(_) => Err(ToolInputError::ExpectedFreeform),
407        }
408    }
409}
410
411/// Invalid access or decoding of typed tool input.
412#[derive(Debug, thiserror::Error)]
413pub enum ToolInputError {
414    /// A function tool received freeform input.
415    #[error("expected JSON function arguments")]
416    ExpectedFunction,
417    /// A custom tool received function arguments.
418    #[error("expected freeform tool input")]
419    ExpectedFreeform,
420    /// Function arguments did not decode into the requested type.
421    #[error("failed to parse function arguments: {0}")]
422    Decode(#[source] serde_json::Error),
423}
424
425/// A caller-defined model-visible tool.
426///
427/// ```
428/// use async_trait::async_trait;
429/// use nanocodex_oai_api::{
430///     responses::JsonSchema,
431///     tools::{
432///         Tool, ToolContext, ToolDefinition, ToolInput, ToolOutput, ToolResult,
433///     },
434/// };
435/// use serde_json::json;
436///
437/// struct DeploymentRegion;
438///
439/// #[async_trait]
440/// impl Tool for DeploymentRegion {
441///     fn definition(&self) -> ToolDefinition {
442///         ToolDefinition::function(
443///             "deployment_region",
444///             "Return the production deployment region.",
445///             JsonSchema::from(json!({
446///                 "type": "object",
447///                 "properties": {},
448///                 "additionalProperties": false
449///             })),
450///         )
451///     }
452///
453///     async fn execute(
454///         &self,
455///         input: ToolInput,
456///         _context: ToolContext<'_>,
457///     ) -> ToolResult {
458///         let _: serde_json::Value = input.decode_json()?;
459///         Ok(ToolOutput::text("us-west-2"))
460///     }
461/// }
462/// ```
463#[async_trait]
464pub trait Tool: Send + Sync + 'static {
465    /// Returns the complete model-visible definition and registry name.
466    fn definition(&self) -> ToolDefinition;
467
468    /// Returns whether this handler is safe to execute alongside sibling tool calls.
469    ///
470    /// Execution is serial by default. Opt in only when the handler's state and
471    /// effects are safe to overlap with other parallel-capable tools.
472    fn supports_parallel_tool_calls(&self) -> bool {
473        false
474    }
475
476    /// Executes one invocation.
477    async fn execute(&self, input: ToolInput, context: ToolContext<'_>) -> ToolResult;
478}