use std::sync::Arc; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use serde_json::Value; use thiserror::Error; #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct ToolDefinition { pub name: String, pub description: Option, pub input_schema: Value, /// MCP `outputSchema`, when the tool declares one. #[serde(default, skip_serializing_if = "Option::is_none")] pub output_schema: Option, /// MCP `readOnlyHint` annotation. #[serde(default, skip_serializing_if = "Option::is_none")] pub read_only: Option, /// MCP `destructiveHint` annotation. #[serde(default, skip_serializing_if = "Option::is_none")] pub destructive: Option, } impl ToolDefinition { pub fn new( name: impl Into, description: Option>, input_schema: Value, ) -> Self { Self { name: name.into(), description: description.map(Into::into), input_schema, ..Self::default() } } /// True when the tool declares that it may change state. pub fn is_mutating(&self) -> bool { self.destructive == Some(true) || self.read_only == Some(false) } } #[derive(Clone, Debug, Default, PartialEq)] pub struct ToolCallOutput { pub content: String, pub is_error: bool, /// Metadata for session records (e.g. code mode's nested MCP calls). /// Never sent to the model. pub audit: Option, } impl ToolCallOutput { pub fn text(content: impl Into) -> Self { Self { content: content.into(), ..Self::default() } } pub fn error(content: impl Into) -> Self { Self { content: content.into(), is_error: true, audit: None, } } } #[derive(Clone, Debug, PartialEq, Eq, Error)] #[error("{0}")] pub struct ToolError(pub String); impl ToolError { pub fn new(message: impl Into) -> Self { Self(message.into()) } } #[async_trait] pub trait ToolExecutor: Send + Sync { async fn list_tools(&self) -> Result, ToolError>; async fn call_tool(&self, name: &str, arguments: Value) -> Result; /// Executor to use for one backend run. Stateful executors (code mode) /// return a fresh scope so per-run state is never shared across runs. fn run_scope(&self) -> Option> { None } /// Executor for one conversation thread: state persists across that /// thread's runs. Defaults to a fresh `run_scope`. fn thread_scope(&self, _thread_id: &str) -> Option> { self.run_scope() } /// State the model should know before this run (code mode: variables /// still in scope). Appended to the newest user message, never to the /// tool description, so the cached prompt prefix stays stable. fn context_note(&self) -> Option { None } } /// Tool output for an NDJSON log record: JSON text is decoded and re-emitted /// as JSON; anything else is logged as a string truncated to `max_chars`. pub fn content_for_log(text: &str, max_chars: usize) -> Value { if let Ok(value) = serde_json::from_str::(text) { return value; } if text.chars().count() <= max_chars { Value::String(text.to_string()) } else { let mut truncated: String = text.chars().take(max_chars).collect(); truncated.push_str("...[truncated]"); Value::String(truncated) } } /// Receives tool call/result notifications from a backend. pub trait ToolObserver: Send + Sync { fn tool_call(&self, id: &str, name: &str, arguments: &Value); /// `audit` is session metadata (e.g. nested calls); not model input. fn tool_result(&self, id: &str, content: &str, is_error: bool, audit: Option<&Value>); }