use std::collections::BTreeMap;
use std::fmt;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
use crate::{
BoxFuture, PermissionRequest, ReadEvidenceStore, ScopedProjectContextProvider,
ToolAdvertisement, ToolCall, ToolResultStore,
};
pub trait CancellationSignal: Send + Sync {
fn is_cancelled(&self) -> bool;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct NeverCancelled;
impl CancellationSignal for NeverCancelled {
fn is_cancelled(&self) -> bool {
false
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolEffect {
Read,
Write,
Process,
Network,
UserInteraction,
Delegation,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SandboxMode {
#[default]
None,
Os,
}
#[derive(Clone)]
pub struct ToolContext {
pub workspace_root: PathBuf,
pub additional_roots: Vec<PathBuf>,
pub limits: ToolLimits,
pub read_evidence: Option<Arc<dyn ReadEvidenceStore>>,
pub tool_results: Option<Arc<dyn ToolResultStore>>,
pub project_context: Option<Arc<dyn ScopedProjectContextProvider>>,
pub cancellation: Arc<dyn CancellationSignal>,
pub sandbox: SandboxMode,
}
impl ToolContext {
pub fn new(workspace_root: PathBuf) -> Self {
Self {
workspace_root,
additional_roots: Vec::new(),
limits: ToolLimits::default(),
read_evidence: None,
tool_results: None,
project_context: None,
cancellation: Arc::new(NeverCancelled),
sandbox: SandboxMode::None,
}
}
}
impl fmt::Debug for ToolContext {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ToolContext")
.field("workspace_root", &self.workspace_root)
.field("additional_roots", &self.additional_roots)
.field("limits", &self.limits)
.field("has_read_evidence", &self.read_evidence.is_some())
.field("has_tool_result_store", &self.tool_results.is_some())
.field("has_project_context", &self.project_context.is_some())
.field("cancelled", &self.cancellation.is_cancelled())
.field("sandbox", &self.sandbox)
.finish()
}
}
#[derive(Clone, Copy, Debug)]
pub struct ToolLimits {
pub max_result_bytes: usize,
pub max_read_file_lines: usize,
pub max_read_file_line_bytes: usize,
pub max_list_entries: usize,
pub command_timeout: Option<Duration>,
}
impl Default for ToolLimits {
fn default() -> Self {
Self {
max_result_bytes: 64 * 1024,
max_read_file_lines: 400,
max_read_file_line_bytes: 2_000,
max_list_entries: 100,
command_timeout: None,
}
}
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
pub struct ToolOutput {
pub content: String,
#[serde(default)]
pub is_error: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub structured: Option<Value>,
#[serde(default)]
pub original_bytes: usize,
#[serde(default)]
pub truncated: bool,
#[serde(skip)]
pub durable_content: Option<String>,
}
impl fmt::Debug for ToolOutput {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ToolOutput")
.field("content", &self.content)
.field("is_error", &self.is_error)
.field("structured", &self.structured)
.field("original_bytes", &self.original_bytes)
.field("truncated", &self.truncated)
.field("has_durable_content", &self.durable_content.is_some())
.finish()
}
}
#[derive(Debug, Error)]
pub enum ToolError {
#[error("invalid arguments: {0}")]
InvalidArguments(String),
#[error("tool call is outside the active workspace: {0}")]
OutsideWorkspace(String),
#[error("permission denied: {0}")]
PermissionDenied(String),
#[error("tool execution failed: {0}")]
Execution(String),
#[error("tool was cancelled")]
Cancelled,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FileChangeReview {
pub path: PathBuf,
pub before: Option<Vec<u8>>,
pub after: Vec<u8>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CommandReview {
pub command: String,
pub cwd: PathBuf,
pub shell: PathBuf,
pub profile: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ToolReview {
FileChange(FileChangeReview),
Command(CommandReview),
}
pub trait PreparedToolAction: Send {
fn commit<'a>(
self: Box<Self>,
context: &'a ToolContext,
) -> BoxFuture<'a, Result<ToolOutput, ToolError>>;
}
pub struct PreparedToolCall {
pub tool_name: String,
pub permission_requests: Vec<PermissionRequest>,
pub irreversible: bool,
pub review: Option<ToolReview>,
action: Box<dyn PreparedToolAction>,
}
impl PreparedToolCall {
pub fn new(
tool_name: impl Into<String>,
permission_requests: Vec<PermissionRequest>,
irreversible: bool,
review: Option<ToolReview>,
action: impl PreparedToolAction + 'static,
) -> Self {
Self {
tool_name: tool_name.into(),
permission_requests,
irreversible,
review,
action: Box::new(action),
}
}
pub fn commit<'a>(
self,
context: &'a ToolContext,
) -> BoxFuture<'a, Result<ToolOutput, ToolError>> {
self.action.commit(context)
}
}
impl fmt::Debug for PreparedToolCall {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("PreparedToolCall")
.field("tool_name", &self.tool_name)
.field("permission_requests", &self.permission_requests)
.field("irreversible", &self.irreversible)
.field("review", &self.review)
.finish_non_exhaustive()
}
}
#[derive(Debug)]
pub enum ToolPreparation {
Direct {
permission_requests: Vec<PermissionRequest>,
irreversible: bool,
},
Prepared(PreparedToolCall),
}
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn input_schema(&self) -> Value;
fn effect(&self, arguments: &Value) -> Result<ToolEffect, ToolError>;
fn irreversible(&self, _arguments: &Value) -> Result<bool, ToolError> {
Ok(false)
}
fn project_context_targets(
&self,
_context: &ToolContext,
_arguments: &Value,
) -> Result<Vec<PathBuf>, ToolError> {
Ok(Vec::new())
}
fn permission_requests(
&self,
context: &ToolContext,
arguments: &Value,
) -> Result<Vec<PermissionRequest>, ToolError> {
Ok(vec![PermissionRequest::new(
self.name(),
context.workspace_root.display().to_string(),
self.effect(arguments)?,
)])
}
fn prepare(
&self,
context: &ToolContext,
arguments: &Value,
) -> Result<ToolPreparation, ToolError> {
Ok(ToolPreparation::Direct {
permission_requests: self.permission_requests(context, arguments)?,
irreversible: self.irreversible(arguments)?,
})
}
fn execute<'a>(
&'a self,
context: &'a ToolContext,
arguments: Value,
) -> BoxFuture<'a, Result<ToolOutput, ToolError>>;
fn advertisement(&self) -> ToolAdvertisement {
ToolAdvertisement::function(self.name(), self.description(), self.input_schema())
}
}
#[derive(Debug, Error, Eq, PartialEq)]
pub enum RegistryError {
#[error("tool `{0}` is already registered")]
Duplicate(String),
#[error("tool `{0}` is not registered")]
Unknown(String),
}
#[derive(Clone, Default)]
pub struct ToolRegistry {
tools: BTreeMap<String, Arc<dyn Tool>>,
order: Vec<String>,
}
impl ToolRegistry {
pub fn register<T: Tool + 'static>(&mut self, tool: T) -> Result<(), RegistryError> {
let name = tool.name().to_owned();
if self.tools.contains_key(&name) {
return Err(RegistryError::Duplicate(name));
}
self.tools.insert(name.clone(), Arc::new(tool));
self.order.push(name);
Ok(())
}
pub fn get(&self, name: &str) -> Result<Arc<dyn Tool>, RegistryError> {
self.tools
.get(name)
.cloned()
.ok_or_else(|| RegistryError::Unknown(name.to_owned()))
}
pub fn advertisements(&self) -> Vec<ToolAdvertisement> {
self.order
.iter()
.filter_map(|name| self.tools.get(name))
.map(|tool| tool.advertisement())
.collect()
}
pub fn validate_call(&self, call: &ToolCall) -> Result<(Arc<dyn Tool>, Value), ToolError> {
let tool = self
.get(&call.name)
.map_err(|error| ToolError::InvalidArguments(error.to_string()))?;
let arguments = call
.arguments()
.map_err(|error| ToolError::InvalidArguments(error.to_string()))?;
if !arguments.is_object() {
return Err(ToolError::InvalidArguments(
"tool arguments must be a JSON object".into(),
));
}
Ok((tool, arguments))
}
pub fn len(&self) -> usize {
self.tools.len()
}
pub fn is_empty(&self) -> bool {
self.tools.is_empty()
}
}