Add FS abstraction and use in view_image (#14960)

Adds an environment crate and environment + file system abstraction.

Environment is a combination of attributes and services specific to
environment the agent is connected to:
File system, process management, OS, default shell.

The goal is to move most of agent logic that assumes environment to work
through the environment abstraction.
This commit is contained in:
pakrym-oai
2026-03-17 17:36:23 -07:00
committed by GitHub
parent 19b887128e
commit 83a60fdb94
17 changed files with 597 additions and 352 deletions

View File

@@ -53,18 +53,13 @@ struct ImageCacheKey {
static IMAGE_CACHE: LazyLock<BlockingLruCache<ImageCacheKey, EncodedImage>> =
LazyLock::new(|| BlockingLruCache::new(NonZeroUsize::new(32).unwrap_or(NonZeroUsize::MIN)));
pub fn load_and_resize_to_fit(path: &Path) -> Result<EncodedImage, ImageProcessingError> {
load_for_prompt(path, PromptImageMode::ResizeToFit)
}
pub fn load_for_prompt(
pub fn load_for_prompt_bytes(
path: &Path,
file_bytes: Vec<u8>,
mode: PromptImageMode,
) -> Result<EncodedImage, ImageProcessingError> {
let path_buf = path.to_path_buf();
let file_bytes = read_file_bytes(path, &path_buf)?;
let key = ImageCacheKey {
digest: sha1_digest(&file_bytes),
mode,
@@ -136,24 +131,6 @@ fn can_preserve_source_bytes(format: ImageFormat) -> bool {
)
}
fn read_file_bytes(path: &Path, path_for_error: &Path) -> Result<Vec<u8>, ImageProcessingError> {
match tokio::runtime::Handle::try_current() {
// If we're inside a Tokio runtime, avoid block_on (it panics on worker threads).
// Use block_in_place and do a standard blocking read safely.
Ok(_) => tokio::task::block_in_place(|| std::fs::read(path)).map_err(|source| {
ImageProcessingError::Read {
path: path_for_error.to_path_buf(),
source,
}
}),
// Outside a runtime, just read synchronously.
Err(_) => std::fs::read(path).map_err(|source| ImageProcessingError::Read {
path: path_for_error.to_path_buf(),
source,
}),
}
}
fn encode_image(
image: &DynamicImage,
preferred_format: ImageFormat,
@@ -223,11 +200,20 @@ fn format_to_mime(format: ImageFormat) -> String {
#[cfg(test)]
mod tests {
use std::io::Cursor;
use super::*;
use image::GenericImageView;
use image::ImageBuffer;
use image::Rgba;
use tempfile::NamedTempFile;
fn image_bytes(image: &ImageBuffer<Rgba<u8>, Vec<u8>>, format: ImageFormat) -> Vec<u8> {
let mut encoded = Cursor::new(Vec::new());
DynamicImage::ImageRgba8(image.clone())
.write_to(&mut encoded, format)
.expect("encode image to bytes");
encoded.into_inner()
}
#[tokio::test(flavor = "multi_thread")]
async fn returns_original_image_when_within_bounds() {
@@ -235,14 +221,15 @@ mod tests {
(ImageFormat::Png, "image/png"),
(ImageFormat::WebP, "image/webp"),
] {
let temp_file = NamedTempFile::new().expect("temp file");
let image = ImageBuffer::from_pixel(64, 32, Rgba([10u8, 20, 30, 255]));
image
.save_with_format(temp_file.path(), format)
.expect("write image to temp file");
let original_bytes = image_bytes(&image, format);
let original_bytes = std::fs::read(temp_file.path()).expect("read written image");
let encoded = load_and_resize_to_fit(temp_file.path()).expect("process image");
let encoded = load_for_prompt_bytes(
Path::new("in-memory-image"),
original_bytes.clone(),
PromptImageMode::ResizeToFit,
)
.expect("process image");
assert_eq!(encoded.width, 64);
assert_eq!(encoded.height, 32);
@@ -257,13 +244,15 @@ mod tests {
(ImageFormat::Png, "image/png"),
(ImageFormat::WebP, "image/webp"),
] {
let temp_file = NamedTempFile::new().expect("temp file");
let image = ImageBuffer::from_pixel(4096, 2048, Rgba([200u8, 10, 10, 255]));
image
.save_with_format(temp_file.path(), format)
.expect("write image to temp file");
let original_bytes = image_bytes(&image, format);
let processed = load_and_resize_to_fit(temp_file.path()).expect("process image");
let processed = load_for_prompt_bytes(
Path::new("in-memory-image"),
original_bytes,
PromptImageMode::ResizeToFit,
)
.expect("process image");
assert!(processed.width <= MAX_WIDTH);
assert!(processed.height <= MAX_HEIGHT);
@@ -281,15 +270,15 @@ mod tests {
#[tokio::test(flavor = "multi_thread")]
async fn preserves_large_image_in_original_mode() {
let temp_file = NamedTempFile::new().expect("temp file");
let image = ImageBuffer::from_pixel(4096, 2048, Rgba([180u8, 30, 30, 255]));
image
.save_with_format(temp_file.path(), ImageFormat::Png)
.expect("write png to temp file");
let original_bytes = image_bytes(&image, ImageFormat::Png);
let original_bytes = std::fs::read(temp_file.path()).expect("read written image");
let processed =
load_for_prompt(temp_file.path(), PromptImageMode::Original).expect("process image");
let processed = load_for_prompt_bytes(
Path::new("in-memory-image"),
original_bytes.clone(),
PromptImageMode::Original,
)
.expect("process image");
assert_eq!(processed.width, 4096);
assert_eq!(processed.height, 2048);
@@ -299,10 +288,12 @@ mod tests {
#[tokio::test(flavor = "multi_thread")]
async fn fails_cleanly_for_invalid_images() {
let temp_file = NamedTempFile::new().expect("temp file");
std::fs::write(temp_file.path(), b"not an image").expect("write bytes");
let err = load_and_resize_to_fit(temp_file.path()).expect_err("invalid image should fail");
let err = load_for_prompt_bytes(
Path::new("in-memory-image"),
b"not an image".to_vec(),
PromptImageMode::ResizeToFit,
)
.expect_err("invalid image should fail");
match err {
ImageProcessingError::Decode { .. } => {}
_ => panic!("unexpected error variant"),
@@ -315,20 +306,25 @@ mod tests {
IMAGE_CACHE.clear();
}
let temp_file = NamedTempFile::new().expect("temp file");
let first_image = ImageBuffer::from_pixel(32, 16, Rgba([20u8, 120, 220, 255]));
first_image
.save_with_format(temp_file.path(), ImageFormat::Png)
.expect("write initial image");
let first_bytes = image_bytes(&first_image, ImageFormat::Png);
let first = load_and_resize_to_fit(temp_file.path()).expect("process first image");
let first = load_for_prompt_bytes(
Path::new("in-memory-image"),
first_bytes,
PromptImageMode::ResizeToFit,
)
.expect("process first image");
let second_image = ImageBuffer::from_pixel(96, 48, Rgba([50u8, 60, 70, 255]));
second_image
.save_with_format(temp_file.path(), ImageFormat::Png)
.expect("write updated image");
let second_bytes = image_bytes(&second_image, ImageFormat::Png);
let second = load_and_resize_to_fit(temp_file.path()).expect("process updated image");
let second = load_for_prompt_bytes(
Path::new("in-memory-image"),
second_bytes,
PromptImageMode::ResizeToFit,
)
.expect("process updated image");
assert_eq!(first.width, 32);
assert_eq!(first.height, 16);