use std::time::Duration;
use serde_json::Value;
use crate::ir::ToolSpec;
use crate::tools::effect::{RetryHint, ToolEffect};
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ToolMetadata {
pub name: String,
pub description: String,
pub input_schema: Value,
pub output_schema: Option<Value>,
pub version: Option<String>,
pub effect: ToolEffect,
pub idempotent: bool,
pub retry_hint: Option<RetryHint>,
pub typical_duration: Option<Duration>,
}
impl ToolMetadata {
#[must_use]
pub fn function(
name: impl Into<String>,
description: impl Into<String>,
input_schema: Value,
) -> Self {
Self {
name: name.into(),
description: description.into(),
input_schema,
output_schema: None,
version: None,
effect: ToolEffect::default(),
idempotent: false,
retry_hint: None,
typical_duration: None,
}
}
#[must_use]
pub fn with_output_schema(mut self, schema: Value) -> Self {
self.output_schema = Some(schema);
self
}
#[must_use]
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
#[must_use]
pub const fn with_effect(mut self, effect: ToolEffect) -> Self {
self.effect = effect;
self
}
#[must_use]
pub fn to_tool_spec(&self) -> ToolSpec {
ToolSpec::function(
self.name.clone(),
self.description.clone(),
self.input_schema.clone(),
)
}
#[must_use]
pub const fn with_idempotent(mut self, idempotent: bool) -> Self {
self.idempotent = idempotent;
self
}
#[must_use]
pub const fn with_retry_hint(mut self, hint: RetryHint) -> Self {
self.retry_hint = Some(hint);
self.idempotent = true;
self
}
#[must_use]
pub const fn with_typical_duration(mut self, duration: Duration) -> Self {
self.typical_duration = Some(duration);
self
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn function_defaults_are_conservative() {
let m = ToolMetadata::function("echo", "echoes input", json!({"type": "object"}));
assert_eq!(m.name, "echo");
assert_eq!(m.description, "echoes input");
assert!(m.output_schema.is_none());
assert!(m.version.is_none());
assert_eq!(m.effect, ToolEffect::ReadOnly);
assert!(!m.idempotent);
assert!(m.retry_hint.is_none());
assert!(m.typical_duration.is_none());
}
#[test]
fn with_retry_hint_implies_idempotent() {
let m = ToolMetadata::function("get", "fetches", json!({}))
.with_retry_hint(RetryHint::idempotent_transport());
assert!(m.idempotent);
assert!(m.retry_hint.is_some());
}
#[test]
fn builder_chain_is_const_friendly() {
let m = ToolMetadata::function("delete", "deletes a row", json!({}))
.with_effect(ToolEffect::Destructive)
.with_version("1.2.0")
.with_typical_duration(Duration::from_millis(50));
assert_eq!(m.effect, ToolEffect::Destructive);
assert_eq!(m.version.as_deref(), Some("1.2.0"));
assert_eq!(m.typical_duration, Some(Duration::from_millis(50)));
}
}