Skip to content

Commit 81dafd0

Browse files
Merge pull request #774 from smallcloudai/fix-and-improve-paths-and-edit-tool-responses
Fix and improve paths and edit tool responses
2 parents 7b36a82 + 4d16efe commit 81dafd0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+372
-174
lines changed

refact-agent/engine/Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ default = ["vecdb"]
1616
vecdb = ["sqlite-vec"]
1717

1818
[build-dependencies]
19-
shadow-rs = "0.36.0"
19+
shadow-rs = "1.1.0"
2020

2121
[dependencies]
2222
astral-tokio-tar = "0.5.2"
@@ -31,7 +31,7 @@ diff = "0.1.13"
3131
dunce = "1.0.5"
3232
dyn_partial_eq = "=0.1.2"
3333
futures = "0.3"
34-
git2 = "0.19.0"
34+
git2 = "0.20.2"
3535
glob = "0.3.1"
3636
hashbrown = "0.15.2"
3737
headless_chrome = "1.0.16"
@@ -63,7 +63,7 @@ serde_cbor = "0.11.2"
6363
serde_json = { version = "1", features = ["preserve_order"] }
6464
serde_yaml = "0.9.31"
6565
# all features = ["compression", "docs", "event_log", "failpoints", "io_uring", "lock_free_delays", "measure_allocs", "miri_optimizations", "mutex", "no_inline", "no_logs", "pretty_backtrace", "testing"]
66-
shadow-rs = { version = "0.36.0", features = [], default-features = false }
66+
shadow-rs = { version = "1.1.0", features = [], default-features = false }
6767
sha2 = "0.10.8"
6868
shell-words = "1.1.0"
6969
shell-escape = "0.1.5"

refact-agent/engine/build.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11

