use crate::compression::CompressionLevel;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Tool {
pub name: String,
pub description: Option<String>,
pub input_schema: serde_json::Value,
}
impl Tool {
pub fn new(
name: impl Into<String>,
description: impl Into<Option<String>>,
input_schema: serde_json::Value,
) -> Self {
Self {
name: name.into(),
description: description.into(),
input_schema,
}
}
pub fn param_names(&self) -> Vec<String> {
self.input_schema
.get("properties")
.and_then(serde_json::Value::as_object)
.map(|properties| properties.keys().cloned().collect())
.unwrap_or_default()
}
}
#[derive(Debug, Clone)]
pub struct CompressionEngine {
level: CompressionLevel,
}
impl CompressionEngine {
pub fn new(level: CompressionLevel) -> Self {
Self { level }
}
pub fn format_listing(&self, tools: &[Tool]) -> String {
if self.level == CompressionLevel::Max {
return String::new();
}
tools
.iter()
.map(|tool| self.format_tool(tool))
.collect::<Vec<_>>()
.join("\n")
}
pub fn format_tool(&self, tool: &Tool) -> String {
format_tool_at_level(tool, &self.level)
}
pub fn get_schema<'a>(&self, tools: &'a [Tool], name: &str) -> Option<&'a Tool> {
tools.iter().find(|tool| tool.name == name)
}
pub fn format_schema_response(tool: &Tool) -> String {
let tool_description = format_tool_at_level(tool, &CompressionLevel::Low);
let schema = serde_json::to_string_pretty(&tool.input_schema)
.unwrap_or_else(|_| tool.input_schema.to_string());
format!("{tool_description}\n\n{schema}")
}
}
fn format_tool_at_level(tool: &Tool, level: &CompressionLevel) -> String {
match level {
CompressionLevel::Max => format!("<tool>{}</tool>", tool.name),
CompressionLevel::High => format!("<tool>{}({})</tool>", tool.name, format_args(tool)),
CompressionLevel::Medium => format_with_description(tool, first_sentence_description(tool)),
CompressionLevel::Low => format_with_description(tool, tool.description.as_deref()),
}
}
fn format_with_description(tool: &Tool, description: Option<&str>) -> String {
let signature = format!("{}({})", tool.name, format_args(tool));
match description.map(str::trim).filter(|description| !description.is_empty()) {
Some(description) => format!("<tool>{signature}: {description}</tool>"),
None => format!("<tool>{signature}</tool>"),
}
}
fn format_args(tool: &Tool) -> String {
tool.param_names().join(", ")
}
fn first_sentence_description(tool: &Tool) -> Option<&str> {
let description = tool.description.as_deref()?.trim();
let first_paragraph = description
.split("\n\n")
.map(str::trim)
.find(|paragraph| !paragraph.is_empty())
.unwrap_or(description);
let first_line = first_paragraph
.lines()
.map(str::trim)
.find(|line| !line.is_empty())
.unwrap_or(first_paragraph);
Some(first_line.split('.').next().unwrap_or_default().trim())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn fetch_tool() -> Tool {
Tool::new(
"fetch",
Some("Fetch a URL. Returns the raw content.".into()),
json!({
"type": "object",
"properties": {
"url": { "type": "string", "description": "Target URL" },
"timeout": { "type": "number", "description": "Timeout in seconds" }
},
"required": ["url"]
}),
)
}
fn multiline_tool() -> Tool {
Tool::new(
"multiline",
Some("First line description.\nSecond line continuation.".into()),
json!({ "type": "object", "properties": { "x": { "type": "string" } } }),
)
}
fn no_desc_tool() -> Tool {
Tool::new(
"ping",
None::<String>,
json!({ "type": "object", "properties": { "host": { "type": "string" } } }),
)
}
fn no_args_tool() -> Tool {
Tool::new(
"health",
Some("Check server health.".into()),
json!({ "type": "object", "properties": {} }),
)
}
#[test]
fn format_tool_max_name_only() {
let engine = CompressionEngine::new(CompressionLevel::Max);
assert_eq!(engine.format_tool(&fetch_tool()), "<tool>fetch</tool>");
}
#[test]
fn format_tool_max_no_description() {
let engine = CompressionEngine::new(CompressionLevel::Max);
assert_eq!(engine.format_tool(&no_desc_tool()), "<tool>ping</tool>");
}
#[test]
fn format_tool_high_name_and_args() {
let engine = CompressionEngine::new(CompressionLevel::High);
assert_eq!(engine.format_tool(&fetch_tool()), "<tool>fetch(url, timeout)</tool>");
}
#[test]
fn format_tool_high_no_args() {
let engine = CompressionEngine::new(CompressionLevel::High);
assert_eq!(engine.format_tool(&no_args_tool()), "<tool>health()</tool>");
}
#[test]
fn format_tool_medium_first_sentence() {
let engine = CompressionEngine::new(CompressionLevel::Medium);
let out = engine.format_tool(&fetch_tool());
assert_eq!(out, "<tool>fetch(url, timeout): Fetch a URL</tool>");
}
#[test]
fn format_tool_medium_first_line_of_multiline() {
let engine = CompressionEngine::new(CompressionLevel::Medium);
let out = engine.format_tool(&multiline_tool());
assert_eq!(out, "<tool>multiline(x): First line description</tool>");
}
#[test]
fn format_tool_low_and_medium_differ_for_paragraph_descriptions() {
let tool = Tool::new(
"search",
Some("\n\nSearch the web.\n\nLonger details that should only appear at low verbosity.".to_string()),
json!({}),
);
let low = CompressionEngine::new(CompressionLevel::Low);
let medium = CompressionEngine::new(CompressionLevel::Medium);
assert!(low
.format_tool(&tool)
.contains("Longer details that should only appear at low verbosity"));
assert_eq!(medium.format_tool(&tool), "<tool>search(): Search the web</tool>");
}
#[test]
fn format_tool_medium_no_description() {
let engine = CompressionEngine::new(CompressionLevel::Medium);
assert_eq!(engine.format_tool(&no_desc_tool()), "<tool>ping(host)</tool>");
}
#[test]
fn format_tool_low_full_description() {
let engine = CompressionEngine::new(CompressionLevel::Low);
assert_eq!(
engine.format_tool(&fetch_tool()),
"<tool>fetch(url, timeout): Fetch a URL. Returns the raw content.</tool>",
);
}
#[test]
fn format_tool_low_multiline_description_kept() {
let engine = CompressionEngine::new(CompressionLevel::Low);
let out = engine.format_tool(&multiline_tool());
assert!(out.contains("First line description."));
assert!(out.contains("Second line continuation."));
}
#[test]
fn format_tool_low_no_args() {
let engine = CompressionEngine::new(CompressionLevel::Low);
assert_eq!(engine.format_tool(&no_args_tool()), "<tool>health(): Check server health.</tool>");
}
#[test]
fn format_listing_max_returns_empty() {
let engine = CompressionEngine::new(CompressionLevel::Max);
assert_eq!(engine.format_listing(&[fetch_tool(), no_desc_tool()]), "");
}
#[test]
fn format_listing_empty_tools() {
for level in [CompressionLevel::Low, CompressionLevel::Medium, CompressionLevel::High] {
let engine = CompressionEngine::new(level);
assert_eq!(engine.format_listing(&[]), "");
}
}
#[test]
fn format_listing_multiple_tools_joined_with_newline() {
let engine = CompressionEngine::new(CompressionLevel::High);
let tools = vec![fetch_tool(), no_args_tool()];
let listing = engine.format_listing(&tools);
let lines: Vec<&str> = listing.lines().collect();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0], "<tool>fetch(url, timeout)</tool>");
assert_eq!(lines[1], "<tool>health()</tool>");
}
#[test]
fn format_listing_single_tool_no_trailing_newline() {
let engine = CompressionEngine::new(CompressionLevel::High);
let listing = engine.format_listing(&[fetch_tool()]);
assert!(!listing.ends_with('\n'));
}
#[test]
fn get_schema_found() {
let engine = CompressionEngine::new(CompressionLevel::Medium);
let tools = vec![fetch_tool()];
let result = engine.get_schema(&tools, "fetch");
assert!(result.is_some());
assert_eq!(result.unwrap().name, "fetch");
}
#[test]
fn get_schema_not_found() {
let engine = CompressionEngine::new(CompressionLevel::Medium);
let tools = vec![fetch_tool()];
assert!(engine.get_schema(&tools, "nonexistent").is_none());
}
#[test]
fn get_schema_empty_list() {
let engine = CompressionEngine::new(CompressionLevel::Medium);
assert!(engine.get_schema(&[], "fetch").is_none());
}
#[test]
fn format_schema_response_contains_low_description() {
let tool = fetch_tool();
let response = CompressionEngine::format_schema_response(&tool);
assert!(response.contains("<tool>fetch(url, timeout):"), "got: {response}");
assert!(response.contains("Fetch a URL. Returns the raw content."));
}
#[test]
fn format_schema_response_contains_json_schema() {
let tool = fetch_tool();
let response = CompressionEngine::format_schema_response(&tool);
assert!(response.contains("\"properties\""), "got: {response}");
assert!(response.contains("\"url\""));
}
#[test]
fn format_schema_response_blank_line_separator() {
let tool = fetch_tool();
let response = CompressionEngine::format_schema_response(&tool);
assert!(response.contains("\n\n"), "expected blank-line separator, got: {response}");
}
#[test]
fn param_names_returns_ordered_params() {
let tool = fetch_tool();
let names = tool.param_names();
assert_eq!(names, vec!["url", "timeout"]);
}
#[test]
fn param_names_empty_schema() {
let tool = no_args_tool();
assert_eq!(tool.param_names(), Vec::<String>::new());
}
}