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)]
#[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>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
impl ToolHints {
pub fn is_empty(&self) -> bool {
*self == Self::default()
}
pub fn with_metadata(mut self, value: serde_json::Value) -> Self {
self.metadata = Some(value);
self
}
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>,
}
pub const URL_ELICITATION_REQUIRED_CODE: &str = "url_elicitation_required";
pub const CONFIRM_URL_ELICITATION_TOOL: &str = "confirm_url_elicitation";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct UrlElicitationRequired {
pub code: String,
pub error: String,
pub url: String,
pub url_host: String,
pub url_is_punycode: bool,
pub server: String,
pub tool: String,
pub retry_tool: String,
pub message: String,
pub declined: bool,
}
impl UrlElicitationRequired {
pub fn from_tool_result(result: &ToolResult) -> Option<Self> {
let value = result.result.as_ref()?;
if value.get("code")?.as_str()? != URL_ELICITATION_REQUIRED_CODE {
return None;
}
serde_json::from_value(value.clone()).ok()
}
}
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::*;
use serde_json::json;
fn definition(kind: &str) -> ToolDefinition {
serde_json::from_value(json!({"type":kind,"name":"tool","description":"Description","parameters":{"type":"object","properties":{"input":{"type":"string"}},"required":["input"]}})).unwrap()
}
#[test]
fn tool_variants_have_complete_literal_wire_defaults_and_effective_policies() {
for kind in ["builtin", "client_side"] {
let tool = definition(kind);
let mut expected = json!({"type":kind,"name":"tool","description":"Description","parameters":{"type":"object","properties":{"input":{"type":"string"}},"required":["input"]}});
if kind == "builtin" {
expected["policy"] = json!("auto");
}
assert_eq!(serde_json::to_value(&tool).unwrap(), expected);
assert_eq!(tool.name(), "tool");
assert_eq!(tool.description(), "Description");
assert_eq!(tool.display_name(), None);
assert_eq!(
tool.parameters(),
&json!({"type":"object","properties":{"input":{"type":"string"}},"required":["input"]})
);
assert_eq!(
tool.policy(),
if kind == "builtin" {
&ToolPolicy::Auto
} else {
&ToolPolicy::ClientSide
}
);
assert_eq!(tool.deferrable(), &DeferrablePolicy::Automatic);
assert!(tool.hints().is_empty());
assert_eq!(tool.concurrency_class(), None);
assert!(!tool.is_cpu_bound());
assert_eq!(tool.side_effect_class(), SideEffectClass::AtMostOnce);
}
let mixed:Vec<ToolDefinition>=serde_json::from_value(json!([
{"type":"builtin","name":"server","description":"Server","parameters":{},"policy":"requires_approval"},
{"type":"client_side","name":"client","description":"Client","parameters":{},"policy":"auto"}
])).unwrap();
assert_eq!(
(
mixed[0].name(),
mixed[0].policy(),
mixed[1].name(),
mixed[1].policy()
),
(
"server",
&ToolPolicy::RequiresApproval,
"client",
&ToolPolicy::ClientSide
)
);
assert!(matches!(&mixed[0], ToolDefinition::Builtin(_)));
assert!(matches!(&mixed[1], ToolDefinition::ClientSide(_)));
for (policy, wire) in [
(ToolPolicy::Auto, "auto"),
(ToolPolicy::RequiresApproval, "requires_approval"),
(ToolPolicy::ClientSide, "client_side"),
] {
assert_eq!(serde_json::to_value(&policy).unwrap(), json!(wire));
assert_eq!(
serde_json::from_value::<ToolPolicy>(json!(wire)).unwrap(),
policy
);
}
}
#[test]
fn hints_builders_preserve_false_values_metadata_and_scheduling_fields() {
for kind in ["builtin", "client_side"] {
let reader = definition(kind).with_hints(ToolHints::default().with_readonly(true));
assert_eq!(reader.concurrency_class(), None);
assert!(!reader.is_cpu_bound());
assert_eq!(reader.side_effect_class(), SideEffectClass::AtMostOnce);
}
for value in [false, true] {
let hints = ToolHints::default()
.with_readonly(value)
.with_destructive(value)
.with_idempotent(value)
.with_open_world(value)
.with_requires_secrets(value)
.with_long_running(value)
.with_supports_background(value)
.with_cpu_bound(value)
.with_persist_output(value)
.with_concurrency_class("session_workspace")
.with_narration_noun("file")
.with_side_effect_class(SideEffectClass::Idempotent)
.with_capability_attribution("files", Some("Files"))
.with_metadata(json!({"risk_tier":"high","nested":[1,false,null]}));
let expected = json!({"readonly":value,"destructive":value,"idempotent":value,"open_world":value,"requires_secrets":value,"long_running":value,"supports_background":value,"cpu_bound":value,"persist_output":value,"concurrency_class":"session_workspace","narration_noun":"file","side_effect_class":"Idempotent","capability_id":"files","capability_name":"Files","metadata":{"risk_tier":"high","nested":[1,false,null]}});
assert_eq!(serde_json::to_value(&hints).unwrap(), expected);
assert_eq!(
serde_json::from_value::<ToolHints>(expected).unwrap(),
hints
);
for kind in ["builtin", "client_side"] {
let tool = definition(kind)
.with_hints(hints.clone())
.with_category("workspace")
.with_capability_attribution("new-files", Some("New Files"));
assert_eq!(tool.category(), Some("workspace"));
assert_eq!(tool.concurrency_class(), Some("session_workspace"));
assert_eq!(tool.is_cpu_bound(), value);
assert_eq!(tool.side_effect_class(), SideEffectClass::Idempotent);
assert_eq!(
tool.capability_attribution(),
Some(("new-files", Some("New Files")))
);
let mut expected_hints = serde_json::to_value(&hints).unwrap();
expected_hints["capability_id"] = json!("new-files");
expected_hints["capability_name"] = json!("New Files");
assert_eq!(serde_json::to_value(tool.hints()).unwrap(), expected_hints);
}
}
assert_eq!(
serde_json::to_value(ToolHints::default()).unwrap(),
json!({})
);
let metadata = ToolHints::default().with_metadata(json!({"any":"thing"}));
assert!(!metadata.is_empty());
assert_eq!(
serde_json::to_value(metadata).unwrap(),
json!({"metadata":{"any":"thing"}})
);
for class in [
SideEffectClass::Pure,
SideEffectClass::Idempotent,
SideEffectClass::AtMostOnce,
] {
assert_eq!(
ToolHints::default()
.with_side_effect_class(class.clone())
.effective_side_effect_class(),
class
);
}
}
#[test]
fn tool_display_and_deferred_schemas_survive_both_wire_variants() {
for kind in ["builtin", "client_side"] {
for (deferrable, wire) in [
(DeferrablePolicy::Never, Some("never")),
(DeferrablePolicy::Automatic, None),
(DeferrablePolicy::Always, Some("always")),
] {
let mut payload = json!({"type":kind,"name":"tool","display_name":"Display","description":"Description","parameters":{"type":"object"},"full_parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]},"category":"files"});
if let Some(wire) = wire {
payload["deferrable"] = json!(wire);
}
let tool: ToolDefinition = serde_json::from_value(payload.clone()).unwrap();
assert_eq!(tool.display_name(), Some("Display"));
assert_eq!(tool.deferrable(), &deferrable);
assert_eq!(tool.parameters(), &json!({"type":"object"}));
assert_eq!(
tool.full_parameters(),
&json!({"type":"object","properties":{"path":{"type":"string"}},"required":["path"]})
);
if kind == "builtin" {
payload["policy"] = json!("auto");
}
assert_eq!(serde_json::to_value(tool).unwrap(), payload);
}
let tool = definition(kind);
assert_eq!(tool.full_parameters(), tool.parameters());
}
}
#[test]
fn tool_call_wire_and_openai_arguments_preserve_the_complete_payload() {
for (arguments, text) in [
(json!({"city":"New York"}), r#"{"city":"New York"}"#),
(json!({}), "{}"),
(
json!({"count":9007199254740993_u64,"text":"line\nquoted\""}),
r#"{"count":9007199254740993,"text":"line\nquoted\""}"#,
),
] {
let call = ToolCall {
id: "call_123".into(),
name: "get_weather".into(),
arguments: arguments.clone(),
};
assert_eq!(
serde_json::to_value(&call).unwrap(),
json!({"id":"call_123","name":"get_weather","arguments":arguments})
);
let parsed: ToolCall = serde_json::from_value(
json!({"id":"call_123","name":"get_weather","arguments":arguments}),
)
.unwrap();
assert_eq!(parsed.arguments, arguments);
assert_eq!(
call.to_openai_format(),
json!({"id":"call_123","type":"function","function":{"name":"get_weather","arguments":text}})
);
}
}
#[test]
fn tool_result_wire_excludes_raw_output_but_preserves_images_errors_and_data() {
let expected = json!({"tool_call_id":"call_123","result":{"temperature":72},"images":[{"base64":"aGk=","media_type":"image/png"}],"error":"partial failure","connection_required":"provider"});
let mut injected = expected.clone();
injected["raw_output"] = json!("private-raw");
let mut result: ToolResult = serde_json::from_value(injected).unwrap();
assert!(result.raw_output.is_none());
result.raw_output = Some("private-raw".into());
assert_eq!(serde_json::to_value(result).unwrap(), expected);
assert_eq!(
serde_json::to_value(ToolResult::error("failed")).unwrap(),
json!({"tool_call_id":"","result":null,"error":"failed"})
);
let success: ToolResult = serde_json::from_value(
json!({"tool_call_id":"call_123","result":{"temperature":72},"error":null}),
)
.unwrap();
assert_eq!(
serde_json::to_value(success).unwrap(),
json!({"tool_call_id":"call_123","result":{"temperature":72},"error":null})
);
}
#[test]
fn narration_schema_is_optional_idempotent_and_does_not_mutate_input_definitions() {
let original = vec![definition("builtin"), definition("client_side")];
let before = serde_json::to_value(&original).unwrap();
let augmented = add_human_intent_to_tool_definitions(&original);
assert_eq!(serde_json::to_value(&original).unwrap(), before);
let property = json!({"type":"string","description":"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.","maxLength":120});
for tool in &augmented {
assert_eq!(
tool.parameters(),
&json!({"type":"object","properties":{"input":{"type":"string"},"human_intent":property},"required":["input"]})
);
}
assert_eq!(
serde_json::to_value(add_human_intent_to_tool_definitions(&augmented)).unwrap(),
serde_json::to_value(&augmented).unwrap()
);
let mut closed = json!({"properties":{"operation":{"type":"string","enum":["list"]}},"required":["operation"],"additionalProperties":false});
add_human_intent_to_schema(&mut closed);
assert_eq!(
closed,
json!({"type":"object","properties":{"operation":{"type":"string","enum":["list"]},"human_intent":property},"required":["operation"],"additionalProperties":false})
);
let mut empty = json!({});
add_human_intent_to_schema(&mut empty);
assert_eq!(
empty,
json!({"type":"object","properties":{"human_intent":property}})
);
for mut scalar in [json!(null), json!(false), json!([])] {
let before = scalar.clone();
add_human_intent_to_schema(&mut scalar);
assert_eq!(scalar, before);
}
}
#[test]
fn execution_strips_only_top_level_narration_and_trims_display_text() {
for (value, expected) in [
(
json!(" Listing all harnesses "),
Some("Listing all harnesses"),
),
(json!(" \t"), None),
(json!(7), None),
(json!(null), None),
] {
let arguments = json!({"operation":"list","human_intent":value,"nested":{"human_intent":"ordinary data"}});
let call = ToolCall {
id: "call".into(),
name: "manage_harnesses".into(),
arguments: arguments.clone(),
};
assert_eq!(human_intent(&call.arguments), expected);
assert_eq!(
call.execution_arguments(),
json!({"operation":"list","nested":{"human_intent":"ordinary data"}})
);
assert_eq!(call.arguments, arguments);
}
for value in [json!(null), json!([1, "text"]), json!({"operation":"list"})] {
assert_eq!(strip_human_intent_argument(&value), value);
assert_eq!(human_intent(&value), None);
}
}
#[test]
fn url_elicitation_requires_discriminator_and_complete_payload() {
let payload = json!({"code":"url_elicitation_required","error":"Waiting","url":"https://consent.example/path","url_host":"consent.example","url_is_punycode":false,"server":"server","tool":"tool","retry_tool":"mcp_server_tool","message":"Connect account","declined":false});
let mut result = ToolResult::error("unrelated");
result.result = Some(payload.clone());
assert_eq!(
serde_json::to_value(UrlElicitationRequired::from_tool_result(&result).unwrap())
.unwrap(),
payload
);
let mut declined = payload.clone();
declined["declined"] = json!(true);
result.result = Some(declined.clone());
assert_eq!(
serde_json::to_value(UrlElicitationRequired::from_tool_result(&result).unwrap())
.unwrap(),
declined
);
for malformed in [
None,
Some(json!(null)),
Some(json!({"code":"url_elicitation_required"})),
Some({
let mut value = payload.clone();
value["code"] = json!("connection_required");
value
}),
Some({
let mut value = payload.clone();
value["declined"] = json!("false");
value
}),
] {
result.result = malformed;
assert!(UrlElicitationRequired::from_tool_result(&result).is_none());
}
}
}