1mod files;
2pub(crate) mod path;
3mod processes;
4
5pub use processes::ProcessRegistry;
6
7use crate::{
8 error::{ErrorCode, ExeoraError},
9 protocol::ToolName,
10};
11use anyhow::{Result, anyhow};
12use jsonschema::Validator;
13use serde_json::Value;
14use std::{collections::HashMap, path::Path, sync::Arc};
15use tokio_util::sync::CancellationToken;
16
17pub struct ToolEngine {
18 validators: HashMap<ToolName, Validator>,
19 processes: Arc<ProcessRegistry>,
20}
21
22impl ToolEngine {
23 pub fn new() -> Result<Self> {
24 let contract: Value = serde_json::from_str(include_str!("../../protocol/contract.json"))?;
25 let tools = contract
26 .pointer("/schemas/tools")
27 .and_then(Value::as_object)
28 .ok_or_else(|| anyhow!("generated tool schemas are missing"))?;
29 let mut validators = HashMap::new();
30 for tool in ToolName::ALL {
31 let schema = tools
32 .get(tool.as_str())
33 .and_then(|value| value.get("input"))
34 .ok_or_else(|| anyhow!("schema missing for {tool}"))?;
35 validators.insert(tool, jsonschema::validator_for(schema)?);
36 }
37 Ok(Self {
38 validators,
39 processes: Arc::new(ProcessRegistry::new()),
40 })
41 }
42
43 pub async fn execute(
44 &self,
45 root: &Path,
46 tool: ToolName,
47 arguments: Value,
48 cancel: CancellationToken,
49 ) -> Result<Value, ExeoraError> {
50 self.execute_for_project(root, &root.to_string_lossy(), tool, arguments, cancel)
51 .await
52 }
53
54 pub async fn execute_for_project(
55 &self,
56 root: &Path,
57 project_scope: &str,
58 tool: ToolName,
59 arguments: Value,
60 cancel: CancellationToken,
61 ) -> Result<Value, ExeoraError> {
62 let validator = self
63 .validators
64 .get(&tool)
65 .ok_or_else(|| ExeoraError::new(ErrorCode::UnknownTool, "Unsupported tool."))?;
66 if let Err(error) = validator.validate(&arguments) {
67 return Err(ExeoraError::new(
68 ErrorCode::InvalidArguments,
69 error.to_string(),
70 ));
71 }
72 if cancel.is_cancelled() {
73 return Err(ExeoraError::new(
74 ErrorCode::Cancelled,
75 "The call was cancelled before it started.",
76 ));
77 }
78
79 match tool {
80 ToolName::ReadFile => files::read_file(root, arguments).await,
81 ToolName::ListFiles => files::list_files(root, arguments).await,
82 ToolName::Grep => files::grep(root, arguments).await,
83 ToolName::EditFile => files::edit_file(root, arguments).await,
84 ToolName::WriteFile => files::write_file(root, arguments).await,
85 ToolName::RunCommand => self.processes.run_command(root, arguments, cancel).await,
86 ToolName::StartCommand => {
87 self.processes
88 .start_command(root, project_scope, arguments)
89 .await
90 }
91 ToolName::GetCommandOutput => self.processes.get_output(root, arguments).await,
92 ToolName::SendCommandInput => self.processes.send_input(root, arguments).await,
93 ToolName::KillCommand => self.processes.kill_command(root, arguments).await,
94 }
95 }
96
97 pub async fn kill_all(&self) {
98 self.processes.kill_all().await;
99 }
100
101 pub async fn kill_root(&self, root: &Path) {
102 self.processes.kill_root(root).await;
103 }
104}