use serde::{Deserialize, Serialize};
use serde_json::Value;
#[cfg(feature = "openapi")]
use utoipa::ToSchema;
pub const HUMAN_INTENT_ARGUMENT: &str = "human_intent";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResultImage {
pub base64: String,
pub media_type: String,
}
const HUMAN_INTENT_DESCRIPTION: &str = "Short user-facing narration of what this tool call will do, written as an action phrase like \"Listing all harnesses\". Do not include hidden reasoning, private chain of thought, secrets, or credential values.";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum ToolPolicy {
#[default]
Auto,
RequiresApproval,
ClientSide,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum DeferrablePolicy {
Never,
#[default]
Automatic,
Always,
}
impl DeferrablePolicy {
pub fn is_default(&self) -> bool {
matches!(self, DeferrablePolicy::Automatic)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolDefinition {
Builtin(BuiltinTool),
ClientSide(ClientSideTool),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct BuiltinTool {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
pub description: String,
pub parameters: serde_json::Value,
#[serde(default)]
pub policy: ToolPolicy,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub category: Option<String>,
#[serde(default, skip_serializing_if = "DeferrablePolicy::is_default")]
pub deferrable: DeferrablePolicy,
#[serde(default, skip_serializing_if = "ToolHints::is_empty")]
pub hints: ToolHints,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub full_parameters: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ClientSideTool {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
pub description: String,
pub parameters: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub category: Option<String>,
#[serde(default, skip_serializing_if = "DeferrablePolicy::is_default")]
pub deferrable: DeferrablePolicy,
#[serde(default, skip_serializing_if = "ToolHints::is_empty")]
pub hints: ToolHints,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub full_parameters: Option<serde_json::Value>,
}
impl ToolDefinition {
pub fn name(&self) -> &str {
match self {
ToolDefinition::Builtin(b) => &b.name,
ToolDefinition::ClientSide(c) => &c.name,
}
}
pub fn display_name(&self) -> Option<&str> {
match self {
ToolDefinition::Builtin(b) => b.display_name.as_deref(),
ToolDefinition::ClientSide(c) => c.display_name.as_deref(),
}
}
pub fn description(&self) -> &str {
match self {
ToolDefinition::Builtin(b) => &b.description,
ToolDefinition::ClientSide(c) => &c.description,
}
}
pub fn parameters(&self) -> &serde_json::Value {
match self {
ToolDefinition::Builtin(b) => &b.parameters,
ToolDefinition::ClientSide(c) => &c.parameters,
}
}
pub fn full_parameters(&self) -> &serde_json::Value {
match self {
ToolDefinition::Builtin(b) => b.full_parameters.as_ref().unwrap_or(&b.parameters),
ToolDefinition::ClientSide(c) => c.full_parameters.as_ref().unwrap_or(&c.parameters),
}
}
pub fn policy(&self) -> &ToolPolicy {
match self {
ToolDefinition::Builtin(b) => &b.policy,
ToolDefinition::ClientSide(_) => &ToolPolicy::ClientSide,
}
}
pub fn category(&self) -> Option<&str> {
match self {
ToolDefinition::Builtin(b) => b.category.as_deref(),
ToolDefinition::ClientSide(c) => c.category.as_deref(),
}
}
pub fn deferrable(&self) -> &DeferrablePolicy {
match self {
ToolDefinition::Builtin(b) => &b.deferrable,
ToolDefinition::ClientSide(c) => &c.deferrable,
}
}
pub fn hints(&self) -> &ToolHints {
match self {
ToolDefinition::Builtin(b) => &b.hints,
ToolDefinition::ClientSide(c) => &c.hints,
}
}
pub fn concurrency_class(&self) -> Option<&str> {
self.hints().concurrency_class.as_deref()
}
pub fn is_cpu_bound(&self) -> bool {
self.hints().cpu_bound.unwrap_or(false)
}
pub fn side_effect_class(&self) -> SideEffectClass {
self.hints().effective_side_effect_class()
}
pub fn capability_attribution(&self) -> Option<(&str, Option<&str>)> {
self.hints()
.capability_id
.as_deref()
.map(|id| (id, self.hints().capability_name.as_deref()))
}
pub fn with_category(mut self, category: impl Into<String>) -> Self {
match &mut self {
ToolDefinition::Builtin(b) => b.category = Some(category.into()),
ToolDefinition::ClientSide(c) => c.category = Some(category.into()),
}
self
}
pub fn with_hints(mut self, hints: ToolHints) -> Self {
match &mut self {
ToolDefinition::Builtin(b) => b.hints = hints,
ToolDefinition::ClientSide(c) => c.hints = hints,
}
self
}
pub fn with_capability_attribution(
mut self,
capability_id: impl Into<String>,
capability_name: Option<impl Into<String>>,
) -> Self {
let capability_id = capability_id.into();
let capability_name = capability_name.map(Into::into);
match &mut self {
ToolDefinition::Builtin(b) => {
b.hints.capability_id = Some(capability_id);
b.hints.capability_name = capability_name;
}
ToolDefinition::ClientSide(c) => {
c.hints.capability_id = Some(capability_id);
c.hints.capability_name = capability_name;
}
}
self
}
pub fn with_human_intent_argument(mut self) -> Self {
match &mut self {
ToolDefinition::Builtin(b) => add_human_intent_to_schema(&mut b.parameters),
ToolDefinition::ClientSide(c) => add_human_intent_to_schema(&mut c.parameters),
}
self
}
}
pub fn add_human_intent_to_tool_definitions(tools: &[ToolDefinition]) -> Vec<ToolDefinition> {
tools
.iter()
.cloned()
.map(ToolDefinition::with_human_intent_argument)
.collect()
}
pub fn human_intent(arguments: &Value) -> Option<&str> {
arguments
.get(HUMAN_INTENT_ARGUMENT)
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
}
pub fn strip_human_intent_argument(arguments: &Value) -> Value {
let mut stripped = arguments.clone();
if let Value::Object(ref mut object) = stripped {
object.remove(HUMAN_INTENT_ARGUMENT);
}
stripped
}
fn add_human_intent_to_schema(schema: &mut Value) {
let Value::Object(schema_obj) = schema else {
return;
};
schema_obj
.entry("type")
.or_insert_with(|| Value::String("object".to_string()));
let properties = schema_obj
.entry("properties")
.or_insert_with(|| Value::Object(serde_json::Map::new()));
if let Value::Object(properties_obj) = properties {
properties_obj.insert(
HUMAN_INTENT_ARGUMENT.to_string(),
serde_json::json!({
"type": "string",
"description": HUMAN_INTENT_DESCRIPTION,
"maxLength": 120,
}),
);
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub enum SideEffectClass {
Pure,
Idempotent,
#[default]
AtMostOnce,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ToolHints {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub readonly: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub destructive: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub idempotent: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub open_world: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requires_secrets: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub long_running: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supports_background: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub concurrency_class: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_bound: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub persist_output: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capability_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capability_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub narration_noun: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub side_effect_class: Option<SideEffectClass>,
}
impl ToolHints {
pub fn is_empty(&self) -> bool {
*self == Self::default()
}
pub fn with_readonly(mut self, value: bool) -> Self {
self.readonly = Some(value);
self
}
pub fn with_destructive(mut self, value: bool) -> Self {
self.destructive = Some(value);
self
}
pub fn with_idempotent(mut self, value: bool) -> Self {
self.idempotent = Some(value);
self
}
pub fn with_open_world(mut self, value: bool) -> Self {
self.open_world = Some(value);
self
}
pub fn with_capability_attribution(
mut self,
capability_id: impl Into<String>,
capability_name: Option<impl Into<String>>,
) -> Self {
self.capability_id = Some(capability_id.into());
self.capability_name = capability_name.map(Into::into);
self
}
pub fn with_requires_secrets(mut self, value: bool) -> Self {
self.requires_secrets = Some(value);
self
}
pub fn with_long_running(mut self, value: bool) -> Self {
self.long_running = Some(value);
self
}
pub fn with_supports_background(mut self, value: bool) -> Self {
self.supports_background = Some(value);
self
}
pub fn with_concurrency_class(mut self, class: impl Into<String>) -> Self {
self.concurrency_class = Some(class.into());
self
}
pub fn with_cpu_bound(mut self, value: bool) -> Self {
self.cpu_bound = Some(value);
self
}
pub fn with_persist_output(mut self, value: bool) -> Self {
self.persist_output = Some(value);
self
}
pub fn with_narration_noun(mut self, noun: impl Into<String>) -> Self {
self.narration_noun = Some(noun.into());
self
}
pub fn with_side_effect_class(mut self, class: SideEffectClass) -> Self {
self.side_effect_class = Some(class);
self
}
pub fn effective_side_effect_class(&self) -> SideEffectClass {
self.side_effect_class
.clone()
.unwrap_or(SideEffectClass::AtMostOnce)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ToolCall {
pub id: String,
pub name: String,
#[cfg_attr(feature = "openapi", schema(value_type = Object))]
pub arguments: serde_json::Value,
}
impl ToolCall {
pub fn execution_arguments(&self) -> serde_json::Value {
strip_human_intent_argument(&self.arguments)
}
pub fn to_openai_format(&self) -> serde_json::Value {
serde_json::json!({
"id": self.id,
"type": "function",
"function": {
"name": self.name,
"arguments": serde_json::to_string(&self.arguments).unwrap_or_else(|_| "{}".to_string())
}
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
pub tool_call_id: String,
pub result: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub images: Option<Vec<ToolResultImage>>,
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub connection_required: Option<String>,
#[serde(skip)]
pub raw_output: Option<String>,
}
impl ToolResult {
pub fn error(msg: &str) -> Self {
Self {
tool_call_id: String::new(),
result: None,
images: None,
error: Some(msg.to_string()),
connection_required: None,
raw_output: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scheduling_hints_builders_and_accessors() {
let reader = ToolDefinition::Builtin(BuiltinTool {
name: "read_file".to_string(),
display_name: None,
description: "read".to_string(),
parameters: serde_json::json!({}),
policy: ToolPolicy::Auto,
category: None,
deferrable: DeferrablePolicy::default(),
hints: ToolHints::default().with_readonly(true),
full_parameters: None,
});
assert_eq!(reader.concurrency_class(), None);
assert!(!reader.is_cpu_bound());
let bash = ToolDefinition::Builtin(BuiltinTool {
name: "bash".to_string(),
display_name: None,
description: "bash".to_string(),
parameters: serde_json::json!({}),
policy: ToolPolicy::Auto,
category: None,
deferrable: DeferrablePolicy::default(),
hints: ToolHints::default()
.with_concurrency_class("session_workspace")
.with_cpu_bound(true),
full_parameters: None,
});
assert_eq!(bash.concurrency_class(), Some("session_workspace"));
assert!(bash.is_cpu_bound());
let json = serde_json::to_string(bash.hints()).unwrap();
let parsed: ToolHints = serde_json::from_str(&json).unwrap();
assert_eq!(
parsed.concurrency_class.as_deref(),
Some("session_workspace")
);
assert_eq!(parsed.cpu_bound, Some(true));
}
#[test]
fn test_builtin_tool_serialization() {
let json = r#"{
"type": "builtin",
"name": "fetch_data",
"description": "Fetch data from URL",
"parameters": {"type": "object"}
}"#;
let tool: ToolDefinition = serde_json::from_str(json).unwrap();
match tool {
ToolDefinition::Builtin(builtin) => {
assert_eq!(builtin.name, "fetch_data");
assert_eq!(builtin.policy, ToolPolicy::Auto);
}
_ => panic!("expected Builtin variant"),
}
}
#[test]
fn test_builtin_tool_requires_approval() {
let json = r#"{
"type": "builtin",
"name": "delete_file",
"description": "Delete a file",
"parameters": {"type": "object"},
"policy": "requires_approval"
}"#;
let tool: ToolDefinition = serde_json::from_str(json).unwrap();
match tool {
ToolDefinition::Builtin(builtin) => {
assert_eq!(builtin.policy, ToolPolicy::RequiresApproval);
}
_ => panic!("expected Builtin variant"),
}
}
#[test]
fn test_tool_call_serialization() {
let tool_call = ToolCall {
id: "call_123".to_string(),
name: "get_weather".to_string(),
arguments: serde_json::json!({"city": "New York"}),
};
let json = serde_json::to_string(&tool_call).unwrap();
let parsed: ToolCall = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.id, tool_call.id);
assert_eq!(parsed.name, tool_call.name);
}
#[test]
fn test_tool_result_serialization() {
let result = ToolResult {
tool_call_id: "call_123".to_string(),
result: Some(serde_json::json!({"temperature": 72})),
images: None,
error: None,
connection_required: None,
raw_output: None,
};
let json = serde_json::to_string(&result).unwrap();
let parsed: ToolResult = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.tool_call_id, result.tool_call_id);
assert!(parsed.result.is_some());
assert!(parsed.error.is_none());
}
#[test]
fn test_tool_definition_accessor_methods() {
let tool = ToolDefinition::Builtin(BuiltinTool {
name: "test_tool".to_string(),
display_name: None,
description: "A test tool".to_string(),
parameters: serde_json::json!({"type": "object"}),
policy: ToolPolicy::RequiresApproval,
category: None,
deferrable: DeferrablePolicy::default(),
hints: ToolHints::default(),
full_parameters: None,
});
assert_eq!(tool.name(), "test_tool");
assert_eq!(tool.display_name(), None);
assert_eq!(tool.description(), "A test tool");
assert_eq!(tool.parameters(), &serde_json::json!({"type": "object"}));
assert_eq!(tool.policy(), &ToolPolicy::RequiresApproval);
}
#[test]
fn test_tool_definition_display_name_accessor() {
let builtin = ToolDefinition::Builtin(BuiltinTool {
name: "get_weather".to_string(),
display_name: Some("Get Weather".to_string()),
description: "Gets weather".to_string(),
parameters: serde_json::json!({}),
policy: ToolPolicy::Auto,
category: None,
deferrable: DeferrablePolicy::default(),
hints: ToolHints::default(),
full_parameters: None,
});
assert_eq!(builtin.display_name(), Some("Get Weather"));
let client = ToolDefinition::ClientSide(ClientSideTool {
name: "deploy".to_string(),
display_name: Some("Deploy".to_string()),
description: "Deploys".to_string(),
parameters: serde_json::json!({}),
category: None,
deferrable: DeferrablePolicy::default(),
hints: ToolHints::default(),
full_parameters: None,
});
assert_eq!(client.display_name(), Some("Deploy"));
}
#[test]
fn test_display_name_serialization_skip_none() {
let tool = BuiltinTool {
name: "test".to_string(),
display_name: None,
description: "test".to_string(),
parameters: serde_json::json!({}),
policy: ToolPolicy::Auto,
category: None,
deferrable: DeferrablePolicy::default(),
hints: ToolHints::default(),
full_parameters: None,
};
let json = serde_json::to_string(&tool).unwrap();
assert!(!json.contains("display_name"));
let tool_with = BuiltinTool {
name: "test".to_string(),
display_name: Some("Test".to_string()),
description: "test".to_string(),
parameters: serde_json::json!({}),
policy: ToolPolicy::Auto,
category: None,
deferrable: DeferrablePolicy::default(),
hints: ToolHints::default(),
full_parameters: None,
};
let json = serde_json::to_string(&tool_with).unwrap();
assert!(json.contains("\"display_name\":\"Test\""));
}
#[test]
fn test_tool_call_to_openai_format() {
let tool_call = ToolCall {
id: "call_123".to_string(),
name: "get_weather".to_string(),
arguments: serde_json::json!({"location": "Tokyo", "units": "celsius"}),
};
let converted = tool_call.to_openai_format();
assert_eq!(converted["id"], "call_123");
assert_eq!(converted["type"], "function");
assert_eq!(converted["function"]["name"], "get_weather");
let args: serde_json::Value =
serde_json::from_str(converted["function"]["arguments"].as_str().unwrap()).unwrap();
assert_eq!(args["location"], "Tokyo");
assert_eq!(args["units"], "celsius");
}
#[test]
fn test_tool_call_to_openai_format_empty_arguments() {
let tool_call = ToolCall {
id: "call_456".to_string(),
name: "list_files".to_string(),
arguments: serde_json::json!({}),
};
let converted = tool_call.to_openai_format();
assert_eq!(converted["id"], "call_456");
assert_eq!(converted["function"]["name"], "list_files");
assert_eq!(converted["function"]["arguments"], "{}");
}
#[test]
fn test_client_side_tool_serialization() {
let json = r#"{
"type": "client_side",
"name": "browser_click",
"description": "Click an element in the browser",
"parameters": {"type": "object", "properties": {"selector": {"type": "string"}}}
}"#;
let tool: ToolDefinition = serde_json::from_str(json).unwrap();
match &tool {
ToolDefinition::ClientSide(client) => {
assert_eq!(client.name, "browser_click");
assert_eq!(client.description, "Click an element in the browser");
}
_ => panic!("expected ClientSide variant"),
}
assert_eq!(tool.name(), "browser_click");
assert_eq!(tool.policy(), &ToolPolicy::ClientSide);
}
#[test]
fn test_client_side_tool_roundtrip() {
let tool = ToolDefinition::ClientSide(ClientSideTool {
name: "run_test".to_string(),
display_name: None,
description: "Run a test suite".to_string(),
parameters: serde_json::json!({"type": "object"}),
category: None,
deferrable: DeferrablePolicy::default(),
hints: ToolHints::default(),
full_parameters: None,
});
let json = serde_json::to_string(&tool).unwrap();
let parsed: ToolDefinition = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.name(), "run_test");
assert_eq!(parsed.description(), "Run a test suite");
assert_eq!(parsed.policy(), &ToolPolicy::ClientSide);
}
#[test]
fn test_client_side_tool_accessor_methods() {
let tool = ToolDefinition::ClientSide(ClientSideTool {
name: "deploy_app".to_string(),
display_name: None,
description: "Deploy application to staging".to_string(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"env": {"type": "string"}
},
"required": ["env"]
}),
category: None,
deferrable: DeferrablePolicy::default(),
hints: ToolHints::default(),
full_parameters: None,
});
assert_eq!(tool.name(), "deploy_app");
assert_eq!(tool.description(), "Deploy application to staging");
assert_eq!(tool.policy(), &ToolPolicy::ClientSide);
assert!(tool.parameters().get("properties").is_some());
}
#[test]
fn test_client_side_tool_policy_always_client_side() {
let tool = ToolDefinition::ClientSide(ClientSideTool {
name: "any_tool".to_string(),
display_name: None,
description: "".to_string(),
parameters: serde_json::json!({}),
category: None,
deferrable: DeferrablePolicy::default(),
hints: ToolHints::default(),
full_parameters: None,
});
assert_eq!(tool.policy(), &ToolPolicy::ClientSide);
}
#[test]
fn test_tool_policy_serialization() {
assert_eq!(
serde_json::to_string(&ToolPolicy::ClientSide).unwrap(),
r#""client_side""#
);
assert_eq!(
serde_json::to_string(&ToolPolicy::Auto).unwrap(),
r#""auto""#
);
assert_eq!(
serde_json::to_string(&ToolPolicy::RequiresApproval).unwrap(),
r#""requires_approval""#
);
}
#[test]
fn test_mixed_tool_definitions_in_vec() {
let tools = vec![
ToolDefinition::Builtin(BuiltinTool {
name: "server_tool".to_string(),
display_name: None,
description: "A server tool".to_string(),
parameters: serde_json::json!({"type": "object"}),
policy: ToolPolicy::Auto,
category: None,
deferrable: DeferrablePolicy::default(),
hints: ToolHints::default(),
full_parameters: None,
}),
ToolDefinition::ClientSide(ClientSideTool {
name: "client_tool".to_string(),
display_name: None,
description: "A client tool".to_string(),
parameters: serde_json::json!({"type": "object"}),
category: None,
deferrable: DeferrablePolicy::default(),
hints: ToolHints::default(),
full_parameters: None,
}),
];
let json = serde_json::to_string(&tools).unwrap();
let parsed: Vec<ToolDefinition> = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.len(), 2);
assert!(matches!(&parsed[0], ToolDefinition::Builtin(_)));
assert!(matches!(&parsed[1], ToolDefinition::ClientSide(_)));
assert_eq!(parsed[0].policy(), &ToolPolicy::Auto);
assert_eq!(parsed[1].policy(), &ToolPolicy::ClientSide);
}
#[test]
fn test_tool_hints_default_is_empty() {
let hints = ToolHints::default();
assert!(hints.is_empty());
assert_eq!(hints.readonly, None);
assert_eq!(hints.destructive, None);
assert_eq!(hints.idempotent, None);
assert_eq!(hints.open_world, None);
assert_eq!(hints.requires_secrets, None);
assert_eq!(hints.long_running, None);
}
#[test]
fn test_tool_hints_builder() {
let hints = ToolHints::default()
.with_readonly(true)
.with_destructive(false)
.with_idempotent(true)
.with_open_world(true)
.with_requires_secrets(true)
.with_long_running(false);
assert!(!hints.is_empty());
assert_eq!(hints.readonly, Some(true));
assert_eq!(hints.destructive, Some(false));
assert_eq!(hints.idempotent, Some(true));
assert_eq!(hints.open_world, Some(true));
assert_eq!(hints.requires_secrets, Some(true));
assert_eq!(hints.long_running, Some(false));
}
#[test]
fn test_tool_hints_serialization_skip_empty() {
let tool = BuiltinTool {
name: "test".to_string(),
display_name: None,
description: "test".to_string(),
parameters: serde_json::json!({}),
policy: ToolPolicy::Auto,
category: None,
deferrable: DeferrablePolicy::default(),
hints: ToolHints::default(),
full_parameters: None,
};
let json = serde_json::to_string(&tool).unwrap();
assert!(!json.contains("hints"), "empty hints should be skipped");
}
#[test]
fn test_tool_hints_serialization_present() {
let tool = BuiltinTool {
name: "test".to_string(),
display_name: None,
description: "test".to_string(),
parameters: serde_json::json!({}),
policy: ToolPolicy::Auto,
category: None,
deferrable: DeferrablePolicy::default(),
hints: ToolHints::default()
.with_readonly(true)
.with_idempotent(true),
full_parameters: None,
};
let json = serde_json::to_string(&tool).unwrap();
assert!(json.contains("\"hints\""));
assert!(json.contains("\"readonly\":true"));
assert!(json.contains("\"idempotent\":true"));
assert!(!json.contains("destructive"));
assert!(!json.contains("open_world"));
}
#[test]
fn test_tool_hints_deserialization_missing() {
let json = r#"{
"type": "builtin",
"name": "test",
"description": "test",
"parameters": {}
}"#;
let tool: ToolDefinition = serde_json::from_str(json).unwrap();
assert!(tool.hints().is_empty());
}
#[test]
fn test_tool_hints_deserialization_present() {
let json = r#"{
"type": "builtin",
"name": "test",
"description": "test",
"parameters": {},
"hints": {"readonly": true, "open_world": true, "requires_secrets": true}
}"#;
let tool: ToolDefinition = serde_json::from_str(json).unwrap();
let hints = tool.hints();
assert_eq!(hints.readonly, Some(true));
assert_eq!(hints.open_world, Some(true));
assert_eq!(hints.requires_secrets, Some(true));
assert_eq!(hints.destructive, None);
assert_eq!(hints.idempotent, None);
assert_eq!(hints.long_running, None);
}
#[test]
fn test_tool_definition_with_hints_builder() {
let tool = ToolDefinition::Builtin(BuiltinTool {
name: "test".to_string(),
display_name: None,
description: "test".to_string(),
parameters: serde_json::json!({}),
policy: ToolPolicy::Auto,
category: None,
deferrable: DeferrablePolicy::default(),
hints: ToolHints::default(),
full_parameters: None,
})
.with_hints(ToolHints::default().with_readonly(true));
assert_eq!(tool.hints().readonly, Some(true));
}
#[test]
fn test_with_human_intent_argument_adds_optional_schema_property() {
let tool = ToolDefinition::Builtin(BuiltinTool {
name: "manage_harnesses".to_string(),
display_name: Some("Manage Harnesses".to_string()),
description: "Manage harnesses".to_string(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"operation": { "type": "string", "enum": ["list"] }
},
"required": ["operation"],
"additionalProperties": false
}),
policy: ToolPolicy::Auto,
category: None,
deferrable: DeferrablePolicy::default(),
hints: ToolHints::default(),
full_parameters: None,
})
.with_human_intent_argument();
let params = tool.parameters();
assert_eq!(
params["properties"][HUMAN_INTENT_ARGUMENT]["type"],
"string"
);
assert!(
params["properties"][HUMAN_INTENT_ARGUMENT]["description"]
.as_str()
.unwrap()
.contains("Listing all harnesses")
);
assert!(
!params["required"]
.as_array()
.unwrap()
.iter()
.any(|item| item.as_str() == Some(HUMAN_INTENT_ARGUMENT))
);
assert_eq!(params["additionalProperties"], false);
}
#[test]
fn test_tool_call_execution_arguments_strip_human_intent() {
let tool_call = ToolCall {
id: "call_1".to_string(),
name: "manage_harnesses".to_string(),
arguments: serde_json::json!({
"operation": "list",
"human_intent": "Listing all harnesses"
}),
};
assert_eq!(
tool_call.execution_arguments(),
serde_json::json!({ "operation": "list" })
);
assert_eq!(
human_intent(&tool_call.arguments),
Some("Listing all harnesses")
);
}
}