1use std::collections::BTreeMap;
2use std::fmt;
3use std::path::PathBuf;
4use std::sync::Arc;
5use std::time::Duration;
6
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use thiserror::Error;
10
11use crate::{
12 BoxFuture, PermissionRequest, ReadEvidenceStore, ScopedProjectContextProvider,
13 ToolAdvertisement, ToolCall, ToolResultStore,
14};
15
16pub trait CancellationSignal: Send + Sync {
18 fn is_cancelled(&self) -> bool;
19}
20
21#[derive(Clone, Copy, Debug, Default)]
22pub struct NeverCancelled;
23
24impl CancellationSignal for NeverCancelled {
25 fn is_cancelled(&self) -> bool {
26 false
27 }
28}
29
30#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
31#[serde(rename_all = "snake_case")]
32pub enum ToolEffect {
33 Read,
34 Write,
35 Process,
36 Network,
37 UserInteraction,
38 Delegation,
39}
40
41#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
47#[serde(rename_all = "lowercase")]
48pub enum SandboxMode {
49 #[default]
50 None,
51 Os,
52}
53
54#[derive(Clone)]
55pub struct ToolContext {
56 pub workspace_root: PathBuf,
57 pub additional_roots: Vec<PathBuf>,
58 pub limits: ToolLimits,
59 pub read_evidence: Option<Arc<dyn ReadEvidenceStore>>,
60 pub tool_results: Option<Arc<dyn ToolResultStore>>,
61 pub project_context: Option<Arc<dyn ScopedProjectContextProvider>>,
62 pub cancellation: Arc<dyn CancellationSignal>,
63 pub sandbox: SandboxMode,
64}
65
66impl ToolContext {
67 pub fn new(workspace_root: PathBuf) -> Self {
68 Self {
69 workspace_root,
70 additional_roots: Vec::new(),
71 limits: ToolLimits::default(),
72 read_evidence: None,
73 tool_results: None,
74 project_context: None,
75 cancellation: Arc::new(NeverCancelled),
76 sandbox: SandboxMode::None,
77 }
78 }
79}
80
81impl fmt::Debug for ToolContext {
82 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83 formatter
84 .debug_struct("ToolContext")
85 .field("workspace_root", &self.workspace_root)
86 .field("additional_roots", &self.additional_roots)
87 .field("limits", &self.limits)
88 .field("has_read_evidence", &self.read_evidence.is_some())
89 .field("has_tool_result_store", &self.tool_results.is_some())
90 .field("has_project_context", &self.project_context.is_some())
91 .field("cancelled", &self.cancellation.is_cancelled())
92 .field("sandbox", &self.sandbox)
93 .finish()
94 }
95}
96
97#[derive(Clone, Copy, Debug)]
98pub struct ToolLimits {
99 pub max_result_bytes: usize,
100 pub max_read_file_lines: usize,
101 pub max_read_file_line_bytes: usize,
102 pub max_list_entries: usize,
103 pub command_timeout: Option<Duration>,
104}
105
106impl Default for ToolLimits {
107 fn default() -> Self {
108 Self {
109 max_result_bytes: 64 * 1024,
110 max_read_file_lines: 400,
111 max_read_file_line_bytes: 2_000,
112 max_list_entries: 100,
113 command_timeout: None,
114 }
115 }
116}
117
118#[derive(Clone, Deserialize, PartialEq, Serialize)]
119pub struct ToolOutput {
120 pub content: String,
121 #[serde(default)]
122 pub is_error: bool,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub structured: Option<Value>,
125 #[serde(default)]
126 pub original_bytes: usize,
127 #[serde(default)]
128 pub truncated: bool,
129 #[serde(skip)]
136 pub durable_content: Option<String>,
137}
138
139impl fmt::Debug for ToolOutput {
140 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
141 formatter
142 .debug_struct("ToolOutput")
143 .field("content", &self.content)
144 .field("is_error", &self.is_error)
145 .field("structured", &self.structured)
146 .field("original_bytes", &self.original_bytes)
147 .field("truncated", &self.truncated)
148 .field("has_durable_content", &self.durable_content.is_some())
149 .finish()
150 }
151}
152
153#[derive(Debug, Error)]
154pub enum ToolError {
155 #[error("invalid arguments: {0}")]
156 InvalidArguments(String),
157 #[error("tool call is outside the active workspace: {0}")]
158 OutsideWorkspace(String),
159 #[error("permission denied: {0}")]
160 PermissionDenied(String),
161 #[error("tool execution failed: {0}")]
162 Execution(String),
163 #[error("tool was cancelled")]
164 Cancelled,
165}
166
167#[derive(Clone, Debug, Eq, PartialEq)]
173pub struct FileChangeReview {
174 pub path: PathBuf,
175 pub before: Option<Vec<u8>>,
176 pub after: Vec<u8>,
177}
178
179#[derive(Clone, Debug, Eq, PartialEq)]
180pub struct CommandReview {
181 pub command: String,
182 pub cwd: PathBuf,
183 pub shell: PathBuf,
184 pub profile: String,
185}
186
187#[derive(Clone, Debug, Eq, PartialEq)]
188pub enum ToolReview {
189 FileChange(FileChangeReview),
190 Command(CommandReview),
191}
192
193pub trait PreparedToolAction: Send {
199 fn commit<'a>(
200 self: Box<Self>,
201 context: &'a ToolContext,
202 ) -> BoxFuture<'a, Result<ToolOutput, ToolError>>;
203}
204
205pub struct PreparedToolCall {
206 pub tool_name: String,
207 pub permission_requests: Vec<PermissionRequest>,
208 pub irreversible: bool,
209 pub review: Option<ToolReview>,
210 action: Box<dyn PreparedToolAction>,
211}
212
213impl PreparedToolCall {
214 pub fn new(
215 tool_name: impl Into<String>,
216 permission_requests: Vec<PermissionRequest>,
217 irreversible: bool,
218 review: Option<ToolReview>,
219 action: impl PreparedToolAction + 'static,
220 ) -> Self {
221 Self {
222 tool_name: tool_name.into(),
223 permission_requests,
224 irreversible,
225 review,
226 action: Box::new(action),
227 }
228 }
229
230 pub fn commit<'a>(
231 self,
232 context: &'a ToolContext,
233 ) -> BoxFuture<'a, Result<ToolOutput, ToolError>> {
234 self.action.commit(context)
235 }
236}
237
238impl fmt::Debug for PreparedToolCall {
239 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
240 formatter
241 .debug_struct("PreparedToolCall")
242 .field("tool_name", &self.tool_name)
243 .field("permission_requests", &self.permission_requests)
244 .field("irreversible", &self.irreversible)
245 .field("review", &self.review)
246 .finish_non_exhaustive()
247 }
248}
249
250#[derive(Debug)]
251pub enum ToolPreparation {
252 Direct {
253 permission_requests: Vec<PermissionRequest>,
254 irreversible: bool,
255 },
256 Prepared(PreparedToolCall),
257}
258
259pub trait Tool: Send + Sync {
262 fn name(&self) -> &str;
263 fn description(&self) -> &str;
264 fn input_schema(&self) -> Value;
265 fn effect(&self, arguments: &Value) -> Result<ToolEffect, ToolError>;
266 fn irreversible(&self, _arguments: &Value) -> Result<bool, ToolError> {
267 Ok(false)
268 }
269
270 fn project_context_targets(
276 &self,
277 _context: &ToolContext,
278 _arguments: &Value,
279 ) -> Result<Vec<PathBuf>, ToolError> {
280 Ok(Vec::new())
281 }
282
283 fn permission_requests(
284 &self,
285 context: &ToolContext,
286 arguments: &Value,
287 ) -> Result<Vec<PermissionRequest>, ToolError> {
288 Ok(vec![PermissionRequest::new(
289 self.name(),
290 context.workspace_root.display().to_string(),
291 self.effect(arguments)?,
292 )])
293 }
294
295 fn prepare(
299 &self,
300 context: &ToolContext,
301 arguments: &Value,
302 ) -> Result<ToolPreparation, ToolError> {
303 Ok(ToolPreparation::Direct {
304 permission_requests: self.permission_requests(context, arguments)?,
305 irreversible: self.irreversible(arguments)?,
306 })
307 }
308
309 fn execute<'a>(
310 &'a self,
311 context: &'a ToolContext,
312 arguments: Value,
313 ) -> BoxFuture<'a, Result<ToolOutput, ToolError>>;
314
315 fn advertisement(&self) -> ToolAdvertisement {
316 ToolAdvertisement::function(self.name(), self.description(), self.input_schema())
317 }
318}
319
320#[derive(Debug, Error, Eq, PartialEq)]
321pub enum RegistryError {
322 #[error("tool `{0}` is already registered")]
323 Duplicate(String),
324 #[error("tool `{0}` is not registered")]
325 Unknown(String),
326}
327
328#[derive(Clone, Default)]
329pub struct ToolRegistry {
330 tools: BTreeMap<String, Arc<dyn Tool>>,
331 order: Vec<String>,
332}
333
334impl ToolRegistry {
335 pub fn register<T: Tool + 'static>(&mut self, tool: T) -> Result<(), RegistryError> {
336 let name = tool.name().to_owned();
337 if self.tools.contains_key(&name) {
338 return Err(RegistryError::Duplicate(name));
339 }
340 self.tools.insert(name.clone(), Arc::new(tool));
341 self.order.push(name);
342 Ok(())
343 }
344
345 pub fn get(&self, name: &str) -> Result<Arc<dyn Tool>, RegistryError> {
346 self.tools
347 .get(name)
348 .cloned()
349 .ok_or_else(|| RegistryError::Unknown(name.to_owned()))
350 }
351
352 pub fn advertisements(&self) -> Vec<ToolAdvertisement> {
353 self.order
354 .iter()
355 .filter_map(|name| self.tools.get(name))
356 .map(|tool| tool.advertisement())
357 .collect()
358 }
359
360 pub fn validate_call(&self, call: &ToolCall) -> Result<(Arc<dyn Tool>, Value), ToolError> {
361 let tool = self
362 .get(&call.name)
363 .map_err(|error| ToolError::InvalidArguments(error.to_string()))?;
364 let arguments = call
365 .arguments()
366 .map_err(|error| ToolError::InvalidArguments(error.to_string()))?;
367 if !arguments.is_object() {
368 return Err(ToolError::InvalidArguments(
369 "tool arguments must be a JSON object".into(),
370 ));
371 }
372 Ok((tool, arguments))
373 }
374
375 pub fn len(&self) -> usize {
376 self.tools.len()
377 }
378
379 pub fn is_empty(&self) -> bool {
380 self.tools.is_empty()
381 }
382}