use acorn_core::prelude::alloc::{format, vec, String, ToString, Vec};
use acorn_core::{AcornError, AcornResult};
use core::fmt;
use schemars::{schema_for, JsonSchema};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct NeedleToolDefinition {
pub name: String,
pub description: String,
pub parameters: Value,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub triggers: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ToolDefinition {
pub name: String,
pub title: String,
pub description: String,
pub input_schema: Value,
pub output_schema: Option<Value>,
pub effects: ToolEffects,
pub exposure: ToolExposure,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ToolEffects {
pub read_only: bool,
pub destructive: bool,
pub idempotent: bool,
pub open_world: bool,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ToolExposure {
pub needle: bool,
pub mcp: bool,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ToolResult {
pub content: String,
pub structured_content: Value,
pub is_error: bool,
}
impl From<ToolDefinition> for NeedleToolDefinition {
fn from(definition: ToolDefinition) -> Self {
Self {
name: definition.name,
description: definition.description,
parameters: definition.input_schema,
triggers: Vec::new(),
}
}
}
impl fmt::Display for ToolDefinition {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let description = match self.name.as_str() {
| "acorn.check_research_activity_data" | "acorn.validate_research_activity_data" =>
"Validate an explicitly supplied, complete ACORN research activity JSON object. Never synthesize activity JSON from prose and never use this tool to modify or persist data.",
| "acorn.export_research_activity_data" =>
"Serialize an explicitly supplied, complete ACORN research activity JSON object. Do not use for unrelated information or synthesize activity JSON from prose.",
| "acorn.export_research_activity_updates" =>
"Render updates from an explicitly supplied, complete ACORN research activity JSON object at or after a supplied RFC 3339 timestamp.",
| "acorn.format_and_enrich_research_activity_data" =>
"Normalize an explicitly supplied, complete ACORN research activity JSON object and enrich it using configured metadata providers.",
| "acorn.propose_logbook_graduation" =>
"Propose reviewable changes from an explicitly supplied, complete ACORN research activity JSON object. This tool cannot apply, persist, or directly modify data.",
| _ => &self.description,
};
formatter.write_str(description)
}
}
impl ToolDefinition {
pub fn new<I: JsonSchema, O: JsonSchema>(name: &str, title: &str, description: &str) -> Self {
Self {
name: name.to_string(),
title: title.to_string(),
description: description.to_string(),
input_schema: serde_json::to_value(schema_for!(I)).unwrap_or_else(|_| serde_json::json!({ "type": "object" })),
output_schema: serde_json::to_value(schema_for!(O)).ok(),
effects: ToolEffects {
read_only: true,
destructive: false,
idempotent: true,
open_world: false,
},
exposure: ToolExposure { needle: true, mcp: true },
}
}
pub fn needle_triggers(&self) -> Vec<String> {
match self.name.as_str() {
| "acorn.version" => vec!["acorn.*version".to_string(), "version.*acorn".to_string()],
| _ => Vec::new(),
}
}
pub fn validate(&self) -> AcornResult<()> {
let valid_characters = self
.name
.chars()
.all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.'));
let valid_name = (1..=128).contains(&self.name.len()) && valid_characters;
[
(!valid_name).then(|| {
AcornError::new(format!(
"Invalid tool name '{}'; use 1-128 ASCII letters, digits, '.', '_' or '-'",
self.name
))
}),
self.title
.trim()
.is_empty()
.then(|| AcornError::new(format!("Tool '{}' requires a title", self.name))),
self.description
.trim()
.is_empty()
.then(|| AcornError::new(format!("Tool '{}' requires a description", self.name))),
(self.input_schema.get("type").and_then(Value::as_str) != Some("object"))
.then(|| AcornError::new(format!("Tool '{}' input schema must describe an object", self.name))),
self.output_schema
.as_ref()
.and_then(|schema| schema.get("type"))
.and_then(Value::as_str)
.is_some_and(|kind| kind != "object")
.then(|| AcornError::new(format!("Tool '{}' output schema must describe an object", self.name))),
(self.effects.read_only && self.effects.destructive)
.then(|| AcornError::new(format!("Read-only tool '{}' cannot be destructive", self.name))),
]
.into_iter()
.flatten()
.next()
.map_or(Ok(()), Err)
}
}
impl ToolResult {
pub fn success(value: Value) -> AcornResult<Self> {
serde_json::to_string(&value)
.map(|content| Self {
content,
structured_content: value,
is_error: false,
})
.map_err(|why| AcornError::new(format!("Failed to serialize tool result — {why}")))
}
}