perf(agent): skip Daytona folder request for edits

This commit is contained in:
Bryan Helmkamp 2026-08-04 15:01:19 -04:00
parent 120f0fa80b
commit 6dfeeca45e
No known key found for this signature in database
4 changed files with 100 additions and 12 deletions

View file

@ -226,7 +226,7 @@ pub fn make_edit_file_tool() -> RegisteredTool {
};
ctx.env
.write_file(file_path, &new_content)
.write_existing_file(file_path, &new_content)
.await
.map_err(|e| e.display_with_causes())?;
Ok(format!("Successfully edited {file_path}"))
@ -1002,6 +1002,7 @@ mod tests {
)
.await;
assert_eq!(result.unwrap(), "Successfully wrote to /out.txt");
assert_eq!(env.existing_file_write_count(), 0);
let written = env.written_files.lock().unwrap();
assert_eq!(written.len(), 1);
assert_eq!(written[0].0, "/out.txt");
@ -1036,6 +1037,7 @@ mod tests {
)
.await;
assert_eq!(result.unwrap(), "Successfully edited /f.txt");
assert_eq!(env.existing_file_write_count(), 1);
let written = env.written_files.lock().unwrap();
assert_eq!(written.len(), 1);
assert_eq!(written[0].1, "goodbye world");

View file

@ -511,6 +511,19 @@ impl DaytonaSandbox {
resolve_path(path, self.working_directory())
}
async fn upload_file_content(&self, resolved_path: &str, content: &str) -> crate::Result<()> {
let sandbox = self.sandbox()?;
let fs_svc = sandbox
.fs()
.await
.map_err(|e| crate::Error::context("Failed to get fs service", e))?;
fs_svc
.upload_file_bytes(resolved_path, content.as_bytes())
.await
.map_err(|e| crate::Error::context(format!("Failed to write file {resolved_path}"), e))
}
/// Verify a Daytona sandbox evaluates commands as non-login Bash.
///
/// Runs on a freshly created sandbox before any Fabro-owned setup, and
@ -1613,17 +1626,12 @@ impl Sandbox for DaytonaSandbox {
}
}
let fs_svc = sandbox
.fs()
.await
.map_err(|e| crate::Error::context("Failed to get fs service", e))?;
self.upload_file_content(&resolved, content).await
}
fs_svc
.upload_file_bytes(&resolved, content.as_bytes())
.await
.map_err(|e| crate::Error::context(format!("Failed to write file {resolved}"), e))?;
Ok(())
async fn write_existing_file(&self, path: &str, content: &str) -> crate::Result<()> {
let resolved = self.resolve_path(path);
self.upload_file_content(&resolved, content).await
}
async fn delete_file(&self, path: &str) -> crate::Result<()> {
@ -3432,6 +3440,62 @@ mod tests {
delete.assert_async().await;
}
#[tokio::test]
async fn write_existing_file_skips_parent_directory_creation() {
let server = MockServer::start_async().await;
let server_url = server.base_url();
let sandbox_response = server
.mock_async(|when, then| {
when.method(GET).path("/sandbox/sandbox-edit");
then.status(200)
.header("content-type", "application/json")
.json_body(sandbox_body("sandbox-edit", SandboxState::Started));
})
.await;
let toolbox_response = server
.mock_async(|when, then| {
when.method(GET)
.path("/sandbox/sandbox-edit/toolbox-proxy-url");
then.status(200)
.header("content-type", "application/json")
.json_body(serde_json::json!({"url": server_url}));
})
.await;
let folder = server
.mock_async(|when, then| {
when.method(POST).path("/sandbox-edit/files/folder");
then.status(200);
})
.await;
let upload = server
.mock_async(|when, then| {
when.method(POST)
.path("/sandbox-edit/files/upload")
.query_param("path", "/home/daytona/workspace/src/lib.rs")
.body_includes("updated contents");
then.status(200);
})
.await;
let sandbox = mock_daytona_sandbox(&server, "dtn_test", DaytonaConfig::default()).await;
let sdk_sandbox = sandbox
.client
.get("sandbox-edit")
.await
.expect("get mock sandbox");
assert!(sandbox.sandbox.set(sdk_sandbox).is_ok());
sandbox
.write_existing_file("src/lib.rs", "updated contents")
.await
.expect("write existing file");
sandbox_response.assert_async().await;
toolbox_response.assert_async().await;
upload.assert_async().await;
folder.assert_calls_async(0).await;
}
/// Recover the inner command a wrapper carries, proving it survives the
/// base64 transport byte-for-byte.
fn decode_wrapped_command(wrapped: &str) -> String {

View file

@ -1048,6 +1048,16 @@ pub trait Sandbox: Send + Sync {
}
async fn write_file(&self, path: &str, content: &str) -> crate::Result<()>;
/// Write a file that the caller has already confirmed exists.
///
/// Providers can override this method to skip setup that is only needed
/// when creating a new path. The default preserves the behavior of
/// [`Sandbox::write_file`].
async fn write_existing_file(&self, path: &str, content: &str) -> crate::Result<()> {
self.write_file(path, content).await
}
async fn delete_file(&self, path: &str) -> crate::Result<()>;
async fn file_exists(&self, path: &str) -> crate::Result<bool>;
async fn list_directory(

View file

@ -1,6 +1,6 @@
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::Duration;
use async_trait::async_trait;
@ -29,6 +29,8 @@ pub struct MockSandbox {
pub os_version_str: String,
/// Captures (path, content) pairs from `write_file` calls.
pub written_files: Mutex<Vec<(String, String)>>,
/// Counts calls to `write_existing_file`.
pub existing_file_writes: AtomicUsize,
/// Captures the `timeout_ms` argument from `exec_command` calls.
pub captured_timeout: Mutex<Option<u64>>,
/// Captures the `command` argument from `exec_command` calls (last only).
@ -104,6 +106,10 @@ impl MockSandbox {
.expect("delete_calls lock poisoned")
}
pub fn existing_file_write_count(&self) -> usize {
self.existing_file_writes.load(Ordering::Relaxed)
}
pub fn set_stdio_process(&self, process: MockStdioProcess) {
*self
.stdio_process
@ -156,6 +162,7 @@ impl Default for MockSandbox {
platform_str: "darwin",
os_version_str: "Darwin 24.0.0".into(),
written_files: Mutex::new(Vec::new()),
existing_file_writes: AtomicUsize::new(0),
captured_timeout: Mutex::new(None),
captured_command: Mutex::new(None),
captured_commands: Mutex::new(Vec::new()),
@ -250,6 +257,11 @@ impl Sandbox for MockSandbox {
Ok(())
}
async fn write_existing_file(&self, path: &str, content: &str) -> crate::Result<()> {
self.existing_file_writes.fetch_add(1, Ordering::Relaxed);
self.write_file(path, content).await
}
async fn delete_file(&self, _path: &str) -> crate::Result<()> {
Ok(())
}