2-
fn main() -> shadow_rs::SdResult<()> {
3-
shadow_rs::new()
2+
fn main() {
3+
shadow_rs::ShadowBuilder::builder().build().unwrap();
44
}

refact-agent/engine/src/call_validation.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ pub struct ChatMessage {
175175
#[serde(default, skip_serializing_if = "String::is_empty")]
176176
pub tool_call_id: String,
177177
#[serde(default, skip_serializing_if = "Option::is_none")]
178+
pub tool_failed: Option<bool>,
179+
#[serde(default, skip_serializing_if = "Option::is_none")]
178180
pub usage: Option<ChatUsage>,
179181
#[serde(default, skip_serializing_if = "Vec::is_empty")]
180182
pub checkpoints: Vec<Checkpoint>,
@@ -187,7 +189,7 @@ pub struct ChatMessage {
187189
pub enum ModelType {
188190
Chat,
189191
Completion,
190-
Embedding,
192+
Embedding,
191193
}
192194

193195
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
@@ -289,7 +291,7 @@ impl ChatMode {
289291
pub fn is_agentic(self) -> bool {
290292
match self {
291293
ChatMode::AGENT => true,
292-
ChatMode::NO_TOOLS | ChatMode::EXPLORE | ChatMode::CONFIGURE |
294+
ChatMode::NO_TOOLS | ChatMode::EXPLORE | ChatMode::CONFIGURE |
293295
ChatMode::PROJECT_SUMMARY => false,
294296
}
295297
}

refact-agent/engine/src/caps/caps.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ pub async fn load_caps_value_from_url(
283283

284284
if !cmdline.api_key.is_empty() {
285285
headers.insert(reqwest::header::AUTHORIZATION, reqwest::header::HeaderValue::from_str(&format!("Bearer {}", cmdline.api_key)).unwrap());
286-
headers.insert(reqwest::header::USER_AGENT, reqwest::header::HeaderValue::from_str(&format!("refact-lsp {}", crate::version::build_info::PKG_VERSION)).unwrap());
286+
headers.insert(reqwest::header::USER_AGENT, reqwest::header::HeaderValue::from_str(&format!("refact-lsp {}", crate::version::build::PKG_VERSION)).unwrap());
287287
}
288288

289289
let mut last_status = 0;

refact-agent/engine/src/files_correction.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,6 @@ async fn complete_path_with_project_dir(
135135
if path_exists(&candidate_path, is_dir) && candidate_path.starts_with(&p) {
136136
return Some(candidate_path);
137137
}
138-
let j_path = p.join(&candidate_path);
139-
if path_exists(&j_path, is_dir) {
140-
return Some(j_path);
141-
}
142138

143139
// This might save a roundtrip:
144140
// .../project1/project1/1.cpp
@@ -456,6 +452,17 @@ pub fn canonicalize_normalized_path(p: PathBuf) -> PathBuf {
456452
p.canonicalize().unwrap_or_else(|_| absolute(&p).unwrap_or(p))
457453
}
458454

455+
pub async fn check_if_its_inside_a_workspace_or_config(gcx: Arc<ARwLock<GlobalContext>>, path: &Path) -> Result<(), String> {
456+
let workspace_folders = get_project_dirs(gcx.clone()).await;
457+
let config_dir = gcx.read().await.config_dir.clone();
458+
459+
if workspace_folders.iter().any(|d| path.starts_with(d)) || path.starts_with(&config_dir) {
460+
Ok(())
461+
} else {
462+
Err(format!("Path '{path:?}' is outside of project directories:\n{workspace_folders:?}"))
463+
}
464+
}
465+
459466
pub fn any_glob_matches_path(globs: &[String], path: &Path) -> bool {
460467
globs.iter().any(|glob| {
461468
let pattern = glob::Pattern::new(glob).unwrap();

refact-agent/engine/src/forward_to_openai_endpoint.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub async fn forward_to_openai_style_endpoint(
2929
headers.insert(AUTHORIZATION, HeaderValue::from_str(&format!("Bearer {}", model_rec.api_key)).unwrap());
3030
}
3131
if model_rec.support_metadata {
32-
headers.insert(USER_AGENT, HeaderValue::from_str(&format!("refact-lsp {}", crate::version::build_info::PKG_VERSION)).unwrap());
32+
headers.insert(USER_AGENT, HeaderValue::from_str(&format!("refact-lsp {}", crate::version::build::PKG_VERSION)).unwrap());
3333
}
3434
let mut data = json!({
3535
"model": model_rec.name.clone(),
@@ -64,7 +64,7 @@ pub async fn forward_to_openai_style_endpoint(
6464
if let Some(meta) = meta {
6565
data["meta"] = json!(meta);
6666
}
67-
67+
6868
// When cancelling requests, coroutine ususally gets aborted here on the following line.
6969
let req = client.post(&model_rec.endpoint)
7070
.headers(headers)
@@ -105,7 +105,7 @@ pub async fn forward_to_openai_style_endpoint_streaming(
105105
headers.insert(AUTHORIZATION, HeaderValue::from_str(&format!("Bearer {}", model_rec.api_key)).unwrap());
106106
}
107107
if model_rec.support_metadata {
108-
headers.insert(USER_AGENT, HeaderValue::from_str(format!("refact-lsp {}", crate::version::build_info::PKG_VERSION).as_str()).unwrap());
108+
headers.insert(USER_AGENT, HeaderValue::from_str(format!("refact-lsp {}", crate::version::build::PKG_VERSION).as_str()).unwrap());
109109
}
110110

111111
let mut data = json!({
@@ -146,7 +146,7 @@ pub async fn forward_to_openai_style_endpoint_streaming(
146146
if let Some(meta) = meta {
147147
data["meta"] = json!(meta);
148148
}
149-
149+
150150
if model_rec.endpoint.is_empty() {
151151
return Err(format!("No endpoint configured for {}", model_rec.id));
152152
}
@@ -252,7 +252,7 @@ pub async fn get_embedding_openai_style(
252252
// info!("get_embedding_openai_style: {:?}", json);
253253
// {"data":[{"embedding":[0.0121664945...],"index":0,"object":"embedding"}, {}, {}]}
254254
// or {"data":[{"embedding":[0.0121664945...]}, {}, {}]} without index
255-
255+
256256
let mut result: Vec<Vec<f32>> = vec![vec![]; B];
257257
match serde_json::from_value::<Vec<EmbeddingsResultOpenAI>>(json["data"].clone()) {
258258
Ok(unordered) => {
@@ -268,7 +268,7 @@ pub async fn get_embedding_openai_style(
268268
match serde_json::from_value::<Vec<EmbeddingsResultOpenAINoIndex>>(json["data"].clone()) {
269269
Ok(ordered) => {
270270
if ordered.len() != B {
271-
return Err(format!("get_embedding_openai_style: response length mismatch: expected {}, got {}",
271+
return Err(format!("get_embedding_openai_style: response length mismatch: expected {}, got {}",
272272
B, ordered.len()));
273273
}
274274
for (i, res) in ordered.into_iter().enumerate() {

refact-agent/engine/src/http/routers/info.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ use crate::custom_error::ScratchError;
99

1010
pub fn get_build_info() -> IndexMap<&'static str, &'static str> {
1111
IndexMap::from([
12-
("version", crate::version::build_info::PKG_VERSION),
13-
("commit", crate::version::build_info::COMMIT_HASH),
14-
("build_os", crate::version::build_info::BUILD_OS),
15-
("rust_version", crate::version::build_info::RUST_VERSION),
16-
("cargo_version", crate::version::build_info::CARGO_VERSION),
12+
("version", crate::version::build::PKG_VERSION),
13+
("commit", crate::version::build::COMMIT_HASH),
14+
("build_os", crate::version::build::BUILD_OS),
15+
("rust_version", crate::version::build::RUST_VERSION),
16+
("cargo_version", crate::version::build::CARGO_VERSION),
1717
])
1818
}
1919

refact-agent/engine/src/http/routers/v1/gui_help_handlers.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use serde::Deserialize;
77
use tokio::sync::RwLock as ARwLock;
88
use crate::at_commands::at_file::{file_repair_candidates, return_one_candidate_or_a_good_error};
99
use crate::custom_error::ScratchError;
10-
use crate::files_correction::correct_to_nearest_dir_path;
10+
use crate::files_correction::{correct_to_nearest_dir_path, preprocess_path_for_normalization};
1111
use crate::global_context::GlobalContext;
1212

1313
#[derive(Deserialize)]
@@ -22,11 +22,12 @@ pub async fn handle_v1_fullpath(
2222
let post = serde_json::from_slice::<ResolveShortenedPathPost>(&body_bytes)
2323
.map_err(|e| ScratchError::new(StatusCode::UNPROCESSABLE_ENTITY, format!("JSON problem: {}", e)))?;
2424

25-
let candidates_file = file_repair_candidates(gcx.clone(), &post.path, 10, false).await;
26-
let candidates_dir = correct_to_nearest_dir_path(gcx.clone(), &post.path, false, 10).await;
25+
let path = preprocess_path_for_normalization(post.path);
26+
let candidates_file = file_repair_candidates(gcx.clone(), &path, 10, false).await;
27+
let candidates_dir = correct_to_nearest_dir_path(gcx.clone(), &path, false, 10).await;
2728
let candidates = candidates_file.into_iter().chain(candidates_dir.clone().into_iter()).collect::<HashSet<_>>().into_iter().collect::<Vec<_>>();
2829

29-
match return_one_candidate_or_a_good_error(gcx.clone(), &post.path, &candidates, &vec![], false).await {
30+
match return_one_candidate_or_a_good_error(gcx.clone(), &path, &candidates, &vec![], false).await {
3031
Ok(candidate) => {
3132
let is_directory = candidates_dir.contains(&candidate);
3233
Ok(Response::builder()

refact-agent/engine/src/integrations/docker/integr_docker.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,9 @@ impl Tool for ToolDocker {
163163
]))
164164
}
165165

166-
fn command_to_match_against_confirm_deny(
166+
async fn command_to_match_against_confirm_deny(
167167
&self,
168+
_ccx: Arc<AMutex<AtCommandsContext>>,
168169
args: &HashMap<String, Value>,
169170
) -> Result<String, String> {
170171
let command = parse_command(args)?;

refact-agent/engine/src/integrations/integr_cmdline.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,8 +305,9 @@ impl Tool for ToolCmdline {
305305
}
306306
}
307307

308-
fn command_to_match_against_confirm_deny(
308+
async fn command_to_match_against_confirm_deny(
309309
&self,
310+
_ccx: Arc<AMutex<AtCommandsContext>>,
310311
args: &HashMap<String, serde_json::Value>,
311312
) -> Result<String, String> {
312313
let (command, _workdir) = _parse_command_args(args, &self.cfg)?;

refact-agent/engine/src/integrations/integr_github.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,9 @@ impl Tool for ToolGithub {
131131
Ok((false, results))
132132
}
133133

134-
fn command_to_match_against_confirm_deny(
134+
async fn command_to_match_against_confirm_deny(
135135
&self,
136+
_ccx: Arc<AMutex<AtCommandsContext>>,
136137
args: &HashMap<String, Value>,
137138
) -> Result<String, String> {
138139
let mut command_args = parse_command_args(args)?;

refact-agent/engine/src/integrations/integr_gitlab.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,9 @@ impl Tool for ToolGitlab {
129129
Ok((false, results))
130130
}
131131

132-
fn command_to_match_against_confirm_deny(
132+
async fn command_to_match_against_confirm_deny(
133133
&self,
134+
_ccx: Arc<AMutex<AtCommandsContext>>,
134135
args: &HashMap<String, Value>,
135136
) -> Result<String, String> {
136137
let mut command_args = parse_command_args(args)?;

refact-agent/engine/src/integrations/integr_mcp.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,8 +534,9 @@ impl Tool for ToolMCP {
534534
sanitized_yaml_name
535535
}
536536

537-
fn command_to_match_against_confirm_deny(
537+
async fn command_to_match_against_confirm_deny(
538538
&self,
539+
_ccx: Arc<AMutex<AtCommandsContext>>,
539540
_args: &HashMap<String, serde_json::Value>,
540541
) -> Result<String, String> {
541542
let command = self.mcp_tool.name.clone();

refact-agent/engine/src/integrations/integr_mysql.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,9 @@ impl Tool for ToolMysql {
137137
Ok((true, results))
138138
}
139139

140-
fn command_to_match_against_confirm_deny(
140+
async fn command_to_match_against_confirm_deny(
141141
&self,
142+
_ccx: Arc<AMutex<AtCommandsContext>>,
142143
args: &HashMap<String, Value>,
143144
) -> Result<String, String> {
144145
let query = match args.get("query") {

refact-agent/engine/src/integrations/integr_pdb.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,9 @@ impl Tool for ToolPdb {
161161
Ok(tool_answer(output, tool_call_id))
162162
}
163163

164-
fn command_to_match_against_confirm_deny(
164+
async fn command_to_match_against_confirm_deny(
165165
&self,
166+
_ccx: Arc<AMutex<AtCommandsContext>>,
166167
args: &HashMap<String, Value>,
167168
) -> Result<String, String> {
168169
let (command, _) = parse_args(args)?;

refact-agent/engine/src/integrations/integr_postgres.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,9 @@ impl Tool for ToolPostgres {
136136
Ok((true, results))
137137
}
138138

139-
fn command_to_match_against_confirm_deny(
139+
async fn command_to_match_against_confirm_deny(
140140
&self,
141+
_ccx: Arc<AMutex<AtCommandsContext>>,
141142
args: &HashMap<String, Value>,
142143
) -> Result<String, String> {
143144
let query = match args.get("query") {

refact-agent/engine/src/integrations/integr_shell.rs

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@ use async_trait::async_trait;
99
use tokio::process::Command;
1010

1111
use crate::at_commands::at_commands::AtCommandsContext;
12+
use crate::at_commands::at_file::return_one_candidate_or_a_good_error;
13+
use crate::files_correction::canonical_path;
14+
use crate::files_correction::canonicalize_normalized_path;
15+
use crate::files_correction::check_if_its_inside_a_workspace_or_config;
16+
use crate::files_correction::correct_to_nearest_dir_path;
1217
use crate::files_correction::get_active_project_path;
18+
use crate::files_correction::get_project_dirs;
19+
use crate::files_correction::preprocess_path_for_normalization;
1320
use crate::files_correction::CommandSimplifiedDirExt;
1421
use crate::global_context::GlobalContext;
1522
use crate::integrations::process_io_utils::execute_command;
@@ -79,10 +86,10 @@ impl Tool for ToolShell {
7986
tool_call_id: &String,
8087
args: &HashMap<String, Value>,
8188
) -> Result<(bool, Vec<ContextEnum>), String> {
82-
let (command, workdir_maybe) = parse_args(args)?;
89+
let gcx = ccx.lock().await.global_context.clone();
90+
let (command, workdir_maybe) = parse_args(gcx.clone(), args).await?;
8391
let timeout = self.cfg.timeout.parse::<u64>().unwrap_or(10);
8492

85-
let gcx = ccx.lock().await.global_context.clone();
8693
let mut error_log = Vec::<YamlError>::new();
8794
let env_variables = crate::integrations::setting_up_integrations::get_vars_for_replacements(gcx.clone(), &mut error_log).await;
8895

@@ -137,10 +144,10 @@ impl Tool for ToolShell {
137144

138145
async fn match_against_confirm_deny(
139146
&self,
140-
_ccx: Arc<AMutex<AtCommandsContext>>,
147+
ccx: Arc<AMutex<AtCommandsContext>>,
141148
args: &HashMap<String, Value>
142149
) -> Result<MatchConfirmDeny, String> {
143-
let command_to_match = self.command_to_match_against_confirm_deny(&args).map_err(|e| {
150+
let command_to_match = self.command_to_match_against_confirm_deny(ccx.clone(), &args).await.map_err(|e| {
144151
format!("Error getting tool command to match: {}", e)
145152
})?;
146153
if command_to_match.is_empty() {
@@ -164,11 +171,13 @@ impl Tool for ToolShell {
164171
})
165172
}
166173

167-
fn command_to_match_against_confirm_deny(
174+
async fn command_to_match_against_confirm_deny(
168175
&self,
176+
ccx: Arc<AMutex<AtCommandsContext>>,
169177
args: &HashMap<String, Value>,
170178
) -> Result<String, String> {
171-
let (command, _) = parse_args(args)?;
179+
let gcx = ccx.lock().await.global_context.clone();
180+
let (command, _) = parse_args(gcx, args).await?;
172181
Ok(command)
173182
}
174183

@@ -221,7 +230,7 @@ pub async fn execute_shell_command(
221230
Ok(out)
222231
}
223232

224-
fn parse_args(args: &HashMap<String, Value>) -> Result<(String, Option<PathBuf>), String> {
233+
async fn parse_args(gcx: Arc<ARwLock<GlobalContext>>, args: &HashMap<String, Value>) -> Result<(String, Option<PathBuf>), String> {
225234
let command = match args.get("command") {
226235
Some(Value::String(s)) => {
227236
if s.is_empty() {
@@ -239,12 +248,7 @@ fn parse_args(args: &HashMap<String, Value>) -> Result<(String, Option<PathBuf>)
239248
if s.is_empty() {
240249
None
241250
} else {
242-
let workdir = crate::files_correction::canonical_path(s);
243-
if !workdir.exists() {
244-
return Err("Workdir doesn't exist".to_string());
245-
} else {
246-
Some(workdir)
247-
}
251+
Some(resolve_shell_workdir(gcx.clone(), s).await?)
248252
}
249253
},
250254
Some(v) => return Err(format!("argument `workdir` is not a string: {:?}", v)),
@@ -254,6 +258,28 @@ fn parse_args(args: &HashMap<String, Value>) -> Result<(String, Option<PathBuf>)
254258
Ok((command, workdir))
255259
}
256260

261+
async fn resolve_shell_workdir(gcx: Arc<ARwLock<GlobalContext>>, raw_path: &str) -> Result<PathBuf, String> {
262+
let path_str = preprocess_path_for_normalization(raw_path.to_string());
263+
let path = PathBuf::from(&path_str);
264+
265+
let workdir = if path.is_absolute() {
266+
let path = canonicalize_normalized_path(path);
267+
check_if_its_inside_a_workspace_or_config(gcx.clone(), &path).await?;
268+
path
269+
} else {
270+
let project_dirs = get_project_dirs(gcx.clone()).await;
271+
let candidates = correct_to_nearest_dir_path(gcx.clone(), &path_str, false, 3).await;
272+
canonical_path(
273+
return_one_candidate_or_a_good_error(gcx.clone(), &path_str, &candidates, &project_dirs, true).await?
274+
)
275+
};
276+
if !workdir.exists() {
277+
Err("Workdir doesn't exist".to_string())
278+
} else {
279+
Ok(workdir)
280+
}
281+
}
282+
257283
pub const SHELL_INTEGRATION_SCHEMA: &str = r#"
258284
fields:
259285
timeout:

0 commit comments

Comments
 (0)